ServiceAccountsListPage.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import { css, cx } from '@emotion/css';
  2. import pluralize from 'pluralize';
  3. import React, { useEffect } from 'react';
  4. import { connect, ConnectedProps } from 'react-redux';
  5. import { GrafanaTheme2, OrgRole } from '@grafana/data';
  6. import { ConfirmModal, FilterInput, LinkButton, RadioButtonGroup, useStyles2 } from '@grafana/ui';
  7. import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
  8. import Page from 'app/core/components/Page/Page';
  9. import PageLoader from 'app/core/components/PageLoader/PageLoader';
  10. import { contextSrv } from 'app/core/core';
  11. import { getNavModel } from 'app/core/selectors/navModel';
  12. import { StoreState, ServiceAccountDTO, AccessControlAction } from 'app/types';
  13. import ServiceAccountListItem from './ServiceAccountsListItem';
  14. import {
  15. changeFilter,
  16. changeQuery,
  17. fetchACOptions,
  18. fetchServiceAccounts,
  19. removeServiceAccount,
  20. updateServiceAccount,
  21. setServiceAccountToRemove,
  22. } from './state/actions';
  23. interface OwnProps {}
  24. type Props = OwnProps & ConnectedProps<typeof connector>;
  25. function mapStateToProps(state: StoreState) {
  26. return {
  27. navModel: getNavModel(state.navIndex, 'serviceaccounts'),
  28. ...state.serviceAccounts,
  29. };
  30. }
  31. const mapDispatchToProps = {
  32. fetchServiceAccounts,
  33. fetchACOptions,
  34. updateServiceAccount,
  35. removeServiceAccount,
  36. setServiceAccountToRemove,
  37. changeFilter,
  38. changeQuery,
  39. };
  40. const connector = connect(mapStateToProps, mapDispatchToProps);
  41. const ServiceAccountsListPage = ({
  42. fetchServiceAccounts,
  43. removeServiceAccount,
  44. fetchACOptions,
  45. updateServiceAccount,
  46. setServiceAccountToRemove,
  47. navModel,
  48. serviceAccounts,
  49. isLoading,
  50. roleOptions,
  51. builtInRoles,
  52. changeFilter,
  53. changeQuery,
  54. query,
  55. filters,
  56. serviceAccountToRemove,
  57. }: Props): JSX.Element => {
  58. const styles = useStyles2(getStyles);
  59. useEffect(() => {
  60. const fetchData = async () => {
  61. await fetchServiceAccounts();
  62. if (contextSrv.licensedAccessControlEnabled()) {
  63. await fetchACOptions();
  64. }
  65. };
  66. fetchData();
  67. }, [fetchServiceAccounts, fetchACOptions]);
  68. const onRoleChange = async (role: OrgRole, serviceAccount: ServiceAccountDTO) => {
  69. const updatedServiceAccount = { ...serviceAccount, role: role };
  70. await updateServiceAccount(updatedServiceAccount);
  71. // need to refetch to display the new value in the list
  72. await fetchServiceAccounts();
  73. if (contextSrv.licensedAccessControlEnabled()) {
  74. fetchACOptions();
  75. }
  76. };
  77. return (
  78. <Page navModel={navModel}>
  79. <Page.Contents>
  80. <h2>Service accounts</h2>
  81. <div className="page-action-bar" style={{ justifyContent: 'flex-end' }}>
  82. <FilterInput
  83. placeholder="Search service account by name."
  84. autoFocus={true}
  85. value={query}
  86. onChange={changeQuery}
  87. />
  88. <RadioButtonGroup
  89. options={[
  90. { label: 'All service accounts', value: false },
  91. { label: 'Expired tokens', value: true },
  92. ]}
  93. onChange={(value) => changeFilter({ name: 'expiredTokens', value })}
  94. value={filters.find((f) => f.name === 'expiredTokens')?.value}
  95. className={styles.filter}
  96. />
  97. {serviceAccounts.length !== 0 && contextSrv.hasPermission(AccessControlAction.ServiceAccountsCreate) && (
  98. <LinkButton href="org/serviceaccounts/create" variant="primary">
  99. Add service account
  100. </LinkButton>
  101. )}
  102. </div>
  103. {isLoading && <PageLoader />}
  104. {!isLoading && serviceAccounts.length === 0 && (
  105. <>
  106. <EmptyListCTA
  107. title="You haven't created any service accounts yet."
  108. buttonIcon="key-skeleton-alt"
  109. buttonLink="org/serviceaccounts/create"
  110. buttonTitle="Add service account"
  111. buttonDisabled={!contextSrv.hasPermission(AccessControlAction.ServiceAccountsCreate)}
  112. proTip="Remember, you can provide specific permissions for API access to other applications."
  113. proTipLink=""
  114. proTipLinkTitle=""
  115. proTipTarget="_blank"
  116. />
  117. </>
  118. )}
  119. {!isLoading && serviceAccounts.length !== 0 && (
  120. <>
  121. <div className={cx(styles.table, 'admin-list-table')}>
  122. <table className="filter-table form-inline filter-table--hover">
  123. <thead>
  124. <tr>
  125. <th></th>
  126. <th>Account</th>
  127. <th>ID</th>
  128. <th>Roles</th>
  129. <th>Status</th>
  130. <th>Tokens</th>
  131. <th style={{ width: '34px' }} />
  132. </tr>
  133. </thead>
  134. <tbody>
  135. {serviceAccounts.map((serviceAccount: ServiceAccountDTO) => (
  136. <ServiceAccountListItem
  137. serviceAccount={serviceAccount}
  138. key={serviceAccount.id}
  139. builtInRoles={builtInRoles}
  140. roleOptions={roleOptions}
  141. onRoleChange={onRoleChange}
  142. onSetToRemove={setServiceAccountToRemove}
  143. />
  144. ))}
  145. </tbody>
  146. </table>
  147. </div>
  148. </>
  149. )}
  150. {serviceAccountToRemove && (
  151. <ConfirmModal
  152. body={
  153. <div>
  154. Are you sure you want to delete &apos;{serviceAccountToRemove.name}&apos;
  155. {Boolean(serviceAccountToRemove.tokens) &&
  156. ` and ${serviceAccountToRemove.tokens} accompanying ${pluralize(
  157. 'token',
  158. serviceAccountToRemove.tokens
  159. )}`}
  160. ?
  161. </div>
  162. }
  163. confirmText="Delete"
  164. title="Delete service account"
  165. onDismiss={() => {
  166. setServiceAccountToRemove(null);
  167. }}
  168. isOpen={true}
  169. onConfirm={() => {
  170. removeServiceAccount(serviceAccountToRemove.id);
  171. setServiceAccountToRemove(null);
  172. }}
  173. />
  174. )}
  175. </Page.Contents>
  176. </Page>
  177. );
  178. };
  179. export const getStyles = (theme: GrafanaTheme2) => {
  180. return {
  181. table: css`
  182. margin-top: ${theme.spacing(3)};
  183. `,
  184. filter: css`
  185. margin: 0 ${theme.spacing(1)};
  186. `,
  187. iconRow: css`
  188. svg {
  189. margin-left: ${theme.spacing(0.5)};
  190. }
  191. `,
  192. row: css`
  193. display: flex;
  194. align-items: center;
  195. height: 100% !important;
  196. a {
  197. padding: ${theme.spacing(0.5)} 0 !important;
  198. }
  199. `,
  200. unitTooltip: css`
  201. display: flex;
  202. flex-direction: column;
  203. `,
  204. unitItem: css`
  205. cursor: pointer;
  206. padding: ${theme.spacing(0.5)} 0;
  207. margin-right: ${theme.spacing(1)};
  208. `,
  209. disabled: css`
  210. color: ${theme.colors.text.disabled};
  211. `,
  212. link: css`
  213. color: inherit;
  214. cursor: pointer;
  215. text-decoration: underline;
  216. `,
  217. };
  218. };
  219. export default connector(ServiceAccountsListPage);