Skip to content

Commit

Permalink
[App Search] New log retention components + major i18n refactors (#88237
Browse files Browse the repository at this point in the history
)

* Add remaining log retention components

- Will shortly be used by Analytics view

* [i18n] Change log retention date from moment to react-intl FormattedDate

- this correctly localizes the dates when Kibana is in different languages, e.g. zh-CN, ja-JP

* Refactor log_retention/messaging

- Convert to all JSX/React vs plain strings (makes it easier to deal with FormattedDate/FormattedMessages)

- Consolidate to single component that takes various log types (e.g. analytics, api)

- This is important for adding future switches & toggles - there is *significantly* less complex strings to localize this way (2 short strings vs 5+ long sentences)

* Update existing instances to use new LogRetentionMessage

* Update translation strings
- to account for new i18n logs type values & IDs

* Attempt to fix test timezone shenanigans

* [PR feedback] Types

* [PR feedback] i18n pluralization

* Update LogRetentionTooltip and LogRetentionCallout to manage fetching log retention

+ change ILM to LogRetention per PR feedback

* Update LogRetentionLogic to prevent duplicate fetches

- e.g. if both LogRetentionTooltip and LogRetentionCallout are on the same page (which they will be)
  • Loading branch information
Constance authored Jan 15, 2021
1 parent 0325014 commit ca42e9a
Show file tree
Hide file tree
Showing 21 changed files with 695 additions and 516 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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.
*/

export { LogRetentionCallout } from './log_retention_callout';
export { LogRetentionTooltip } from './log_retention_tooltip';
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* 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_useeffect.mock';
import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock';
import { mountWithIntl } from '../../../../__mocks__';

import React from 'react';
import { shallow } from 'enzyme';
import { EuiCallOut, EuiLink } from '@elastic/eui';

import { LogRetentionOptions } from '../';
import { LogRetentionCallout } from './';

describe('LogRetentionCallout', () => {
const actions = { fetchLogRetention: jest.fn() };
const values = { myRole: { canManageLogSettings: true } };
const DISABLED = {
disabledAt: '01 Jan 1970 12:00:00 +0000',
enabled: false,
};

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

it('renders an analytics callout', () => {
setMockValues({ ...values, logRetention: { analytics: DISABLED } });
const wrapper = mountWithIntl(<LogRetentionCallout type={LogRetentionOptions.Analytics} />);

expect(wrapper.find(EuiCallOut)).toHaveLength(1);
expect(wrapper.find('.euiCallOutHeader__title').text()).toEqual(
'Analytics have been disabled since January 1, 1970.'
);
expect(wrapper.find(EuiLink)).toHaveLength(1);
expect(wrapper.find('p').text()).toEqual('To manage analytics & logging, visit your settings.');
});

it('renders an API callout', () => {
setMockValues({ ...values, logRetention: { api: DISABLED } });
const wrapper = mountWithIntl(<LogRetentionCallout type={LogRetentionOptions.API} />);

expect(wrapper.find(EuiCallOut)).toHaveLength(1);
expect(wrapper.find('.euiCallOutHeader__title').text()).toEqual(
'API Logs have been disabled since January 1, 1970.'
);
expect(wrapper.find(EuiLink)).toHaveLength(1);
expect(wrapper.find('p').text()).toEqual('To manage analytics & logging, visit your settings.');
});

it('renders a generic title if no disabled date is present', () => {
setMockValues({ ...values, logRetention: { api: { enabled: false, disabledAt: null } } });
const wrapper = mountWithIntl(<LogRetentionCallout type={LogRetentionOptions.API} />);

expect(wrapper.find(EuiCallOut)).toHaveLength(1);
expect(wrapper.find('.euiCallOutHeader__title').text()).toEqual('API Logs have been disabled.');
});

it('does not render a settings link if the user cannot manage settings', () => {
setMockValues({ myRole: { canManageLogSettings: false }, logRetention: { api: DISABLED } });
const wrapper = mountWithIntl(<LogRetentionCallout type={LogRetentionOptions.API} />);

expect(wrapper.find(EuiCallOut)).toHaveLength(1);
expect(wrapper.find(EuiLink)).toHaveLength(0);
expect(wrapper.find('p')).toHaveLength(0);
});

it('does not render if log retention is enabled', () => {
setMockValues({ ...values, logRetention: { api: { enabled: true } } });
const wrapper = shallow(<LogRetentionCallout type={LogRetentionOptions.API} />);

expect(wrapper.isEmptyRender()).toBe(true);
});

it('does not render if log retention is not available', () => {
setMockValues({ ...values, logRetention: null });
const wrapper = shallow(<LogRetentionCallout type={LogRetentionOptions.API} />);

expect(wrapper.isEmptyRender()).toBe(true);
});

describe('on mount', () => {
it('fetches log retention data when not already loaded', () => {
setMockValues({ ...values, logRetention: null });
shallow(<LogRetentionCallout type={LogRetentionOptions.API} />);

expect(actions.fetchLogRetention).toHaveBeenCalled();
});

it('does not fetch log retention data if it has already been loaded', () => {
setMockValues({ ...values, logRetention: {} });
shallow(<LogRetentionCallout type={LogRetentionOptions.API} />);

expect(actions.fetchLogRetention).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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 { useValues, useActions } from 'kea';

import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';

import { EuiLinkTo } from '../../../../shared/react_router_helpers';

import { AppLogic } from '../../../app_logic';
import { SETTINGS_PATH } from '../../../routes';
import { ANALYTICS_TITLE } from '../../analytics';
import { API_LOGS_TITLE } from '../../api_logs';

import { LogRetentionLogic, LogRetentionOptions, renderLogRetentionDate } from '../';

const TITLE_MAP = {
[LogRetentionOptions.Analytics]: ANALYTICS_TITLE,
[LogRetentionOptions.API]: API_LOGS_TITLE,
};

interface Props {
type: LogRetentionOptions;
}
export const LogRetentionCallout: React.FC<Props> = ({ type }) => {
const { fetchLogRetention } = useActions(LogRetentionLogic);
const { logRetention } = useValues(LogRetentionLogic);
const {
myRole: { canManageLogSettings },
} = useValues(AppLogic);

const hasLogRetention = logRetention !== null;

useEffect(() => {
if (!hasLogRetention) fetchLogRetention();
}, []);

const logRetentionSettings = logRetention?.[type];
const title = TITLE_MAP[type];
const hasLogRetentionDisabled = hasLogRetention && !logRetentionSettings?.enabled;

return hasLogRetentionDisabled ? (
<>
<EuiCallOut
iconType="alert"
color="primary"
title={
logRetentionSettings?.disabledAt ? (
<FormattedMessage
id="xpack.enterpriseSearch.appSearch.logRetention.callout.disabledSinceTitle"
defaultMessage="{logsTitle} have been disabled since {disabledDate}."
values={{
logsTitle: title,
disabledDate: renderLogRetentionDate(logRetentionSettings.disabledAt),
}}
/>
) : (
i18n.translate('xpack.enterpriseSearch.appSearch.logRetention.callout.disabledTitle', {
defaultMessage: '{logsTitle} have been disabled.',
values: {
logsTitle: title,
},
})
)
}
>
{canManageLogSettings && (
<p>
<FormattedMessage
id="xpack.enterpriseSearch.appSearch.logRetention.callout.description.manageSettingsDetail"
defaultMessage="To manage analytics & logging, {visitSettingsLink}."
values={{
visitSettingsLink: (
<EuiLinkTo to={SETTINGS_PATH}>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.logRetention.callout.description.manageSettingsLinkText',
{ defaultMessage: 'visit your settings' }
)}
</EuiLinkTo>
),
}}
/>
</p>
)}
</EuiCallOut>
<EuiSpacer />
</>
) : null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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_useeffect.mock';
import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock';

import React from 'react';
import { shallow, mount } from 'enzyme';
import { EuiIconTip } from '@elastic/eui';

import { LogRetentionOptions, LogRetentionMessage } from '../';
import { LogRetentionTooltip } from './';

describe('LogRetentionTooltip', () => {
const values = { logRetention: {} };
const actions = { fetchLogRetention: jest.fn() };

beforeEach(() => {
jest.clearAllMocks();
setMockValues(values);
setMockActions(actions);
});

it('renders an analytics tooltip', () => {
const wrapper = shallow(<LogRetentionTooltip type={LogRetentionOptions.Analytics} />);
const tooltipContent = wrapper.find(EuiIconTip).prop('content') as React.ReactElement;

expect(tooltipContent.type).toEqual(LogRetentionMessage);
expect(tooltipContent.props.type).toEqual('analytics');
});

it('renders an API tooltip', () => {
const wrapper = shallow(<LogRetentionTooltip type={LogRetentionOptions.API} />);
const tooltipContent = wrapper.find(EuiIconTip).prop('content') as React.ReactElement;

expect(tooltipContent.type).toEqual(LogRetentionMessage);
expect(tooltipContent.props.type).toEqual('api');
});

it('passes custom tooltip positions', () => {
const wrapper = shallow(<LogRetentionTooltip type={LogRetentionOptions.API} />);
expect(wrapper.find(EuiIconTip).prop('position')).toEqual('bottom');

wrapper.setProps({ position: 'right' });
expect(wrapper.find(EuiIconTip).prop('position')).toEqual('right');
});

it('does not render if log retention is not available', () => {
setMockValues({ logRetention: null });
const wrapper = mount(<LogRetentionTooltip type={LogRetentionOptions.API} />);

expect(wrapper.isEmptyRender()).toBe(true);
});

describe('on mount', () => {
it('fetches log retention data when not already loaded', () => {
setMockValues({ logRetention: null });
shallow(<LogRetentionTooltip type={LogRetentionOptions.Analytics} />);

expect(actions.fetchLogRetention).toHaveBeenCalled();
});

it('does not fetch log retention data if it has already been loaded', () => {
setMockValues({ logRetention: {} });
shallow(<LogRetentionTooltip type={LogRetentionOptions.Analytics} />);

expect(actions.fetchLogRetention).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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 { useValues, useActions } from 'kea';

import { i18n } from '@kbn/i18n';
import { EuiIconTip } from '@elastic/eui';

import { LogRetentionLogic, LogRetentionMessage, LogRetentionOptions } from '../';

interface Props {
type: LogRetentionOptions;
position?: 'top' | 'right' | 'bottom' | 'left';
}
export const LogRetentionTooltip: React.FC<Props> = ({ type, position = 'bottom' }) => {
const { fetchLogRetention } = useActions(LogRetentionLogic);
const { logRetention } = useValues(LogRetentionLogic);

const hasLogRetention = logRetention !== null;

useEffect(() => {
if (!hasLogRetention) fetchLogRetention();
}, []);

return hasLogRetention ? (
<EuiIconTip
aria-label={i18n.translate('xpack.enterpriseSearch.appSearch.logRetention.tooltip', {
defaultMessage: 'Log retention info',
})}
size="l"
type="iInCircle"
color="primary"
position={position}
content={<LogRetentionMessage type={type} />}
/>
) : null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
export { LogRetentionLogic } from './log_retention_logic';
export * from './types';
export * from './messaging';
export * from './components';
Loading

0 comments on commit ca42e9a

Please sign in to comment.