mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
API: Optionally list expired keys (#20468)
* API: Optionally list expired keys * Update docs
This commit is contained in:
parent
1b38d94537
commit
d1c523838b
@ -67,6 +67,10 @@ Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
Query Parameters:
|
||||
|
||||
- `includeExpired`: boolean. enable listing of expired keys. Optional.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
|
@ -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)
|
||||
|
@ -50,7 +50,7 @@ type DeleteApiKeyCommand struct {
|
||||
|
||||
type GetApiKeysQuery struct {
|
||||
OrgId int64
|
||||
IncludeInvalid bool
|
||||
IncludeExpired bool
|
||||
Result []*ApiKey
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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<Props, any> {
|
||||
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<Props, any> {
|
||||
}
|
||||
|
||||
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<Props, any> {
|
||||
// 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<Props, any> {
|
||||
|
||||
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<Props, any> {
|
||||
{this.renderAddApiKeyForm()}
|
||||
|
||||
<h3 className="page-heading">Existing Keys</h3>
|
||||
<Switch
|
||||
label="Show expired"
|
||||
checked={includeExpired}
|
||||
onChange={event => {
|
||||
// @ts-ignore
|
||||
this.onIncludeExpiredChange(event.target.checked);
|
||||
}}
|
||||
/>
|
||||
<table className="filter-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -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,
|
||||
};
|
||||
|
@ -26,27 +26,31 @@ const apiKeysLoaded = (apiKeys: ApiKey[]): LoadApiKeysAction => ({
|
||||
payload: apiKeys,
|
||||
});
|
||||
|
||||
export function addApiKey(apiKey: ApiKey, openModal: (key: string) => void): ThunkResult<void> {
|
||||
export function addApiKey(
|
||||
apiKey: ApiKey,
|
||||
openModal: (key: string) => void,
|
||||
includeExpired: boolean
|
||||
): ThunkResult<void> {
|
||||
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<void> {
|
||||
export function loadApiKeys(includeExpired: boolean): ThunkResult<void> {
|
||||
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<void> {
|
||||
export function deleteApiKey(id: number, includeExpired: boolean): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
getBackendSrv()
|
||||
.delete('/api/auth/keys/' + id)
|
||||
.then(dispatch(loadApiKeys()));
|
||||
.then(dispatch(loadApiKeys(includeExpired)));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ export const initialApiKeysState: ApiKeysState = {
|
||||
keys: [],
|
||||
searchQuery: '',
|
||||
hasFetched: false,
|
||||
includeExpired: false,
|
||||
};
|
||||
|
||||
export const apiKeysReducer = (state = initialApiKeysState, action: Action): ApiKeysState => {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -18,4 +18,5 @@ export interface ApiKeysState {
|
||||
keys: ApiKey[];
|
||||
searchQuery: string;
|
||||
hasFetched: boolean;
|
||||
includeExpired: boolean;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user