diff --git a/docs/sources/http_api/auth.md b/docs/sources/http_api/auth.md index fd5007f8af7..ce3fd6eb507 100644 --- a/docs/sources/http_api/auth.md +++ b/docs/sources/http_api/auth.md @@ -67,6 +67,10 @@ Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ``` +Query Parameters: + +- `includeExpired`: boolean. enable listing of expired keys. Optional. + **Example Response**: ```http diff --git a/pkg/api/apikey.go b/pkg/api/apikey.go index b1c7ac76427..98f9546dcd4 100644 --- a/pkg/api/apikey.go +++ b/pkg/api/apikey.go @@ -10,7 +10,7 @@ import ( ) func GetAPIKeys(c *models.ReqContext) Response { - query := models.GetApiKeysQuery{OrgId: c.OrgId} + query := models.GetApiKeysQuery{OrgId: c.OrgId, IncludeExpired: c.QueryBool("includeExpired")} if err := bus.Dispatch(&query); err != nil { return Error(500, "Failed to list api keys", err) diff --git a/pkg/models/apikey.go b/pkg/models/apikey.go index fe96bbd14df..8b18a0185b1 100644 --- a/pkg/models/apikey.go +++ b/pkg/models/apikey.go @@ -50,7 +50,7 @@ type DeleteApiKeyCommand struct { type GetApiKeysQuery struct { OrgId int64 - IncludeInvalid bool + IncludeExpired bool Result []*ApiKey } diff --git a/pkg/services/sqlstore/apikey.go b/pkg/services/sqlstore/apikey.go index 65b06ca186d..d4c0cc30fab 100644 --- a/pkg/services/sqlstore/apikey.go +++ b/pkg/services/sqlstore/apikey.go @@ -19,7 +19,7 @@ func init() { func GetApiKeys(query *models.GetApiKeysQuery) error { sess := x.Limit(100, 0).Where("org_id=? and ( expires IS NULL or expires >= ?)", query.OrgId, timeNow().Unix()).Asc("name") - if query.IncludeInvalid { + if query.IncludeExpired { sess = x.Limit(100, 0).Where("org_id=?", query.OrgId).Asc("name") } diff --git a/pkg/services/sqlstore/apikey_test.go b/pkg/services/sqlstore/apikey_test.go index 272fcea8aec..1d04c3cd98a 100644 --- a/pkg/services/sqlstore/apikey_test.go +++ b/pkg/services/sqlstore/apikey_test.go @@ -91,7 +91,7 @@ func TestApiKeyDataAccess(t *testing.T) { // advance mocked getTime by 1s timeNow() - query := models.GetApiKeysQuery{OrgId: 1, IncludeInvalid: false} + query := models.GetApiKeysQuery{OrgId: 1, IncludeExpired: false} err = GetApiKeys(&query) assert.Nil(t, err) @@ -101,7 +101,7 @@ func TestApiKeyDataAccess(t *testing.T) { } } - query = models.GetApiKeysQuery{OrgId: 1, IncludeInvalid: true} + query = models.GetApiKeysQuery{OrgId: 1, IncludeExpired: true} err = GetApiKeys(&query) assert.Nil(t, err) diff --git a/public/app/features/api-keys/ApiKeysPage.test.tsx b/public/app/features/api-keys/ApiKeysPage.test.tsx index d1d12381a4d..7ae70833075 100644 --- a/public/app/features/api-keys/ApiKeysPage.test.tsx +++ b/public/app/features/api-keys/ApiKeysPage.test.tsx @@ -23,6 +23,7 @@ const setup = (propOverrides?: object) => { setSearchQuery: jest.fn(), addApiKey: jest.fn(), apiKeysCount: 0, + includeExpired: false, }; Object.assign(props, propOverrides); @@ -63,7 +64,7 @@ describe('Life cycle', () => { instance.componentDidMount(); - expect(instance.props.loadApiKeys).toHaveBeenCalled(); + expect(instance.props.loadApiKeys).toHaveBeenCalledWith(false); }); }); @@ -72,7 +73,7 @@ describe('Functions', () => { it('should call delete team', () => { const { instance } = setup(); instance.onDeleteApiKey(getMockKey()); - expect(instance.props.deleteApiKey).toHaveBeenCalledWith(1); + expect(instance.props.deleteApiKey).toHaveBeenCalledWith(1, false); }); }); diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index 0f68995f0f5..b374b459d2e 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -12,7 +12,7 @@ import ApiKeysAddedModal from './ApiKeysAddedModal'; import config from 'app/core/config'; import appEvents from 'app/core/app_events'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; -import { DeleteButton, EventsWithValidation, FormLabel, Input, ValidationEvents } from '@grafana/ui'; +import { DeleteButton, EventsWithValidation, FormLabel, Input, Switch, ValidationEvents } from '@grafana/ui'; import { NavModel, dateTime, isDateTime } from '@grafana/data'; import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; import { store } from 'app/store/store'; @@ -51,6 +51,7 @@ export interface Props { setSearchQuery: typeof setSearchQuery; addApiKey: typeof addApiKey; apiKeysCount: number; + includeExpired: boolean; } export interface State { @@ -76,7 +77,7 @@ const tooltipText = export class ApiKeysPage extends PureComponent { constructor(props: Props) { super(props); - this.state = { isAdding: false, newApiKey: initialApiKeyState }; + this.state = { isAdding: false, newApiKey: initialApiKeyState, includeExpired: false }; } componentDidMount() { @@ -84,17 +85,21 @@ export class ApiKeysPage extends PureComponent { } async fetchApiKeys() { - await this.props.loadApiKeys(); + await this.props.loadApiKeys(this.state.includeExpired); } onDeleteApiKey(key: ApiKey) { - this.props.deleteApiKey(key.id); + this.props.deleteApiKey(key.id, this.props.includeExpired); } onSearchQueryChange = (value: string) => { this.props.setSearchQuery(value); }; + onIncludeExpiredChange = (value: boolean) => { + this.setState({ hasFetched: false, includeExpired: value }, this.fetchApiKeys); + }; + onToggleAdding = () => { this.setState({ isAdding: !this.state.isAdding }); }; @@ -114,7 +119,7 @@ export class ApiKeysPage extends PureComponent { // make sure that secondsToLive is number or null const secondsToLive = this.state.newApiKey['secondsToLive']; this.state.newApiKey['secondsToLive'] = secondsToLive ? kbn.interval_to_seconds(secondsToLive) : null; - this.props.addApiKey(this.state.newApiKey, openModal); + this.props.addApiKey(this.state.newApiKey, openModal, this.props.includeExpired); this.setState((prevState: State) => { return { ...prevState, @@ -232,7 +237,7 @@ export class ApiKeysPage extends PureComponent { renderApiKeyList() { const { isAdding } = this.state; - const { apiKeys, searchQuery } = this.props; + const { apiKeys, searchQuery, includeExpired } = this.props; return ( <> @@ -256,6 +261,14 @@ export class ApiKeysPage extends PureComponent { {this.renderAddApiKeyForm()}

Existing Keys

+ { + // @ts-ignore + this.onIncludeExpiredChange(event.target.checked); + }} + /> @@ -304,6 +317,7 @@ function mapStateToProps(state: any) { navModel: getNavModel(state.navIndex, 'apikeys'), apiKeys: getApiKeys(state.apiKeys), searchQuery: state.apiKeys.searchQuery, + includeExpired: state.includeExpired, apiKeysCount: getApiKeysCount(state.apiKeys), hasFetched: state.apiKeys.hasFetched, }; diff --git a/public/app/features/api-keys/state/actions.ts b/public/app/features/api-keys/state/actions.ts index 63e91088476..b46d3dd9ff4 100644 --- a/public/app/features/api-keys/state/actions.ts +++ b/public/app/features/api-keys/state/actions.ts @@ -26,27 +26,31 @@ const apiKeysLoaded = (apiKeys: ApiKey[]): LoadApiKeysAction => ({ payload: apiKeys, }); -export function addApiKey(apiKey: ApiKey, openModal: (key: string) => void): ThunkResult { +export function addApiKey( + apiKey: ApiKey, + openModal: (key: string) => void, + includeExpired: boolean +): ThunkResult { return async dispatch => { const result = await getBackendSrv().post('/api/auth/keys', apiKey); dispatch(setSearchQuery('')); - dispatch(loadApiKeys()); + dispatch(loadApiKeys(includeExpired)); openModal(result.key); }; } -export function loadApiKeys(): ThunkResult { +export function loadApiKeys(includeExpired: boolean): ThunkResult { return async dispatch => { - const response = await getBackendSrv().get('/api/auth/keys'); + const response = await getBackendSrv().get('/api/auth/keys?includeExpired=' + includeExpired); dispatch(apiKeysLoaded(response)); }; } -export function deleteApiKey(id: number): ThunkResult { +export function deleteApiKey(id: number, includeExpired: boolean): ThunkResult { return async dispatch => { getBackendSrv() .delete('/api/auth/keys/' + id) - .then(dispatch(loadApiKeys())); + .then(dispatch(loadApiKeys(includeExpired))); }; } diff --git a/public/app/features/api-keys/state/reducers.ts b/public/app/features/api-keys/state/reducers.ts index 57849b20d4f..1bdf5fe8a13 100644 --- a/public/app/features/api-keys/state/reducers.ts +++ b/public/app/features/api-keys/state/reducers.ts @@ -5,6 +5,7 @@ export const initialApiKeysState: ApiKeysState = { keys: [], searchQuery: '', hasFetched: false, + includeExpired: false, }; export const apiKeysReducer = (state = initialApiKeysState, action: Action): ApiKeysState => { diff --git a/public/app/features/api-keys/state/selectors.test.ts b/public/app/features/api-keys/state/selectors.test.ts index 5e9ba51462f..308c1044b6c 100644 --- a/public/app/features/api-keys/state/selectors.test.ts +++ b/public/app/features/api-keys/state/selectors.test.ts @@ -7,7 +7,7 @@ describe('API Keys selectors', () => { const mockKeys = getMultipleMockKeys(5); it('should return all keys if no search query', () => { - const mockState: ApiKeysState = { keys: mockKeys, searchQuery: '', hasFetched: false }; + const mockState: ApiKeysState = { keys: mockKeys, searchQuery: '', hasFetched: false, includeExpired: false }; const keys = getApiKeys(mockState); @@ -15,7 +15,7 @@ describe('API Keys selectors', () => { }); it('should filter keys if search query exists', () => { - const mockState: ApiKeysState = { keys: mockKeys, searchQuery: '5', hasFetched: false }; + const mockState: ApiKeysState = { keys: mockKeys, searchQuery: '5', hasFetched: false, includeExpired: false }; const keys = getApiKeys(mockState); diff --git a/public/app/types/apiKeys.ts b/public/app/types/apiKeys.ts index 4df2ebd41e8..380aaabe7c2 100644 --- a/public/app/types/apiKeys.ts +++ b/public/app/types/apiKeys.ts @@ -18,4 +18,5 @@ export interface ApiKeysState { keys: ApiKey[]; searchQuery: string; hasFetched: boolean; + includeExpired: boolean; }