Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions x-pack/plugins/enterprise_search/common/types/app_search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,42 @@ export interface IConfiguredLimits {
maxEnginesPerMetaEngine: number;
};
}

export interface IApiToken {
access_all_engines?: boolean;
key?: string;
engines?: string[];
id?: number;
name: string;
read?: boolean;
type: string;
write?: boolean;
}

export interface IMetaPage {
current: number;
size: number;
total_pages: number;
total_results: number;
}

export interface IMeta {
page: IMetaPage;
}

export interface IEngine {
name: string;
type: string;
language: string;
result_fields: object[];
}

export interface ICredentialsDetails {
lmAccount: {
id: string;
key: string;
};
apiUrl: string;
apiTokens: IApiToken[];
engines: IEngine[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface IAppActions {
}

export const AppLogic = kea<MakeLogicType<IAppValues, IAppActions>>({
path: ['enterprise_search', 'app_search', 'app'],
actions: {
initializeAppData: (props) => props,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import '../../../__mocks__/shallow_usecontext.mock';
import '../../../__mocks__/kea.mock';

import React from 'react';
import { shallow } from 'enzyme';
import { useValues, useActions } from 'kea';

import { Credentials } from './credentials';
import { EuiPageContentBody } from '@elastic/eui';
import { CredentialsFlyout } from './credentials_flyout';

describe('Credentials', () => {
const mockKea = ({ values = {}, actions = {} }) => {
const mergedValues = {
apiUrl: 'http://www.example.com',
dataLoading: false,
showCredentialsForm: false,
...values,
};

const mergedActions = {
initializeCredentialsData: jest.fn,
...actions,
};

(useValues as jest.Mock).mockImplementationOnce(() => mergedValues);
(useActions as jest.Mock).mockImplementationOnce(() => mergedActions);
};

beforeEach(() => {
jest.clearAllMocks();
});

it('renders', () => {
mockKea({});
const wrapper = shallow(<Credentials />);
expect(wrapper.find(EuiPageContentBody)).toHaveLength(1);
});

it('initializes data on mount', () => {
const initializeCredentialsData = jest.fn();
mockKea({ actions: { initializeCredentialsData } });
const wrapper = shallow(<Credentials />);
expect(initializeCredentialsData).toHaveBeenCalledTimes(1);
});

it('renders nothing if data is still oading', () => {
mockKea({ values: { dataLoading: true } });
const wrapper = shallow(<Credentials />);
expect(wrapper.find(EuiPageContentBody)).toHaveLength(0);
});

it('will render CredentialsFlyout if showCredentialsForm is true', () => {
mockKea({ values: { showCredentialsForm: true } });
const wrapper = shallow(<Credentials />);
expect(wrapper.find(CredentialsFlyout)).toHaveLength(1);
});

it('will NOT render CredentialsFlyout if showCredentialsForm is false', () => {
mockKea({ values: { showCredentialsForm: false } });
const wrapper = shallow(<Credentials />);
expect(wrapper.find(CredentialsFlyout)).toHaveLength(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';

import {
EuiPageHeader,
EuiPageHeaderSection,
EuiTitle,
EuiPageContentBody,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiCopy,
EuiButtonIcon,
EuiSpacer,
EuiButton,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { CredentialsList } from './credentials_list';
import { CredentialsFlyout } from './credentials_flyout';
import {
CredentialsLogic,
ICredentialsLogicActions,
ICredentialsLogicValues,
} from './credentials_logic';

export const Credentials: React.FC = () => {
const { initializeCredentialsData, resetCredentials, toggleCredentialsForm } = useActions(
CredentialsLogic
) as ICredentialsLogicActions;

const { apiUrl, dataLoading, showCredentialsForm } = useValues(
CredentialsLogic
) as ICredentialsLogicValues;

useEffect(() => {
initializeCredentialsData();
return resetCredentials;
}, []);

// TODO
// if (dataLoading) { return <Loading /> }
if (dataLoading) {
return null;
}
return (
<>
<SetPageChrome isRoot />
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>
<FormattedMessage
id="xpack.enterpriseSearch.appSearch.credentials.title"
defaultMessage="Credentials"
/>
</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContentBody>
{showCredentialsForm && <CredentialsFlyout />}
<EuiFlexGroup>
<EuiFlexItem>
<EuiPanel style={{ textAlign: 'center' }}>
<EuiTitle size="s">
<h2>
<FormattedMessage
id="xpack.enterpriseSearch.appSearch.credentials.apiEndpoint"
defaultMessage="API Endpoint"
/>
</h2>
</EuiTitle>
<EuiCopy
textToCopy={apiUrl}
afterMessage={i18n.translate(
'xpack.enterpriseSearch.appSearch.credentials.copied',
{
defaultMessage: 'Copied',
}
)}
>
{(copy) => (
<div>
<EuiButtonIcon
onClick={copy}
iconType="copyClipboard"
aria-label={i18n.translate(
'xpack.enterpriseSearch.appSearch.credentials.copyApiEndpoint',
{
defaultMessage: 'Copy API Endpoint to clipboard',
}
)}
/>
<span>{apiUrl}</span>
</div>
)}
</EuiCopy>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xxl" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="m">
<h2>
<FormattedMessage
id="xpack.enterpriseSearch.appSearch.credentials.apiKeys"
defaultMessage="API Keys"
/>
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
color="primary"
data-test-subj="CreateAPIKeyButton"
fill={true}
onClick={() => toggleCredentialsForm()}
>
<FormattedMessage
id="xpack.enterpriseSearch.appSearch.credentials.createKey"
defaultMessage="Create a Key"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem>
<EuiPanel>
<CredentialsList />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentBody>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import '../../../__mocks__/shallow_usecontext.mock';
import '../../../__mocks__/kea.mock';

import React from 'react';
import { shallow } from 'enzyme';
import { useValues, useActions } from 'kea';

import { PRIVATE } from '../../constants/credentials';

import { CredentialsFlyout } from './credentials_flyout';
import { EuiFlyout } from '@elastic/eui';
import { IApiToken } from '../../../../../common/types/app_search';

describe('Credentials', () => {
const apiToken: IApiToken = {
name: '',
type: PRIVATE,
read: true,
write: true,
access_all_engines: true,
};

const mockKea = ({ values = {}, actions = {} }) => {
const mergedValues = {
activeApiToken: apiToken,
activeApiTokenIsExisting: false,
...values,
};

const mergedActions = {
hideCredentialsForm: jest.fn,
...actions,
};

(useValues as jest.Mock).mockImplementationOnce(() => mergedValues);
(useActions as jest.Mock).mockImplementationOnce(() => mergedActions);
};

beforeEach(() => {
jest.clearAllMocks();
});

it('renders', () => {
mockKea({});
const wrapper = shallow(<CredentialsFlyout />);
expect(wrapper.find(EuiFlyout)).toHaveLength(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import { useActions, useValues } from 'kea';
import {
EuiFlyout,
EuiFlyoutHeader,
EuiTitle,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiButton,
} from '@elastic/eui';

import {
CredentialsLogic,
ICredentialsLogicActions,
ICredentialsLogicValues,
} from './credentials_logic';

const CredentialsFlyout: React.FC = () => {
const { hideCredentialsForm } = useActions(CredentialsLogic) as ICredentialsLogicActions;

const { activeApiToken, activeApiTokenIsExisting } = useValues(
CredentialsLogic
) as ICredentialsLogicValues;

return (
<EuiFlyout
onClose={hideCredentialsForm}
hideCloseButton={true}
// ownFocus={true}
aria-labelledby="credentialsFlyoutTitle"
size="s"
>
<EuiFlyoutHeader hasBorder={true}>
<EuiTitle size="m">
<h2 id="credentialsFlyoutTitle">
{activeApiToken.id ? `Update ${activeApiToken.name}` : 'Create A New Key'}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody style={{ display: 'flex' }}>Details go here</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={hideCredentialsForm}>
Close
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={() => window.alert(`submit`)}
fill={true}
color="secondary"
iconType="check"
data-test-subj="APIKeyActionButton"
>
{activeApiTokenIsExisting ? 'Update' : 'Save'}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
};

export { CredentialsFlyout };
Loading