Commit c0996e7a by Torkel Ödegaard Committed by GitHub

Merge pull request #13444 from grafana/13411-react-api-key

13411 react api key
parents bf2abc69 3081e0f8
import React, { Component } from 'react'; import React, { Component } from 'react';
import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; import { UserPicker } from 'app/core/components/Picker/UserPicker';
import { TeamPicker, Team } from 'app/core/components/Picker/TeamPicker'; import { TeamPicker, Team } from 'app/core/components/Picker/TeamPicker';
import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker'; import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker';
import { User } from 'app/types';
import { import {
dashboardPermissionLevels, dashboardPermissionLevels,
dashboardAclTargets, dashboardAclTargets,
......
...@@ -3,6 +3,7 @@ import Select from 'react-select'; ...@@ -3,6 +3,7 @@ import Select from 'react-select';
import PickerOption from './PickerOption'; import PickerOption from './PickerOption';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { getBackendSrv } from 'app/core/services/backend_srv'; import { getBackendSrv } from 'app/core/services/backend_srv';
import { User } from 'app/types';
export interface Props { export interface Props {
onSelected: (user: User) => void; onSelected: (user: User) => void;
...@@ -14,13 +15,6 @@ export interface State { ...@@ -14,13 +15,6 @@ export interface State {
isLoading: boolean; isLoading: boolean;
} }
export interface User {
id: number;
label: string;
avatarUrl: string;
login: string;
}
export class UserPicker extends Component<Props, State> { export class UserPicker extends Component<Props, State> {
debouncedSearch: any; debouncedSearch: any;
......
import React from 'react';
import { shallow } from 'enzyme';
import { ApiKeysAddedModal, Props } from './ApiKeysAddedModal';
const setup = (propOverrides?: object) => {
const props: Props = {
apiKey: 'api key test',
rootPath: 'test/path',
};
Object.assign(props, propOverrides);
const wrapper = shallow(<ApiKeysAddedModal {...props} />);
return {
wrapper,
};
};
describe('Render', () => {
it('should render component', () => {
const { wrapper } = setup();
expect(wrapper).toMatchSnapshot();
});
});
import React from 'react';
export interface Props {
apiKey: string;
rootPath: string;
}
export const ApiKeysAddedModal = (props: Props) => {
return (
<div className="modal-body">
<div className="modal-header">
<h2 className="modal-header-title">
<i className="fa fa-key" />
<span className="p-l-1">API Key Created</span>
</h2>
<a className="modal-header-close" ng-click="dismiss();">
<i className="fa fa-remove" />
</a>
</div>
<div className="modal-content">
<div className="gf-form-group">
<div className="gf-form">
<span className="gf-form-label">Key</span>
<span className="gf-form-label">{props.apiKey}</span>
</div>
</div>
<div className="grafana-info-box" style={{ border: 0 }}>
You will only be able to view this key here once! It is not stored in this form. So be sure to copy it now.
<br />
<br />
You can authenticate request using the Authorization HTTP header, example:
<br />
<br />
<pre className="small">
curl -H "Authorization: Bearer {props.apiKey}" {props.rootPath}/api/dashboards/home
</pre>
</div>
</div>
</div>
);
};
export default ApiKeysAddedModal;
import React from 'react';
import { shallow } from 'enzyme';
import { Props, ApiKeysPage } from './ApiKeysPage';
import { NavModel, ApiKey } from 'app/types';
import { getMultipleMockKeys, getMockKey } from './__mocks__/apiKeysMock';
const setup = (propOverrides?: object) => {
const props: Props = {
navModel: {} as NavModel,
apiKeys: [] as ApiKey[],
searchQuery: '',
loadApiKeys: jest.fn(),
deleteApiKey: jest.fn(),
setSearchQuery: jest.fn(),
addApiKey: jest.fn(),
};
Object.assign(props, propOverrides);
const wrapper = shallow(<ApiKeysPage {...props} />);
const instance = wrapper.instance() as ApiKeysPage;
return {
wrapper,
instance,
};
};
describe('Render', () => {
it('should render component', () => {
const { wrapper } = setup();
expect(wrapper).toMatchSnapshot();
});
it('should render API keys table', () => {
const { wrapper } = setup({
apiKeys: getMultipleMockKeys(5),
});
expect(wrapper).toMatchSnapshot();
});
});
describe('Life cycle', () => {
it('should call loadApiKeys', () => {
const { instance } = setup();
instance.componentDidMount();
expect(instance.props.loadApiKeys).toHaveBeenCalled();
});
});
describe('Functions', () => {
describe('Delete team', () => {
it('should call delete team', () => {
const { instance } = setup();
instance.onDeleteApiKey(getMockKey());
expect(instance.props.deleteApiKey).toHaveBeenCalledWith(1);
});
});
describe('on search query change', () => {
it('should call setSearchQuery', () => {
const { instance } = setup();
const mockEvent = { target: { value: 'test' } };
instance.onSearchQueryChange(mockEvent);
expect(instance.props.setSearchQuery).toHaveBeenCalledWith('test');
});
});
});
import React, { PureComponent } from 'react';
import ReactDOMServer from 'react-dom/server';
import { connect } from 'react-redux';
import { hot } from 'react-hot-loader';
import { NavModel, ApiKey, NewApiKey, OrgRole } from 'app/types';
import { getNavModel } from 'app/core/selectors/navModel';
import { getApiKeys } from './state/selectors';
import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions';
import PageHeader from 'app/core/components/PageHeader/PageHeader';
import SlideDown from 'app/core/components/Animations/SlideDown';
import ApiKeysAddedModal from './ApiKeysAddedModal';
import config from 'app/core/config';
import appEvents from 'app/core/app_events';
export interface Props {
navModel: NavModel;
apiKeys: ApiKey[];
searchQuery: string;
loadApiKeys: typeof loadApiKeys;
deleteApiKey: typeof deleteApiKey;
setSearchQuery: typeof setSearchQuery;
addApiKey: typeof addApiKey;
}
export interface State {
isAdding: boolean;
newApiKey: NewApiKey;
}
enum ApiKeyStateProps {
Name = 'name',
Role = 'role',
}
const initialApiKeyState = {
name: '',
role: OrgRole.Viewer,
};
export class ApiKeysPage extends PureComponent<Props, any> {
constructor(props) {
super(props);
this.state = { isAdding: false, newApiKey: initialApiKeyState };
}
componentDidMount() {
this.fetchApiKeys();
}
async fetchApiKeys() {
await this.props.loadApiKeys();
}
onDeleteApiKey(key: ApiKey) {
this.props.deleteApiKey(key.id);
}
onSearchQueryChange = evt => {
this.props.setSearchQuery(evt.target.value);
};
onToggleAdding = () => {
this.setState({ isAdding: !this.state.isAdding });
};
onAddApiKey = async evt => {
evt.preventDefault();
const openModal = (apiKey: string) => {
const rootPath = window.location.origin + config.appSubUrl;
const modalTemplate = ReactDOMServer.renderToString(<ApiKeysAddedModal apiKey={apiKey} rootPath={rootPath} />);
appEvents.emit('show-modal', {
templateHtml: modalTemplate,
});
};
this.props.addApiKey(this.state.newApiKey, openModal);
this.setState((prevState: State) => {
return {
...prevState,
newApiKey: initialApiKeyState,
};
});
};
onApiKeyStateUpdate = (evt, prop: string) => {
const value = evt.currentTarget.value;
this.setState((prevState: State) => {
const newApiKey = {
...prevState.newApiKey,
};
newApiKey[prop] = value;
return {
...prevState,
newApiKey: newApiKey,
};
});
};
render() {
const { newApiKey, isAdding } = this.state;
const { navModel, apiKeys, searchQuery } = this.props;
return (
<div>
<PageHeader model={navModel} />
<div className="page-container page-body">
<div className="page-action-bar">
<div className="gf-form gf-form--grow">
<label className="gf-form--has-input-icon gf-form--grow">
<input
type="text"
className="gf-form-input"
placeholder="Search keys"
value={searchQuery}
onChange={this.onSearchQueryChange}
/>
<i className="gf-form-input-icon fa fa-search" />
</label>
</div>
<div className="page-action-bar__spacer" />
<button className="btn btn-success pull-right" onClick={this.onToggleAdding} disabled={isAdding}>
<i className="fa fa-plus" /> Add API Key
</button>
</div>
<SlideDown in={isAdding}>
<div className="cta-form">
<button className="cta-form__close btn btn-transparent" onClick={this.onToggleAdding}>
<i className="fa fa-close" />
</button>
<h5>Add API Key</h5>
<form className="gf-form-group" onSubmit={this.onAddApiKey}>
<div className="gf-form-inline">
<div className="gf-form max-width-21">
<span className="gf-form-label">Key name</span>
<input
type="text"
className="gf-form-input"
value={newApiKey.name}
placeholder="Name"
onChange={evt => this.onApiKeyStateUpdate(evt, ApiKeyStateProps.Name)}
/>
</div>
<div className="gf-form">
<span className="gf-form-label">Role</span>
<span className="gf-form-select-wrapper">
<select
className="gf-form-input gf-size-auto"
value={newApiKey.role}
onChange={evt => this.onApiKeyStateUpdate(evt, ApiKeyStateProps.Role)}
>
{Object.keys(OrgRole).map(role => {
return (
<option key={role} label={role} value={role}>
{role}
</option>
);
})}
</select>
</span>
</div>
<div className="gf-form">
<button className="btn gf-form-btn btn-success">Add</button>
</div>
</div>
</form>
</div>
</SlideDown>
<h3 className="page-heading">Existing Keys</h3>
<table className="filter-table">
<thead>
<tr>
<th>Name</th>
<th>Role</th>
<th style={{ width: '34px' }} />
</tr>
</thead>
{apiKeys.length > 0 ? (
<tbody>
{apiKeys.map(key => {
return (
<tr key={key.id}>
<td>{key.name}</td>
<td>{key.role}</td>
<td>
<a onClick={() => this.onDeleteApiKey(key)} className="btn btn-danger btn-mini">
<i className="fa fa-remove" />
</a>
</td>
</tr>
);
})}
</tbody>
) : null}
</table>
</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
navModel: getNavModel(state.navIndex, 'apikeys'),
apiKeys: getApiKeys(state.apiKeys),
searchQuery: state.apiKeys.searchQuery,
};
}
const mapDispatchToProps = {
loadApiKeys,
deleteApiKey,
setSearchQuery,
addApiKey,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(ApiKeysPage));
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,
});
}
return keys;
};
export const getMockKey = (): ApiKey => {
return {
id: 1,
name: 'test',
role: OrgRole.Admin,
};
};
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div
className="modal-body"
>
<div
className="modal-header"
>
<h2
className="modal-header-title"
>
<i
className="fa fa-key"
/>
<span
className="p-l-1"
>
API Key Created
</span>
</h2>
<a
className="modal-header-close"
ng-click="dismiss();"
>
<i
className="fa fa-remove"
/>
</a>
</div>
<div
className="modal-content"
>
<div
className="gf-form-group"
>
<div
className="gf-form"
>
<span
className="gf-form-label"
>
Key
</span>
<span
className="gf-form-label"
>
api key test
</span>
</div>
</div>
<div
className="grafana-info-box"
style={
Object {
"border": 0,
}
}
>
You will only be able to view this key here once! It is not stored in this form. So be sure to copy it now.
<br />
<br />
You can authenticate request using the Authorization HTTP header, example:
<br />
<br />
<pre
className="small"
>
curl -H "Authorization: Bearer
api key test
"
test/path
/api/dashboards/home
</pre>
</div>
</div>
</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render API keys table 1`] = `
<div>
<PageHeader
model={Object {}}
/>
<div
className="page-container page-body"
>
<div
className="page-action-bar"
>
<div
className="gf-form gf-form--grow"
>
<label
className="gf-form--has-input-icon gf-form--grow"
>
<input
className="gf-form-input"
onChange={[Function]}
placeholder="Search keys"
type="text"
value=""
/>
<i
className="gf-form-input-icon fa fa-search"
/>
</label>
</div>
<div
className="page-action-bar__spacer"
/>
<button
className="btn btn-success pull-right"
disabled={false}
onClick={[Function]}
>
<i
className="fa fa-plus"
/>
Add API Key
</button>
</div>
<Component
in={false}
>
<div
className="cta-form"
>
<button
className="cta-form__close btn btn-transparent"
onClick={[Function]}
>
<i
className="fa fa-close"
/>
</button>
<h5>
Add API Key
</h5>
<form
className="gf-form-group"
onSubmit={[Function]}
>
<div
className="gf-form-inline"
>
<div
className="gf-form max-width-21"
>
<span
className="gf-form-label"
>
Key name
</span>
<input
className="gf-form-input"
onChange={[Function]}
placeholder="Name"
type="text"
value=""
/>
</div>
<div
className="gf-form"
>
<span
className="gf-form-label"
>
Role
</span>
<span
className="gf-form-select-wrapper"
>
<select
className="gf-form-input gf-size-auto"
onChange={[Function]}
value="Viewer"
>
<option
key="Viewer"
label="Viewer"
value="Viewer"
>
Viewer
</option>
<option
key="Editor"
label="Editor"
value="Editor"
>
Editor
</option>
<option
key="Admin"
label="Admin"
value="Admin"
>
Admin
</option>
</select>
</span>
</div>
<div
className="gf-form"
>
<button
className="btn gf-form-btn btn-success"
>
Add
</button>
</div>
</div>
</form>
</div>
</Component>
<h3
className="page-heading"
>
Existing Keys
</h3>
<table
className="filter-table"
>
<thead>
<tr>
<th>
Name
</th>
<th>
Role
</th>
<th
style={
Object {
"width": "34px",
}
}
/>
</tr>
</thead>
<tbody>
<tr
key="1"
>
<td>
test-1
</td>
<td>
Viewer
</td>
<td>
<a
className="btn btn-danger btn-mini"
onClick={[Function]}
>
<i
className="fa fa-remove"
/>
</a>
</td>
</tr>
<tr
key="2"
>
<td>
test-2
</td>
<td>
Viewer
</td>
<td>
<a
className="btn btn-danger btn-mini"
onClick={[Function]}
>
<i
className="fa fa-remove"
/>
</a>
</td>
</tr>
<tr
key="3"
>
<td>
test-3
</td>
<td>
Viewer
</td>
<td>
<a
className="btn btn-danger btn-mini"
onClick={[Function]}
>
<i
className="fa fa-remove"
/>
</a>
</td>
</tr>
<tr
key="4"
>
<td>
test-4
</td>
<td>
Viewer
</td>
<td>
<a
className="btn btn-danger btn-mini"
onClick={[Function]}
>
<i
className="fa fa-remove"
/>
</a>
</td>
</tr>
<tr
key="5"
>
<td>
test-5
</td>
<td>
Viewer
</td>
<td>
<a
className="btn btn-danger btn-mini"
onClick={[Function]}
>
<i
className="fa fa-remove"
/>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
`;
exports[`Render should render component 1`] = `
<div>
<PageHeader
model={Object {}}
/>
<div
className="page-container page-body"
>
<div
className="page-action-bar"
>
<div
className="gf-form gf-form--grow"
>
<label
className="gf-form--has-input-icon gf-form--grow"
>
<input
className="gf-form-input"
onChange={[Function]}
placeholder="Search keys"
type="text"
value=""
/>
<i
className="gf-form-input-icon fa fa-search"
/>
</label>
</div>
<div
className="page-action-bar__spacer"
/>
<button
className="btn btn-success pull-right"
disabled={false}
onClick={[Function]}
>
<i
className="fa fa-plus"
/>
Add API Key
</button>
</div>
<Component
in={false}
>
<div
className="cta-form"
>
<button
className="cta-form__close btn btn-transparent"
onClick={[Function]}
>
<i
className="fa fa-close"
/>
</button>
<h5>
Add API Key
</h5>
<form
className="gf-form-group"
onSubmit={[Function]}
>
<div
className="gf-form-inline"
>
<div
className="gf-form max-width-21"
>
<span
className="gf-form-label"
>
Key name
</span>
<input
className="gf-form-input"
onChange={[Function]}
placeholder="Name"
type="text"
value=""
/>
</div>
<div
className="gf-form"
>
<span
className="gf-form-label"
>
Role
</span>
<span
className="gf-form-select-wrapper"
>
<select
className="gf-form-input gf-size-auto"
onChange={[Function]}
value="Viewer"
>
<option
key="Viewer"
label="Viewer"
value="Viewer"
>
Viewer
</option>
<option
key="Editor"
label="Editor"
value="Editor"
>
Editor
</option>
<option
key="Admin"
label="Admin"
value="Admin"
>
Admin
</option>
</select>
</span>
</div>
<div
className="gf-form"
>
<button
className="btn gf-form-btn btn-success"
>
Add
</button>
</div>
</div>
</form>
</div>
</Component>
<h3
className="page-heading"
>
Existing Keys
</h3>
<table
className="filter-table"
>
<thead>
<tr>
<th>
Name
</th>
<th>
Role
</th>
<th
style={
Object {
"width": "34px",
}
}
/>
</tr>
</thead>
</table>
</div>
</div>
`;
import { ThunkAction } from 'redux-thunk';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { StoreState, ApiKey } from 'app/types';
export enum ActionTypes {
LoadApiKeys = 'LOAD_API_KEYS',
SetApiKeysSearchQuery = 'SET_API_KEYS_SEARCH_QUERY',
}
export interface LoadApiKeysAction {
type: ActionTypes.LoadApiKeys;
payload: ApiKey[];
}
export interface SetSearchQueryAction {
type: ActionTypes.SetApiKeysSearchQuery;
payload: string;
}
export type Action = LoadApiKeysAction | SetSearchQueryAction;
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
const apiKeysLoaded = (apiKeys: ApiKey[]): LoadApiKeysAction => ({
type: ActionTypes.LoadApiKeys,
payload: apiKeys,
});
export function addApiKey(apiKey: ApiKey, openModal: (key: string) => void): ThunkResult<void> {
return async dispatch => {
const result = await getBackendSrv().post('/api/auth/keys', apiKey);
dispatch(setSearchQuery(''));
dispatch(loadApiKeys());
openModal(result.key);
};
}
export function loadApiKeys(): ThunkResult<void> {
return async dispatch => {
const response = await getBackendSrv().get('/api/auth/keys');
dispatch(apiKeysLoaded(response));
};
}
export function deleteApiKey(id: number): ThunkResult<void> {
return async dispatch => {
getBackendSrv()
.delete('/api/auth/keys/' + id)
.then(dispatch(loadApiKeys()));
};
}
export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({
type: ActionTypes.SetApiKeysSearchQuery,
payload: searchQuery,
});
import { Action, ActionTypes } from './actions';
import { initialApiKeysState, apiKeysReducer } from './reducers';
import { getMultipleMockKeys } from '../__mocks__/apiKeysMock';
describe('API Keys reducer', () => {
it('should set keys', () => {
const payload = getMultipleMockKeys(4);
const action: Action = {
type: ActionTypes.LoadApiKeys,
payload,
};
const result = apiKeysReducer(initialApiKeysState, action);
expect(result.keys).toEqual(payload);
});
it('should set search query', () => {
const payload = 'test query';
const action: Action = {
type: ActionTypes.SetApiKeysSearchQuery,
payload,
};
const result = apiKeysReducer(initialApiKeysState, action);
expect(result.searchQuery).toEqual('test query');
});
});
import { ApiKeysState } from 'app/types';
import { Action, ActionTypes } from './actions';
export const initialApiKeysState: ApiKeysState = {
keys: [],
searchQuery: '',
};
export const apiKeysReducer = (state = initialApiKeysState, action: Action): ApiKeysState => {
switch (action.type) {
case ActionTypes.LoadApiKeys:
return { ...state, keys: action.payload };
case ActionTypes.SetApiKeysSearchQuery:
return { ...state, searchQuery: action.payload };
}
return state;
};
export default {
apiKeys: apiKeysReducer,
};
import { getApiKeys } from './selectors';
import { getMultipleMockKeys } from '../__mocks__/apiKeysMock';
import { ApiKeysState } from 'app/types';
describe('API Keys selectors', () => {
describe('Get API Keys', () => {
const mockKeys = getMultipleMockKeys(5);
it('should return all keys if no search query', () => {
const mockState: ApiKeysState = { keys: mockKeys, searchQuery: '' };
const keys = getApiKeys(mockState);
expect(keys).toEqual(mockKeys);
});
it('should filter keys if search query exists', () => {
const mockState: ApiKeysState = { keys: mockKeys, searchQuery: '5' };
const keys = getApiKeys(mockState);
expect(keys.length).toEqual(1);
});
});
});
import { ApiKeysState } from 'app/types';
export const getApiKeys = (state: ApiKeysState) => {
const regex = RegExp(state.searchQuery, 'i');
return state.keys.filter(key => {
return regex.test(key.name) || regex.test(key.role);
});
};
...@@ -6,6 +6,5 @@ import './change_password_ctrl'; ...@@ -6,6 +6,5 @@ import './change_password_ctrl';
import './new_org_ctrl'; import './new_org_ctrl';
import './user_invite_ctrl'; import './user_invite_ctrl';
import './create_team_ctrl'; import './create_team_ctrl';
import './org_api_keys_ctrl';
import './org_details_ctrl'; import './org_details_ctrl';
import './prefs_control'; import './prefs_control';
import angular from 'angular';
export class OrgApiKeysCtrl {
/** @ngInject */
constructor($scope, $http, backendSrv, navModelSrv) {
$scope.navModel = navModelSrv.getNav('cfg', 'apikeys', 0);
$scope.roleTypes = ['Viewer', 'Editor', 'Admin'];
$scope.token = { role: 'Viewer' };
$scope.init = () => {
$scope.getTokens();
};
$scope.getTokens = () => {
backendSrv.get('/api/auth/keys').then(tokens => {
$scope.tokens = tokens;
});
};
$scope.removeToken = id => {
backendSrv.delete('/api/auth/keys/' + id).then($scope.getTokens);
};
$scope.addToken = () => {
backendSrv.post('/api/auth/keys', $scope.token).then(result => {
const modalScope = $scope.$new(true);
modalScope.key = result.key;
modalScope.rootPath = window.location.origin + $scope.$root.appSubUrl;
$scope.appEvent('show-modal', {
src: 'public/app/features/org/partials/apikeyModal.html',
scope: modalScope,
});
$scope.getTokens();
});
};
$scope.init();
}
}
angular.module('grafana.controllers').controller('OrgApiKeysCtrl', OrgApiKeysCtrl);
<div class="modal-body">
<div class="modal-header">
<h2 class="modal-header-title">
<i class="fa fa-key"></i>
<span class="p-l-1">API Key Created</span>
</h2>
<a class="modal-header-close" ng-click="dismiss();">
<i class="fa fa-remove"></i>
</a>
</div>
<div class="modal-content">
<div class="gf-form-group">
<div class="gf-form">
<span class="gf-form-label">Key</span>
<span class="gf-form-label">{{key}}</span>
</div>
</div>
<div class="grafana-info-box" style="border: 0;">
You will only be able to view this key here once! It is not stored in this form. So be sure to copy it now.
<br>
<br>
You can authenticate request using the Authorization HTTP header, example:
<br>
<br>
<pre class="small">
curl -H "Authorization: Bearer {{key}}" {{rootPath}}/api/dashboards/home
</pre>
</div>
</div>
</div>
<page-header model="navModel"></page-header>
<div class="page-container page-body">
<h3 class="page-heading">Add new</h3>
<form name="addTokenForm" class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form max-width-21">
<span class="gf-form-label">Key name</span>
<input type="text" class="gf-form-input" ng-model='token.name' placeholder="Name"></input>
</div>
<div class="gf-form">
<span class="gf-form-label">Role</span>
<span class="gf-form-select-wrapper">
<select class="gf-form-input gf-size-auto" ng-model="token.role" ng-options="r for r in roleTypes"></select>
</span>
</div>
<div class="gf-form">
<button class="btn gf-form-btn btn-success" ng-click="addToken()">Add</button>
</div>
</div>
</form>
<h3 class="page-heading">Existing Keys</h3>
<table class="filter-table">
<thead>
<tr>
<th>Name</th>
<th>Role</th>
<th style="width: 34px;"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="t in tokens">
<td>{{t.name}}</td>
<td>{{t.role}}</td>
<td>
<a ng-click="removeToken(t.id)" class="btn btn-danger btn-mini">
<i class="fa fa-remove"></i>
</a>
</td>
</tr>
</tbody>
</table>
</div>
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import SlideDown from 'app/core/components/Animations/SlideDown'; import SlideDown from 'app/core/components/Animations/SlideDown';
import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; import { UserPicker } from 'app/core/components/Picker/UserPicker';
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
import { TeamMember } from '../../types'; import { TeamMember, User } from 'app/types';
import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions'; import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions';
import { getSearchMemberQuery, getTeamMembers } from './state/selectors'; import { getSearchMemberQuery, getTeamMembers } from './state/selectors';
......
import { Team, TeamGroup, TeamMember } from '../../../types'; import { Team, TeamGroup, TeamMember } from 'app/types';
export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
const teams: Team[] = []; const teams: Team[] = [];
......
...@@ -5,6 +5,7 @@ import ServerStats from 'app/features/admin/ServerStats'; ...@@ -5,6 +5,7 @@ import ServerStats from 'app/features/admin/ServerStats';
import AlertRuleList from 'app/features/alerting/AlertRuleList'; import AlertRuleList from 'app/features/alerting/AlertRuleList';
import TeamPages from 'app/features/teams/TeamPages'; import TeamPages from 'app/features/teams/TeamPages';
import TeamList from 'app/features/teams/TeamList'; import TeamList from 'app/features/teams/TeamList';
import ApiKeys from 'app/features/api-keys/ApiKeysPage';
import PluginListPage from 'app/features/plugins/PluginListPage'; import PluginListPage from 'app/features/plugins/PluginListPage';
import FolderSettingsPage from 'app/features/folders/FolderSettingsPage'; import FolderSettingsPage from 'app/features/folders/FolderSettingsPage';
import FolderPermissions from 'app/features/folders/FolderPermissions'; import FolderPermissions from 'app/features/folders/FolderPermissions';
...@@ -139,8 +140,11 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { ...@@ -139,8 +140,11 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
controllerAs: 'ctrl', controllerAs: 'ctrl',
}) })
.when('/org/apikeys', { .when('/org/apikeys', {
templateUrl: 'public/app/features/org/partials/orgApiKeys.html', template: '<react-container />',
controller: 'OrgApiKeysCtrl', resolve: {
roles: () => ['Editor', 'Admin'],
component: () => ApiKeys,
},
}) })
.when('/org/teams', { .when('/org/teams', {
template: '<react-container />', template: '<react-container />',
......
...@@ -4,6 +4,7 @@ import { createLogger } from 'redux-logger'; ...@@ -4,6 +4,7 @@ import { createLogger } from 'redux-logger';
import sharedReducers from 'app/core/reducers'; import sharedReducers from 'app/core/reducers';
import alertingReducers from 'app/features/alerting/state/reducers'; import alertingReducers from 'app/features/alerting/state/reducers';
import teamsReducers from 'app/features/teams/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers';
import apiKeysReducers from 'app/features/api-keys/state/reducers';
import foldersReducers from 'app/features/folders/state/reducers'; import foldersReducers from 'app/features/folders/state/reducers';
import dashboardReducers from 'app/features/dashboard/state/reducers'; import dashboardReducers from 'app/features/dashboard/state/reducers';
import pluginReducers from 'app/features/plugins/state/reducers'; import pluginReducers from 'app/features/plugins/state/reducers';
...@@ -12,6 +13,7 @@ const rootReducer = combineReducers({ ...@@ -12,6 +13,7 @@ const rootReducer = combineReducers({
...sharedReducers, ...sharedReducers,
...alertingReducers, ...alertingReducers,
...teamsReducers, ...teamsReducers,
...apiKeysReducers,
...foldersReducers, ...foldersReducers,
...dashboardReducers, ...dashboardReducers,
...pluginReducers, ...pluginReducers,
......
import { OrgRole } from './acl';
export interface ApiKey {
id: number;
name: string;
role: OrgRole;
}
export interface NewApiKey {
name: string;
role: OrgRole;
}
export interface ApiKeysState {
keys: ApiKey[];
searchQuery: string;
}
...@@ -6,6 +6,8 @@ import { FolderDTO, FolderState, FolderInfo } from './folders'; ...@@ -6,6 +6,8 @@ import { FolderDTO, FolderState, FolderInfo } from './folders';
import { DashboardState } from './dashboard'; import { DashboardState } from './dashboard';
import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
import { DataSource } from './datasources'; import { DataSource } from './datasources';
import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
import { User } from './user';
import { PluginMeta, Plugin, PluginsState } from './plugins'; import { PluginMeta, Plugin, PluginsState } from './plugins';
export { export {
...@@ -33,6 +35,10 @@ export { ...@@ -33,6 +35,10 @@ export {
PermissionLevel, PermissionLevel,
DataSource, DataSource,
PluginMeta, PluginMeta,
ApiKey,
ApiKeysState,
NewApiKey,
User,
Plugin, Plugin,
PluginsState, PluginsState,
}; };
......
export interface User {
id: number;
label: string;
avatarUrl: string;
login: string;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment