Skip to content

Commit

Permalink
[Synthetics] TLS Certs page copied from uptime (elastic#159541)
Browse files Browse the repository at this point in the history
Co-authored-by: Justin Kambic <jk@elastic.co>
  • Loading branch information
2 people authored and saarikabhasi committed Jun 14, 2023
1 parent 6f5b58a commit 7ce45fd
Show file tree
Hide file tree
Showing 23 changed files with 932 additions and 17 deletions.
@@ -0,0 +1,30 @@
/*
* 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 from 'react';
import { EuiToolTip } from '@elastic/eui';
import { MonitorPageLink } from './monitor_page_link';
import { CertMonitor } from '../../../../../common/runtime_types';

interface Props {
monitors: CertMonitor[];
}

export const CertMonitors: React.FC<Props> = ({ monitors }) => {
return (
<span>
{monitors.map((mon: CertMonitor, ind: number) => (
<span key={mon.id}>
{ind > 0 && ', '}
<EuiToolTip content={mon.url}>
<MonitorPageLink configId={mon.configId!}>{mon.name || mon.id}</MonitorPageLink>
</EuiToolTip>
</span>
))}
</span>
);
};
@@ -0,0 +1,56 @@
/*
* 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, { useContext } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiHideFor,
EuiShowFor,
} from '@elastic/eui';
import * as labels from './translations';
import { SyntheticsRefreshContext } from '../../contexts';

export const CertRefreshBtn = () => {
const { refreshApp } = useContext(SyntheticsRefreshContext);

return (
<EuiFlexItem
style={{ alignItems: 'flex-end' }}
grow={false}
data-test-subj="certificatesRefreshButton"
>
<EuiFlexGroup responsive={false} gutterSize="s">
<EuiFlexItem grow={false}>
<EuiHideFor sizes={['xs']}>
<EuiButton
fill
iconType="refresh"
onClick={() => {
refreshApp();
}}
data-test-subj="superDatePickerApplyTimeButton"
>
{labels.REFRESH_CERT}
</EuiButton>
</EuiHideFor>
<EuiShowFor sizes={['xs']}>
<EuiButtonEmpty
iconType="refresh"
onClick={() => {
refreshApp();
}}
data-test-subj="superDatePickerApplyTimeButton"
/>
</EuiShowFor>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
);
};
@@ -0,0 +1,49 @@
/*
* 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, { ChangeEvent, useState } from 'react';
import { EuiFieldSearch } from '@elastic/eui';
import styled from 'styled-components';
import useDebounce from 'react-use/lib/useDebounce';
import * as labels from './translations';

const WrapFieldSearch = styled('div')`
max-width: 700px;
`;

interface Props {
setSearch: (val: string) => void;
}

export const CertificateSearch: React.FC<Props> = ({ setSearch }) => {
const [debouncedValue, setDebouncedValue] = useState('');

const onChange = (e: ChangeEvent<HTMLInputElement>) => {
setDebouncedValue(e.target.value);
};

useDebounce(
() => {
setSearch(debouncedValue);
},
350,
[debouncedValue]
);

return (
<WrapFieldSearch>
<EuiFieldSearch
data-test-subj="uptimeCertSearch"
placeholder={labels.SEARCH_CERTS}
onChange={onChange}
isClearable={true}
aria-label={labels.SEARCH_CERTS}
fullWidth={true}
/>
</WrapFieldSearch>
);
};
@@ -0,0 +1,96 @@
/*
* 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 from 'react';
import moment from 'moment';
import styled from 'styled-components';
import { EuiHealth, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useCertStatus } from './use_cert_status';
import { CERT_STATUS, DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../common/constants';
import { Cert } from '../../../../../common/runtime_types';
import * as labels from './translations';

interface Props {
cert: Cert;
}

const DateText = styled(EuiText)`
display: inline-block;
margin-left: 5px;
`;

export const CertStatus: React.FC<Props> = ({ cert }) => {
const certStatus = useCertStatus(cert?.not_after, cert?.not_before);

const relativeDate = moment(cert?.not_after).fromNow();

if (certStatus === CERT_STATUS.EXPIRING_SOON) {
return (
<EuiHealth color="warning">
<span>
{labels.EXPIRES_SOON}
{' '}
<DateText color="subdued" size="xs">
{relativeDate}
</DateText>
</span>
</EuiHealth>
);
}
if (certStatus === CERT_STATUS.EXPIRED) {
return (
<EuiHealth color="danger">
<span>
{labels.EXPIRED}
{' '}
<DateText color="subdued" size="xs">
{relativeDate}
</DateText>
</span>
</EuiHealth>
);
}

if (certStatus === CERT_STATUS.TOO_OLD) {
const ageThreshold = DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold;

const oldRelativeDate = moment(cert?.not_before).add(ageThreshold, 'days').fromNow();

return (
<EuiHealth color="danger">
<span>
{labels.TOO_OLD}
<DateText color="subdued" size="xs">
{oldRelativeDate}
</DateText>
</span>
</EuiHealth>
);
}

const okRelativeDate = moment(cert?.not_after).fromNow(true);

return (
<EuiHealth color="success">
<span>
{labels.OK}
{' '}
<DateText color="subdued" size="xs">
<FormattedMessage
id="xpack.synthetics.certs.status.ok.label"
defaultMessage=" for {okRelativeDate}"
description='Denotes an amount of time for which a cert is valid. Example: "OK for 2 days"'
values={{
okRelativeDate,
}}
/>
</DateText>
</span>
</EuiHealth>
);
};
@@ -0,0 +1,25 @@
/*
* 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 from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { useSelector } from 'react-redux';
import { certificatesSelector } from '../../state/certificates/certificates';

export const CertificateTitle = () => {
const total = useSelector(certificatesSelector);

return (
<FormattedMessage
id="xpack.synthetics.certificates.heading"
defaultMessage="TLS Certificates ({total})"
values={{
total: <span data-test-subj="uptimeCertTotal">{total ?? 0}</span>,
}}
/>
);
};
@@ -0,0 +1,19 @@
/*
* 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 from 'react';
import { CertificatesPage } from './certificates';
import { render } from '../../utils/testing';

describe('CertificatesPage', () => {
it('renders expected elements for valid props', async () => {
const { findByText } = render(<CertificatesPage />);

expect(await findByText('This table contains 0 rows; Page 1 of 0.')).toBeInTheDocument();
expect(await findByText('No Certificates found.')).toBeInTheDocument();
});
});
@@ -0,0 +1,72 @@
/*
* 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 { useDispatch } from 'react-redux';
import { EuiSpacer } from '@elastic/eui';
import React, { useEffect, useState } from 'react';
import { useTrackPageview } from '@kbn/observability-shared-plugin/public';
import { setCertificatesTotalAction } from '../../state/certificates/certificates';
import { CertificateSearch } from './cert_search';
import { useCertSearch } from './use_cert_search';
import { CertificateList, CertSort } from './certificates_list';
import { useBreadcrumbs } from '../../hooks';

const DEFAULT_PAGE_SIZE = 10;
const LOCAL_STORAGE_KEY = 'xpack.uptime.certList.pageSize';
const getPageSizeValue = () => {
const value = parseInt(localStorage.getItem(LOCAL_STORAGE_KEY) ?? '', 10);
if (isNaN(value)) {
return DEFAULT_PAGE_SIZE;
}
return value;
};

export const CertificatesPage: React.FC = () => {
useTrackPageview({ app: 'uptime', path: 'certificates' });
useTrackPageview({ app: 'uptime', path: 'certificates', delay: 15000 });

useBreadcrumbs([{ text: 'Certificates' }]);

const [page, setPage] = useState({ index: 0, size: getPageSizeValue() });
const [sort, setSort] = useState<CertSort>({
field: 'not_after',
direction: 'asc',
});
const [search, setSearch] = useState('');

const dispatch = useDispatch();

const certificates = useCertSearch({
search,
size: page.size,
pageIndex: page.index,
sortBy: sort.field,
direction: sort.direction,
});

useEffect(() => {
dispatch(setCertificatesTotalAction({ total: certificates.total }));
}, [certificates.total, dispatch]);

return (
<>
<EuiSpacer size="m" />
<CertificateSearch setSearch={setSearch} />
<EuiSpacer size="m" />
<CertificateList
page={page}
onChange={(pageVal, sortVal) => {
setPage(pageVal);
setSort(sortVal);
localStorage.setItem(LOCAL_STORAGE_KEY, pageVal.size.toString());
}}
sort={sort}
certificates={certificates}
/>
</>
);
};

0 comments on commit 7ce45fd

Please sign in to comment.