Skip to content

Commit

Permalink
[Workplace Search] Add API Keys view to replace Access tokens (#120147)
Browse files Browse the repository at this point in the history
  • Loading branch information
scottybollinger authored Dec 7, 2021
1 parent 233aa7d commit 062be17
Show file tree
Hide file tree
Showing 35 changed files with 2,001 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ readonly links: {
readonly usersAccess: string;
};
readonly workplaceSearch: {
readonly apiKeys: string;
readonly box: string;
readonly confluenceCloud: string;
readonly confluenceServer: string;
Expand Down

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/core/public/doc_links/doc_links_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export class DocLinksService {
usersAccess: `${ENTERPRISE_SEARCH_DOCS}users-access.html`,
},
workplaceSearch: {
apiKeys: `${WORKPLACE_SEARCH_DOCS}workplace-search-api-authentication.html`,
box: `${WORKPLACE_SEARCH_DOCS}workplace-search-box-connector.html`,
confluenceCloud: `${WORKPLACE_SEARCH_DOCS}workplace-search-confluence-cloud-connector.html`,
confluenceServer: `${WORKPLACE_SEARCH_DOCS}workplace-search-confluence-server-connector.html`,
Expand Down Expand Up @@ -671,6 +672,7 @@ export interface DocLinksStart {
readonly usersAccess: string;
};
readonly workplaceSearch: {
readonly apiKeys: string;
readonly box: string;
readonly confluenceCloud: string;
readonly confluenceServer: string;
Expand Down
1 change: 1 addition & 0 deletions src/core/public/public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ export interface DocLinksStart {
readonly usersAccess: string;
};
readonly workplaceSearch: {
readonly apiKeys: string;
readonly box: string;
readonly confluenceCloud: string;
readonly confluenceServer: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class DocLinks {
public enterpriseSearchMailService: string;
public enterpriseSearchUsersAccess: string;
public licenseManagement: string;
public workplaceSearchApiKeys: string;
public workplaceSearchBox: string;
public workplaceSearchConfluenceCloud: string;
public workplaceSearchConfluenceServer: string;
Expand Down Expand Up @@ -86,6 +87,7 @@ class DocLinks {
this.enterpriseSearchMailService = '';
this.enterpriseSearchUsersAccess = '';
this.licenseManagement = '';
this.workplaceSearchApiKeys = '';
this.workplaceSearchBox = '';
this.workplaceSearchConfluenceCloud = '';
this.workplaceSearchConfluenceServer = '';
Expand Down Expand Up @@ -139,6 +141,7 @@ class DocLinks {
this.enterpriseSearchMailService = docLinks.links.enterpriseSearch.mailService;
this.enterpriseSearchUsersAccess = docLinks.links.enterpriseSearch.usersAccess;
this.licenseManagement = docLinks.links.enterpriseSearch.licenseManagement;
this.workplaceSearchApiKeys = docLinks.links.workplaceSearch.apiKeys;
this.workplaceSearchBox = docLinks.links.workplaceSearch.box;
this.workplaceSearchConfluenceCloud = docLinks.links.workplaceSearch.confluenceCloud;
this.workplaceSearchConfluenceServer = docLinks.links.workplaceSearch.confluenceServer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ describe('useWorkplaceSearchNav', () => {
name: 'Users and roles',
href: '/users_and_roles',
},
{
id: 'apiKeys',
name: 'API keys',
href: '/api_keys',
},
{
id: 'security',
name: 'Security',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { EuiSideNavItemType } from '@elastic/eui';
import { generateNavLink } from '../../../shared/layout';
import { NAV } from '../../constants';
import {
API_KEYS_PATH,
SOURCES_PATH,
SECURITY_PATH,
USERS_AND_ROLES_PATH,
Expand Down Expand Up @@ -47,6 +48,11 @@ export const useWorkplaceSearchNav = () => {
name: NAV.ROLE_MAPPINGS,
...generateNavLink({ to: USERS_AND_ROLES_PATH }),
},
{
id: 'apiKeys',
name: NAV.API_KEYS,
...generateNavLink({ to: API_KEYS_PATH }),
},
{
id: 'security',
name: NAV.SECURITY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export const NAV = {
ROLE_MAPPINGS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.roleMappings', {
defaultMessage: 'Users and roles',
}),
API_KEYS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.apiKeys', {
defaultMessage: 'API keys',
}),
SECURITY: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.security', {
defaultMessage: 'Security',
}),
Expand Down Expand Up @@ -329,6 +332,20 @@ export const SOURCE_OBJ_TYPES = {
),
};

export const API_KEYS_TITLE = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.sources.apiKeysTitle',
{
defaultMessage: 'API keys',
}
);

export const API_KEY_LABEL = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.sources.apiKeyLabel',
{
defaultMessage: 'API key',
}
);

export const GITHUB_LINK_TITLE = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.sources.applicationLinkTitles.github',
{
Expand Down Expand Up @@ -866,3 +883,14 @@ export const PLATINUM_FEATURE = i18n.translate(
defaultMessage: 'Platinum feature',
}
);

export const COPY_TOOLTIP = i18n.translate('xpack.enterpriseSearch.workplaceSearch.copy.tooltip', {
defaultMessage: 'Copy to clipboard',
});

export const COPIED_TOOLTIP = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.copied.tooltip',
{
defaultMessage: 'Copied!',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ import {
PRIVATE_SOURCES_PATH,
ORG_SETTINGS_PATH,
USERS_AND_ROLES_PATH,
API_KEYS_PATH,
SECURITY_PATH,
PERSONAL_SETTINGS_PATH,
PERSONAL_PATH,
} from './routes';
import { AccountSettings } from './views/account_settings';
import { ApiKeys } from './views/api_keys';
import { SourcesRouter } from './views/content_sources';
import { SourceAdded } from './views/content_sources/components/source_added';
import { ErrorState } from './views/error_state';
Expand Down Expand Up @@ -133,6 +135,9 @@ export const WorkplaceSearchConfigured: React.FC<InitialAppData> = (props) => {
<Route path={USERS_AND_ROLES_PATH}>
<RoleMappings />
</Route>
<Route path={API_KEYS_PATH}>
<ApiKeys />
</Route>
<Route path={SECURITY_PATH}>
<Security />
</Route>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export const SEARCH_AUTHORIZE_PATH = `${PERSONAL_PATH}/authorize_search`;

export const USERS_AND_ROLES_PATH = '/users_and_roles';

export const API_KEYS_PATH = '/api_keys';

export const SECURITY_PATH = '/security';

export const GROUPS_PATH = '/groups';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,3 +302,9 @@ export interface WSRoleMapping extends RoleMapping {
allGroups: boolean;
groups: RoleGroup[];
}

export interface ApiToken {
key?: string;
id?: string;
name: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic';
import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock';

import React from 'react';

import { shallow } from 'enzyme';

import { EuiEmptyPrompt, EuiCopy } from '@elastic/eui';

import { DEFAULT_META } from '../../../shared/constants';
import { externalUrl } from '../../../shared/enterprise_search_url';

import { ApiKeys } from './api_keys';
import { ApiKeyFlyout } from './components/api_key_flyout';
import { ApiKeysList } from './components/api_keys_list';

describe('ApiKeys', () => {
const fetchApiKeys = jest.fn();
const resetApiKeys = jest.fn();
const showApiKeysForm = jest.fn();
const apiToken = {
id: '1',
name: 'test',
key: 'foo',
};

const values = {
apiKeyFormVisible: false,
meta: DEFAULT_META,
dataLoading: false,
apiTokens: [apiToken],
};

beforeEach(() => {
jest.clearAllMocks();
setMockValues(values);
setMockActions({
fetchApiKeys,
resetApiKeys,
showApiKeysForm,
});
});

it('renders', () => {
const wrapper = shallow(<ApiKeys />);

expect(wrapper.find(ApiKeysList)).toHaveLength(1);
});

it('renders EuiEmptyPrompt when no api keys present', () => {
setMockValues({ ...values, apiTokens: [] });
const wrapper = shallow(<ApiKeys />);

expect(wrapper.find(ApiKeysList)).toHaveLength(0);
expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
});

it('fetches data on mount', () => {
shallow(<ApiKeys />);

expect(fetchApiKeys).toHaveBeenCalledTimes(1);
});

it('calls resetApiKeys on unmount', () => {
shallow(<ApiKeys />);
unmountHandler();

expect(resetApiKeys).toHaveBeenCalledTimes(1);
});

it('renders the API endpoint and a button to copy it', () => {
externalUrl.enterpriseSearchUrl = 'http://localhost:3002';
const copyMock = jest.fn();
const wrapper = shallow(<ApiKeys />);
// We wrap children in a div so that `shallow` can render it.
const copyEl = shallow(<div>{wrapper.find(EuiCopy).props().children(copyMock)}</div>);

expect(copyEl.find('EuiButtonIcon').props().onClick).toEqual(copyMock);
expect(copyEl.text().replace('<EuiButtonIcon />', '')).toEqual('http://localhost:3002');
});

it('will render ApiKeyFlyout if apiKeyFormVisible is true', () => {
setMockValues({ ...values, apiKeyFormVisible: true });
const wrapper = shallow(<ApiKeys />);

expect(wrapper.find(ApiKeyFlyout)).toHaveLength(1);
});

it('will NOT render ApiKeyFlyout if apiKeyFormVisible is false', () => {
setMockValues({ ...values, apiKeyFormVisible: false });
const wrapper = shallow(<ApiKeys />);

expect(wrapper.find(ApiKeyFlyout)).toHaveLength(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { useEffect } from 'react';

import { useActions, useValues } from 'kea';

import {
EuiButton,
EuiTitle,
EuiPanel,
EuiCopy,
EuiButtonIcon,
EuiSpacer,
EuiEmptyPrompt,
} from '@elastic/eui';

import { docLinks } from '../../../shared/doc_links';
import { externalUrl } from '../../../shared/enterprise_search_url/external_url';

import { WorkplaceSearchPageTemplate } from '../../components/layout';
import { NAV, API_KEYS_TITLE } from '../../constants';

import { ApiKeysLogic } from './api_keys_logic';
import { ApiKeyFlyout } from './components/api_key_flyout';
import { ApiKeysList } from './components/api_keys_list';
import {
API_KEYS_EMPTY_TITLE,
API_KEYS_EMPTY_BODY,
API_KEYS_EMPTY_BUTTON_LABEL,
CREATE_KEY_BUTTON_LABEL,
ENDPOINT_TITLE,
COPIED_TOOLTIP,
COPY_API_ENDPOINT_BUTTON_LABEL,
} from './constants';

export const ApiKeys: React.FC = () => {
const { fetchApiKeys, resetApiKeys, showApiKeyForm } = useActions(ApiKeysLogic);

const { meta, dataLoading, apiKeyFormVisible, apiTokens } = useValues(ApiKeysLogic);

useEffect(() => {
fetchApiKeys();
return resetApiKeys;
}, [meta.page.current]);

const hasApiKeys = apiTokens.length > 0;

const addKeyButton = (
<EuiButton fill onClick={showApiKeyForm}>
{CREATE_KEY_BUTTON_LABEL}
</EuiButton>
);

const emptyPrompt = (
<EuiEmptyPrompt
iconType="editorStrike"
title={<h2>{API_KEYS_EMPTY_TITLE}</h2>}
body={API_KEYS_EMPTY_BODY}
actions={
<EuiButton
size="s"
target="_blank"
iconType="popout"
href={docLinks.workplaceSearchApiKeys}
>
{API_KEYS_EMPTY_BUTTON_LABEL}
</EuiButton>
}
/>
);

return (
<WorkplaceSearchPageTemplate
pageChrome={[NAV.API_KEYS]}
pageHeader={{
pageTitle: API_KEYS_TITLE,
rightSideItems: [addKeyButton],
}}
isLoading={dataLoading}
emptyState={!hasApiKeys && emptyPrompt}
>
{apiKeyFormVisible && <ApiKeyFlyout />}
<EuiPanel color="subdued" className="eui-textCenter">
<EuiTitle size="s">
<h2>{ENDPOINT_TITLE}</h2>
</EuiTitle>
<EuiCopy textToCopy={externalUrl.enterpriseSearchUrl} afterMessage={COPIED_TOOLTIP}>
{(copy) => (
<>
<EuiButtonIcon
onClick={copy}
iconType="copyClipboard"
aria-label={COPY_API_ENDPOINT_BUTTON_LABEL}
/>
{externalUrl.enterpriseSearchUrl}
</>
)}
</EuiCopy>
</EuiPanel>
<EuiSpacer size="xxl" />
<EuiPanel hasBorder>{hasApiKeys ? <ApiKeysList /> : emptyPrompt}</EuiPanel>
</WorkplaceSearchPageTemplate>
);
};
Loading

0 comments on commit 062be17

Please sign in to comment.