diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 505b99f5004..1cfcde48ad3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -496,7 +496,6 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/features/actions/ @grafana/dataviz-squad /public/app/features/auth-config/ @grafana/identity-squad /public/app/features/annotations/ @grafana/dashboards-squad -/public/app/features/api-keys/ @grafana/identity-squad /public/app/features/canvas/ @grafana/dataviz-squad /public/app/features/geo/ @grafana/dataviz-squad /public/app/features/visualization/data-hover/ @grafana/dataviz-squad diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index 99877d65c90..4a545d2c350 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -4,7 +4,6 @@ import { AnyAction, combineReducers } from 'redux'; import sharedReducers from 'app/core/reducers'; import ldapReducers from 'app/features/admin/state/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; -import apiKeysReducers from 'app/features/api-keys/state/reducers'; import authConfigReducers from 'app/features/auth-config/state/reducers'; import { browseDashboardsAPI } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import browseDashboardsReducers from 'app/features/browse-dashboards/state/slice'; @@ -42,7 +41,6 @@ const rootReducers = { ...sharedReducers, ...alertingReducers, ...teamsReducers, - ...apiKeysReducers, ...foldersReducers, ...dashboardReducers, ...exploreReducers, diff --git a/public/app/core/utils/navBarItem-translations.ts b/public/app/core/utils/navBarItem-translations.ts index d32ffcc1840..819cfa6c786 100644 --- a/public/app/core/utils/navBarItem-translations.ts +++ b/public/app/core/utils/navBarItem-translations.ts @@ -117,8 +117,6 @@ export function getNavTitle(navId: string | undefined) { return t('nav.plugins.title', 'Plugins'); case 'org-settings': return t('nav.org-settings.title', 'Default preferences'); - case 'apikeys': - return t('nav.api-keys.title', 'API keys'); case 'serviceaccounts': return t('nav.service-accounts.title', 'Service accounts'); case 'admin': @@ -269,8 +267,6 @@ export function getNavSubTitle(navId: string | undefined) { return t('nav.plugins.subtitle', 'Extend the Grafana experience with plugins'); case 'org-settings': return t('nav.org-settings.subtitle', 'Manage preferences across an organization'); - case 'apikeys': - return t('nav.api-keys.subtitle', 'Manage and create API keys that are used to interact with Grafana HTTP APIs'); case 'serviceaccounts': return t('nav.service-accounts.subtitle', 'Use service accounts to run automated workloads in Grafana'); case 'groupsync': diff --git a/public/app/features/api-keys/ApiKeysActionBar.tsx b/public/app/features/api-keys/ApiKeysActionBar.tsx deleted file mode 100644 index e200fca4ba0..00000000000 --- a/public/app/features/api-keys/ApiKeysActionBar.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { useTranslate } from '@grafana/i18n'; -import { FilterInput, InlineField } from '@grafana/ui'; - -interface Props { - searchQuery: string; - disabled: boolean; - onSearchChange: (value: string) => void; -} - -export const ApiKeysActionBar = ({ searchQuery, disabled, onSearchChange }: Props) => { - const { t } = useTranslate(); - - return ( -
- - - -
- ); -}; diff --git a/public/app/features/api-keys/ApiKeysPage.test.tsx b/public/app/features/api-keys/ApiKeysPage.test.tsx deleted file mode 100644 index 728508a14e8..00000000000 --- a/public/app/features/api-keys/ApiKeysPage.test.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { render, screen, within } from '@testing-library/react'; -import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event'; -import { TestProvider } from 'test/helpers/TestProvider'; - -import { ApiKey, OrgRole } from 'app/types'; - -import { mockToolkitActionCreator } from '../../../test/core/redux/mocks'; -import { silenceConsoleOutput } from '../../../test/core/utils/silenceConsoleOutput'; - -import { ApiKeysPageUnconnected, Props } from './ApiKeysPage'; -import { getMultipleMockKeys } from './__mocks__/apiKeysMock'; -import { setSearchQuery } from './state/reducers'; - -jest.mock('app/core/core', () => { - return { - contextSrv: { - hasPermission: () => true, - hasPermissionInMetadata: () => true, - }, - }; -}); - -const setup = (propOverrides: Partial) => { - const loadApiKeysMock = jest.fn(); - const deleteApiKeyMock = jest.fn(); - const migrateApiKeyMock = jest.fn(); - const addApiKeyMock = jest.fn(); - const migrateAllMock = jest.fn(); - const toggleIncludeExpiredMock = jest.fn(); - const setSearchQueryMock = mockToolkitActionCreator(setSearchQuery); - const props: Props = { - apiKeys: [] as ApiKey[], - searchQuery: '', - hasFetched: false, - loadApiKeys: loadApiKeysMock, - deleteApiKey: deleteApiKeyMock, - setSearchQuery: setSearchQueryMock, - migrateApiKey: migrateApiKeyMock, - migrateAll: migrateAllMock, - apiKeysCount: 0, - timeZone: 'utc', - includeExpired: false, - includeExpiredDisabled: false, - toggleIncludeExpired: toggleIncludeExpiredMock, - canCreate: true, - migrationResult: undefined, - }; - - Object.assign(props, propOverrides); - - const { rerender } = render( - - - - ); - return { - rerender: (element: JSX.Element) => rerender({element}), - props, - loadApiKeysMock, - setSearchQueryMock, - deleteApiKeyMock, - addApiKeyMock, - toggleIncludeExpiredMock, - }; -}; - -describe('ApiKeysPage', () => { - silenceConsoleOutput(); - describe('when mounted', () => { - it('then it should call loadApiKeys', () => { - const { loadApiKeysMock } = setup({}); - expect(loadApiKeysMock).toHaveBeenCalledTimes(1); - }); - }); - - describe('when loading', () => { - it('then should show Loading message', () => { - setup({ hasFetched: false }); - expect(screen.getByText(/loading \.\.\./i)).toBeInTheDocument(); - }); - }); - - describe('when there are API keys', () => { - it('then it should render API keys table', async () => { - const apiKeys = [ - { id: 1, name: 'First', role: OrgRole.Admin, secondsToLive: 60, expiration: '2021-01-01' }, - { id: 2, name: 'Second', role: OrgRole.Editor, secondsToLive: 60, expiration: '2021-01-02' }, - { id: 3, name: 'Third', role: OrgRole.Viewer, secondsToLive: 0, expiration: undefined }, - ]; - setup({ apiKeys, apiKeysCount: apiKeys.length, hasFetched: true }); - expect(screen.getByRole('table')).toBeInTheDocument(); - expect(screen.getAllByRole('row').length).toBe(4); - expect(screen.getByRole('row', { name: /first admin 2021-01-01 00:00:00/i })).toBeInTheDocument(); - expect(screen.getByRole('row', { name: /second editor 2021-01-02 00:00:00/i })).toBeInTheDocument(); - expect(screen.getByRole('row', { name: /third viewer no expiration date/i })).toBeInTheDocument(); - }); - }); - - describe('when a user toggles the Show expired toggle', () => { - it('then it should dispatch toggleIncludeExpired', async () => { - const apiKeys = getMultipleMockKeys(3); - const { toggleIncludeExpiredMock } = setup({ apiKeys, apiKeysCount: apiKeys.length, hasFetched: true }); - - await toggleShowExpired(); - expect(toggleIncludeExpiredMock).toHaveBeenCalledTimes(1); - }); - }); - - describe('when a user searches for an API key', () => { - it('then it should dispatch setSearchQuery with correct parameters', async () => { - const apiKeys = getMultipleMockKeys(3); - const { setSearchQueryMock } = setup({ apiKeys, apiKeysCount: apiKeys.length, hasFetched: true }); - - setSearchQueryMock.mockClear(); - expect(screen.getByPlaceholderText(/search keys/i)).toBeInTheDocument(); - await userEvent.type(screen.getByPlaceholderText(/search keys/i), 'First'); - expect(setSearchQueryMock).toHaveBeenCalledTimes(5); - }); - }); - - describe('when a user deletes an API key', () => { - it('then it should dispatch deleteApi with correct parameters', async () => { - const apiKeys = [ - { id: 1, name: 'First', role: OrgRole.Admin, secondsToLive: 60, expiration: '2021-01-01' }, - { id: 2, name: 'Second', role: OrgRole.Editor, secondsToLive: 60, expiration: '2021-01-02' }, - { id: 3, name: 'Third', role: OrgRole.Viewer, secondsToLive: 0, expiration: undefined }, - ]; - const { deleteApiKeyMock } = setup({ apiKeys, apiKeysCount: apiKeys.length, hasFetched: true }); - const firstRow = screen.getByRole('row', { name: /first admin 2021-01-01 00:00:00/i }); - const secondRow = screen.getByRole('row', { name: /second editor 2021-01-02 00:00:00/i }); - - deleteApiKeyMock.mockClear(); - expect(within(firstRow).getByLabelText('Delete API key')).toBeInTheDocument(); - await userEvent.click(within(firstRow).getByLabelText('Delete API key')); - - expect(within(firstRow).getByRole('button', { name: /delete$/i })).toBeInTheDocument(); - await userEvent.click(within(firstRow).getByRole('button', { name: /delete$/i })); - expect(deleteApiKeyMock).toHaveBeenCalledTimes(1); - expect(deleteApiKeyMock).toHaveBeenCalledWith(1); - - await toggleShowExpired(); - - deleteApiKeyMock.mockClear(); - expect(within(secondRow).getByLabelText('Delete API key')).toBeInTheDocument(); - await userEvent.click(within(secondRow).getByLabelText('Delete API key')); - expect(within(secondRow).getByRole('button', { name: /delete$/i })).toBeInTheDocument(); - await userEvent.click(within(secondRow).getByRole('button', { name: /delete$/i }), { - pointerEventsCheck: PointerEventsCheckLevel.Never, - }); - expect(deleteApiKeyMock).toHaveBeenCalledTimes(1); - expect(deleteApiKeyMock).toHaveBeenCalledWith(2); - }); - }); -}); - -async function toggleShowExpired() { - expect(screen.queryByLabelText(/include expired keys/i)).toBeInTheDocument(); - await userEvent.click(screen.getByLabelText(/include expired keys/i)); -} diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx deleted file mode 100644 index c54d7a6e26e..00000000000 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import { PureComponent } from 'react'; -import * as React from 'react'; -import { connect, ConnectedProps } from 'react-redux'; - -// Utils -import { Trans } from '@grafana/i18n'; -import { t } from '@grafana/i18n/internal'; -import { InlineField, InlineSwitch, Modal, Button, EmptyState } from '@grafana/ui'; -import { Page } from 'app/core/components/Page/Page'; -import { contextSrv } from 'app/core/core'; -import { getTimeZone } from 'app/features/profile/state/selectors'; -import { AccessControlAction, ApiKey, ApikeyMigrationResult, StoreState } from 'app/types'; - -import { ApiKeysActionBar } from './ApiKeysActionBar'; -import { ApiKeysTable } from './ApiKeysTable'; -import { MigrateToServiceAccountsCard } from './MigrateToServiceAccountsCard'; -import { deleteApiKey, migrateApiKey, migrateAll, loadApiKeys, toggleIncludeExpired } from './state/actions'; -import { setSearchQuery } from './state/reducers'; -import { getApiKeys, getApiKeysCount, getIncludeExpired, getIncludeExpiredDisabled } from './state/selectors'; - -function mapStateToProps(state: StoreState) { - const canCreate = contextSrv.hasPermission(AccessControlAction.ActionAPIKeysCreate); - return { - apiKeys: getApiKeys(state.apiKeys), - searchQuery: state.apiKeys.searchQuery, - apiKeysCount: getApiKeysCount(state.apiKeys), - hasFetched: state.apiKeys.hasFetched, - timeZone: getTimeZone(state.user), - includeExpired: getIncludeExpired(state.apiKeys), - includeExpiredDisabled: getIncludeExpiredDisabled(state.apiKeys), - canCreate: canCreate, - migrationResult: state.apiKeys.migrationResult, - }; -} - -const defaultPageProps = { - navId: 'apikeys', -}; - -const mapDispatchToProps = { - loadApiKeys, - deleteApiKey, - migrateApiKey, - migrateAll, - setSearchQuery, - toggleIncludeExpired, -}; - -const connector = connect(mapStateToProps, mapDispatchToProps); - -interface OwnProps {} - -export type Props = OwnProps & ConnectedProps; - -interface State { - showMigrationResult: boolean; -} - -export class ApiKeysPageUnconnected extends PureComponent { - constructor(props: Props) { - super(props); - this.state = { - showMigrationResult: false, - }; - } - - componentDidMount() { - this.fetchApiKeys(); - } - - async fetchApiKeys() { - await this.props.loadApiKeys(); - } - - onDeleteApiKey = (key: ApiKey) => { - this.props.deleteApiKey(key.id!); - }; - - onMigrateApiKey = (key: ApiKey) => { - this.props.migrateApiKey(key.id!); - }; - - onSearchQueryChange = (value: string) => { - this.props.setSearchQuery(value); - }; - - onIncludeExpiredChange = (event: React.SyntheticEvent) => { - this.props.toggleIncludeExpired(); - }; - - onMigrateApiKeys = async () => { - try { - await this.props.migrateAll(); - this.setState({ - showMigrationResult: true, - }); - } catch (err) { - console.error(err); - } - }; - - dismissModal = async () => { - this.setState({ showMigrationResult: false }); - }; - - render() { - const { - hasFetched, - apiKeysCount, - apiKeys, - searchQuery, - timeZone, - includeExpired, - includeExpiredDisabled, - canCreate, - migrationResult, - } = this.props; - - const showTable = apiKeysCount > 0; - return ( - - - - {showTable ? ( - - ) : null} - - - - {apiKeys.length > 0 ? ( - - ) : ( - - )} - - {migrationResult && ( - - )} - - ); - } -} -export type MigrationSummaryProps = { - visible: boolean; - data: ApikeyMigrationResult; - onDismiss: () => void; -}; - -const styles: { [key: string]: React.CSSProperties } = { - migrationSummary: { - padding: '20px', - }, - infoText: { - color: '#007bff', - }, - summaryDetails: { - marginTop: '20px', - }, - summaryParagraph: { - margin: '10px 0', - }, -}; - -export const MigrationSummary: React.FC = ({ visible, data, onDismiss }) => { - return ( - - {data.failedApikeyIDs.length === 0 && ( -
-

- Migration Successful! -

-

- - Total: - {'{{total}}'} - -

-

- - Migrated: - {'{{migrated}}'} - -

-
- )} - {data.failedApikeyIDs.length !== 0 && ( -
-

- - Migration complete! Please note, while there might be a few API keys flagged as `failed migrations`, rest - assured, all of your API keys are fully functional and operational. Please try again or contact support. - -

-
-

- - Total: - {'{{total}}'} - -

-

- - Migrated: - {'{{migrated}}'} - -

-

- - Failed: - {'{{failed}}'} - -

-

- - Failed api key IDs: - {'{{ids}}'} - -

-

- - Failed details: - {'{{details}}'} - -

-
- )} - - - -
- ); -}; - -const ApiKeysPage = connector(ApiKeysPageUnconnected); -export default ApiKeysPage; diff --git a/public/app/features/api-keys/ApiKeysTable.tsx b/public/app/features/api-keys/ApiKeysTable.tsx deleted file mode 100644 index d758530ed6b..00000000000 --- a/public/app/features/api-keys/ApiKeysTable.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { css } from '@emotion/css'; - -import { dateTimeFormat, GrafanaTheme2, TimeZone } from '@grafana/data'; -import { Trans, useTranslate } from '@grafana/i18n'; -import { Button, DeleteButton, Icon, Stack, Tooltip, useTheme2 } from '@grafana/ui'; -import { contextSrv } from 'app/core/core'; -import { AccessControlAction } from 'app/types'; - -import { ApiKey } from '../../types'; - -interface Props { - apiKeys: ApiKey[]; - timeZone: TimeZone; - onDelete: (apiKey: ApiKey) => void; - onMigrate: (apiKey: ApiKey) => void; -} - -export const ApiKeysTable = ({ apiKeys, timeZone, onDelete, onMigrate }: Props) => { - const theme = useTheme2(); - const { t } = useTranslate(); - const styles = getStyles(theme); - - return ( - - - - - - - - - - {apiKeys.length > 0 ? ( - - {apiKeys.map((key) => { - const isExpired = Boolean(key.expiration && Date.now() > new Date(key.expiration).getTime()); - return ( - - - - - - - - ); - })} - - ) : null} -
- Name - - Role - - Expires - - Last used at - -
{key.name}{key.role} - {formatDate(key.expiration, timeZone)} - {isExpired && ( - - - - - - )} - {formatLastUsedAtDate(timeZone, key.lastUsedAt)} - - - onDelete(key)} - disabled={!contextSrv.hasPermissionInMetadata(AccessControlAction.ActionAPIKeysDelete, key)} - /> - -
- ); -}; - -function formatLastUsedAtDate(timeZone: TimeZone, lastUsedAt?: string): string { - if (!lastUsedAt) { - return 'Never'; - } - return dateTimeFormat(lastUsedAt, { timeZone }); -} - -function formatDate(expiration: string | undefined, timeZone: TimeZone): string { - if (!expiration) { - return 'No expiration date'; - } - return dateTimeFormat(expiration, { timeZone }); -} - -const getStyles = (theme: GrafanaTheme2) => ({ - tableRow: (isExpired: boolean) => - css({ - color: isExpired ? theme.colors.text.secondary : theme.colors.text.primary, - }), - tooltipContainer: css({ - marginLeft: theme.spacing(1), - }), -}); diff --git a/public/app/features/api-keys/MigrateToServiceAccountsCard.tsx b/public/app/features/api-keys/MigrateToServiceAccountsCard.tsx deleted file mode 100644 index c45c33a58df..00000000000 --- a/public/app/features/api-keys/MigrateToServiceAccountsCard.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { css } from '@emotion/css'; -import { useState } from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { Trans, useTranslate } from '@grafana/i18n'; -import { Alert, Button, ConfirmModal, useStyles2 } from '@grafana/ui'; - -interface Props { - onMigrate: () => void; - apikeysCount: number; - disabled?: boolean; -} - -export const MigrateToServiceAccountsCard = ({ onMigrate, apikeysCount, disabled }: Props): JSX.Element => { - const [isModalOpen, setIsModalOpen] = useState(false); - const styles = useStyles2(getStyles); - const { t } = useTranslate(); - const docsLink = ( - - - Find out more about the migration here. - - - ); - const migrationBoxDesc = ( - - - Migrating all API keys will hide the API keys tab. - - - ); - - return ( - <> - {apikeysCount > 0 && ( - -
- - API keys are deprecated and will be removed from Grafana on Jan 31, 2025. Each API key will be migrated - into a service account with a token and will continue to work as they were. We encourage you to migrate - your API keys to service accounts now. {''} - -
-
- - setIsModalOpen(false)} - confirmVariant="primary" - confirmButtonVariant="primary" - /> -
-
- )} - {apikeysCount === 0 && ( - <> - -
- - No API keys were found. If you reload the browser, this page will not be available anymore. - -
-
- - )} - - ); -}; - -export const getStyles = (theme: GrafanaTheme2) => ({ - text: css({ - marginBottom: theme.spacing(2), - }), - actionRow: css({ - display: 'flex', - alignItems: 'center', - }), - actionButton: css({ - marginRight: theme.spacing(2), - }), -}); diff --git a/public/app/features/api-keys/__mocks__/apiKeysMock.ts b/public/app/features/api-keys/__mocks__/apiKeysMock.ts deleted file mode 100644 index c244cb6af8f..00000000000 --- a/public/app/features/api-keys/__mocks__/apiKeysMock.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ApiKey, OrgRole } from 'app/types'; - -export const getMultipleMockKeys = (numberOfKeys: number): ApiKey[] => { - const keys: ApiKey[] = []; - - for (let i = 1; i <= numberOfKeys; i++) { - keys.push({ - id: i, - name: `test-${i}`, - role: OrgRole.Viewer, - secondsToLive: 100, - expiration: '2019-06-04', - }); - } - - return keys; -}; - -export const getMockKey = (): ApiKey => { - return { - id: 1, - name: 'test', - role: OrgRole.Admin, - secondsToLive: 200, - expiration: '2019-06-04', - }; -}; diff --git a/public/app/features/api-keys/state/actions.ts b/public/app/features/api-keys/state/actions.ts deleted file mode 100644 index f81a8310d48..00000000000 --- a/public/app/features/api-keys/state/actions.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { getBackendSrv } from 'app/core/services/backend_srv'; -import { ThunkResult } from 'app/types'; - -import { apiKeysLoaded, includeExpiredToggled, isFetching, setMigrationResult } from './reducers'; - -export function loadApiKeys(): ThunkResult { - return async (dispatch) => { - dispatch(isFetching()); - const [keys, keysIncludingExpired] = await Promise.all([ - getBackendSrv().get('/api/auth/keys?includeExpired=false&accesscontrol=true'), - getBackendSrv().get('/api/auth/keys?includeExpired=true&accesscontrol=true'), - ]); - dispatch(apiKeysLoaded({ keys, keysIncludingExpired })); - }; -} - -export function deleteApiKey(id: number): ThunkResult { - return async (dispatch) => { - getBackendSrv() - .delete(`/api/auth/keys/${id}`) - .then(() => dispatch(loadApiKeys())); - }; -} - -export function migrateApiKey(id: number): ThunkResult { - return async (dispatch) => { - try { - await getBackendSrv().post(`/api/serviceaccounts/migrate/${id}`); - } finally { - dispatch(loadApiKeys()); - } - }; -} - -export function migrateAll(): ThunkResult { - return async (dispatch) => { - try { - const payload = await getBackendSrv().post('/api/serviceaccounts/migrate'); - dispatch(setMigrationResult(payload)); - } finally { - dispatch(loadApiKeys()); - } - }; -} - -export function toggleIncludeExpired(): ThunkResult { - return (dispatch) => { - dispatch(includeExpiredToggled()); - }; -} diff --git a/public/app/features/api-keys/state/reducers.test.ts b/public/app/features/api-keys/state/reducers.test.ts deleted file mode 100644 index b4048dd194f..00000000000 --- a/public/app/features/api-keys/state/reducers.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { reducerTester } from '../../../../test/core/redux/reducerTester'; -import { ApiKeysState } from '../../../types'; -import { getMultipleMockKeys } from '../__mocks__/apiKeysMock'; - -import { - apiKeysLoaded, - apiKeysReducer, - includeExpiredToggled, - initialApiKeysState, - isFetching, - setSearchQuery, -} from './reducers'; - -describe('API Keys reducer', () => { - it('should set keys', () => { - reducerTester() - .givenReducer(apiKeysReducer, { ...initialApiKeysState }) - .whenActionIsDispatched( - apiKeysLoaded({ keys: getMultipleMockKeys(4), keysIncludingExpired: getMultipleMockKeys(6) }) - ) - .thenStateShouldEqual({ - ...initialApiKeysState, - keys: getMultipleMockKeys(4), - keysIncludingExpired: getMultipleMockKeys(6), - hasFetched: true, - }); - }); - - it('should set search query', () => { - reducerTester() - .givenReducer(apiKeysReducer, { ...initialApiKeysState }) - .whenActionIsDispatched(setSearchQuery('test query')) - .thenStateShouldEqual({ - ...initialApiKeysState, - searchQuery: 'test query', - }); - }); - - it('should toggle the includeExpired state', () => { - reducerTester() - .givenReducer(apiKeysReducer, { ...initialApiKeysState }) - .whenActionIsDispatched(includeExpiredToggled()) - .thenStateShouldEqual({ - ...initialApiKeysState, - includeExpired: true, - }); - }); - - it('should set state when fetching', () => { - reducerTester() - .givenReducer(apiKeysReducer, { ...initialApiKeysState }) - .whenActionIsDispatched(isFetching()) - .thenStateShouldEqual({ - ...initialApiKeysState, - hasFetched: false, - }); - }); -}); diff --git a/public/app/features/api-keys/state/reducers.ts b/public/app/features/api-keys/state/reducers.ts deleted file mode 100644 index 6e5ad5d277e..00000000000 --- a/public/app/features/api-keys/state/reducers.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit'; - -import { ApiKeysState } from 'app/types'; - -export const initialApiKeysState: ApiKeysState = { - hasFetched: false, - includeExpired: false, - keys: [], - keysIncludingExpired: [], - searchQuery: '', - migrationResult: { - total: 0, - migrated: 0, - failed: 0, - failedApikeyIDs: [0], - failedDetails: [], - }, -}; - -const apiKeysSlice = createSlice({ - name: 'apiKeys', - initialState: initialApiKeysState, - reducers: { - apiKeysLoaded: (state, action): ApiKeysState => { - const { keys, keysIncludingExpired } = action.payload; - const includeExpired = - action.payload.keys.length === 0 && action.payload.keysIncludingExpired.length > 0 - ? true - : state.includeExpired; - return { ...state, hasFetched: true, keys, keysIncludingExpired, includeExpired }; - }, - setSearchQuery: (state, action): ApiKeysState => { - return { ...state, searchQuery: action.payload }; - }, - includeExpiredToggled: (state): ApiKeysState => { - return { ...state, includeExpired: !state.includeExpired }; - }, - isFetching: (state): ApiKeysState => { - return { ...state, hasFetched: false }; - }, - setMigrationResult: (state, action): ApiKeysState => { - return { ...state, migrationResult: action.payload }; - }, - }, -}); - -export const { apiKeysLoaded, includeExpiredToggled, isFetching, setSearchQuery, setMigrationResult } = - apiKeysSlice.actions; - -export const apiKeysReducer = apiKeysSlice.reducer; - -export default { - apiKeys: apiKeysReducer, -}; diff --git a/public/app/features/api-keys/state/selectors.test.ts b/public/app/features/api-keys/state/selectors.test.ts deleted file mode 100644 index e1ba2f4d0f9..00000000000 --- a/public/app/features/api-keys/state/selectors.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { ApiKeysState } from 'app/types'; - -import { getMultipleMockKeys } from '../__mocks__/apiKeysMock'; - -import { getApiKeys, getApiKeysCount, getIncludeExpired, getIncludeExpiredDisabled } from './selectors'; - -describe('API Keys selectors', () => { - const mockKeys = getMultipleMockKeys(5); - const mockKeysIncludingExpired = getMultipleMockKeys(8); - - describe('getApiKeysCount', () => { - it('returns the correct count when includeExpired is false', () => { - const mockState: ApiKeysState = { - keys: mockKeys, - keysIncludingExpired: mockKeysIncludingExpired, - searchQuery: '', - hasFetched: true, - includeExpired: false, - }; - const keyCount = getApiKeysCount(mockState); - expect(keyCount).toBe(5); - }); - - it('returns the correct count when includeExpired is true', () => { - const mockState: ApiKeysState = { - keys: mockKeys, - keysIncludingExpired: mockKeysIncludingExpired, - searchQuery: '', - hasFetched: true, - includeExpired: true, - }; - const keyCount = getApiKeysCount(mockState); - expect(keyCount).toBe(8); - }); - }); - - describe('getApiKeys', () => { - describe('when includeExpired is false', () => { - it('should return all keys if no search query', () => { - const mockState: ApiKeysState = { - keys: mockKeys, - keysIncludingExpired: mockKeysIncludingExpired, - searchQuery: '', - hasFetched: true, - includeExpired: false, - }; - const keys = getApiKeys(mockState); - expect(keys).toEqual(mockKeys); - }); - - it('should filter keys if search query exists', () => { - const mockState: ApiKeysState = { - keys: mockKeys, - keysIncludingExpired: mockKeysIncludingExpired, - searchQuery: '5', - hasFetched: true, - includeExpired: false, - }; - const keys = getApiKeys(mockState); - expect(keys.length).toEqual(1); - }); - }); - - describe('when includeExpired is true', () => { - it('should return all keys if no search query', () => { - const mockState: ApiKeysState = { - keys: mockKeys, - keysIncludingExpired: mockKeysIncludingExpired, - searchQuery: '', - hasFetched: true, - includeExpired: true, - }; - const keys = getApiKeys(mockState); - expect(keys).toEqual(mockKeysIncludingExpired); - }); - - it('should filter keys if search query exists', () => { - const mockState: ApiKeysState = { - keys: mockKeys, - keysIncludingExpired: mockKeysIncludingExpired, - searchQuery: '5', - hasFetched: true, - includeExpired: true, - }; - const keys = getApiKeys(mockState); - expect(keys.length).toEqual(1); - }); - }); - }); - - describe('getIncludeExpired', () => { - it('returns true if includeExpired is true', () => { - const mockState: ApiKeysState = { - keys: mockKeys, - keysIncludingExpired: mockKeysIncludingExpired, - searchQuery: '', - hasFetched: true, - includeExpired: true, - }; - const includeExpired = getIncludeExpired(mockState); - expect(includeExpired).toBe(true); - }); - - it('returns false if includeExpired is false', () => { - const mockState: ApiKeysState = { - keys: mockKeys, - keysIncludingExpired: mockKeysIncludingExpired, - searchQuery: '', - hasFetched: true, - includeExpired: false, - }; - const includeExpired = getIncludeExpired(mockState); - expect(includeExpired).toBe(false); - }); - }); - - describe('getIncludeExpiredDisabled', () => { - it('returns true if there are no active keys but there are expired keys', () => { - const mockState: ApiKeysState = { - keys: [], - keysIncludingExpired: mockKeysIncludingExpired, - searchQuery: '', - hasFetched: true, - includeExpired: true, - }; - const includeExpiredDisabled = getIncludeExpiredDisabled(mockState); - expect(includeExpiredDisabled).toBe(true); - }); - - it('returns false otherwise', () => { - const mockState: ApiKeysState = { - keys: mockKeys, - keysIncludingExpired: mockKeysIncludingExpired, - searchQuery: '', - hasFetched: true, - includeExpired: false, - }; - const includeExpiredDisabled = getIncludeExpired(mockState); - expect(includeExpiredDisabled).toBe(false); - }); - }); -}); diff --git a/public/app/features/api-keys/state/selectors.ts b/public/app/features/api-keys/state/selectors.ts deleted file mode 100644 index df05e856253..00000000000 --- a/public/app/features/api-keys/state/selectors.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ApiKeysState } from 'app/types'; - -export const getApiKeysCount = (state: ApiKeysState) => - state.includeExpired ? state.keysIncludingExpired.length : state.keys.length; - -export const getApiKeys = (state: ApiKeysState) => { - const regex = RegExp(state.searchQuery, 'i'); - const keysToFilter = state.includeExpired ? state.keysIncludingExpired : state.keys; - - return keysToFilter.filter((key) => { - return regex.test(key.name) || regex.test(key.role); - }); -}; - -export const getIncludeExpired = (state: ApiKeysState) => state.includeExpired; - -export const getIncludeExpiredDisabled = (state: ApiKeysState) => - state.keys.length === 0 && state.keysIncludingExpired.length > 0; diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 90b70ca5a9c..252b8703bd4 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -248,13 +248,6 @@ export function getAppRoutes(): RouteDescriptor[] { () => import(/* webpackChunkName: "UserInvitePage" */ 'app/features/org/UserInvitePage') ), }, - { - path: '/org/apikeys', - roles: () => contextSrv.evaluatePermission([AccessControlAction.ActionAPIKeysRead]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "ApiKeysPage" */ 'app/features/api-keys/ApiKeysPage') - ), - }, { path: '/org/serviceaccounts', roles: () => diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 5e7ab6e9e0c..1ad2e74501d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2964,51 +2964,6 @@ "title-query-result": "Query result" } }, - "api-keys": { - "api-keys-action-bar": { - "placeholder-search-keys": "Search keys" - }, - "api-keys-page-unconnected": { - "label-include-expired-keys": "Include expired keys" - }, - "api-keys-table": { - "aria-label-delete-api-key": "Delete API key", - "content-this-api-key-has-expired": "This API key has expired.", - "expires": "Expires", - "last-used-at": "Last used at", - "migrate-to-service-account": "Migrate to service account", - "name": "Name", - "role": "Role" - }, - "empty-state": { - "message": "No API keys found" - }, - "migrate-to-service-accounts-card": { - "body-no-api-keys-found": "No API keys were found. If you reload the browser, this page will not be available anymore.", - "body-switch-service-accounts": "API keys are deprecated and will be removed from Grafana on Jan 31, 2025. Each API key will be migrated into a service account with a token and will continue to work as they were. We encourage you to migrate your API keys to service accounts now. ", - "docs-link": { - "about-migration": "Find out more about the migration here." - }, - "migrate-all-service-accounts": "Migrate all service accounts", - "migration-box-desc": { - "migrating": "Migrating all API keys will hide the API keys tab." - }, - "modal-title": "Migrate API keys to service accounts", - "title-no-api-keys-found": "No API keys found", - "title-switch-service-accounts": "Switch from API keys to service accounts" - }, - "migration-summary": { - "close": "Close", - "failed": "<0>Failed: {{failed}}", - "failed-details": "<0>Failed details: {{details}}", - "failed-ids": "<0>Failed api key IDs: {{ids}}", - "migrated": "<0>Migrated: {{migrated}}", - "migration-complete": "Migration complete! Please note, while there might be a few API keys flagged as `failed migrations`, rest assured, all of your API keys are fully functional and operational. Please try again or contact support.", - "migration-successful": "Migration Successful!", - "title-migration-summary": "Migration summary", - "total": "<0>Total: {{total}}" - } - }, "app-chrome": { "skip-content-button": "Skip to main content", "top-bar": { @@ -8162,10 +8117,6 @@ "subtitle": "See recently deleted alert rules", "title": "Recently deleted" }, - "api-keys": { - "subtitle": "Manage and create API keys that are used to interact with Grafana HTTP APIs", - "title": "API keys" - }, "application": { "title": "Application" },