[Synthetics] TLS Certs page copied from uptime (#159541)

Co-authored-by: Justin Kambic <jk@elastic.co>
This commit is contained in:
Shahzad 2023-06-13 18:21:02 +02:00 committed by GitHub
parent ceca2bdbd8
commit d72524a72d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 932 additions and 17 deletions

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>,
}}
/>
);
};

View file

@ -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();
});
});

View file

@ -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}
/>
</>
);
};

View file

@ -0,0 +1,76 @@
/*
* 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 { CertificateList, CertSort } from './certificates_list';
import { render } from '../../utils/testing';
describe('CertificateList', () => {
it('render empty state', () => {
const page = {
index: 0,
size: 10,
};
const sort: CertSort = {
field: 'not_after',
direction: 'asc',
};
const { getByText } = render(
<CertificateList
page={page}
sort={sort}
onChange={jest.fn()}
certificates={{ loading: false, total: 0, certs: [] }}
/>
);
expect(getByText('No Certificates found.')).toBeInTheDocument();
});
it('renders certificates list', () => {
const page = {
index: 0,
size: 10,
};
const sort: CertSort = {
field: 'not_after',
direction: 'asc',
};
const { getByText } = render(
<CertificateList
page={page}
sort={sort}
onChange={jest.fn()}
certificates={{
loading: false,
total: 1,
certs: [
{
monitors: [
{
name: 'BadSSL Expired',
id: 'expired-badssl',
url: 'https://expired.badssl.com/',
},
],
issuer: 'COMODO RSA Domain Validation Secure Server CA',
sha1: '404bbd2f1f4cc2fdeef13aabdd523ef61f1c71f3',
sha256: 'ba105ce02bac76888ecee47cd4eb7941653e9ac993b61b2eb3dcc82014d21b4f',
not_after: '2015-04-12T23:59:59.000Z',
not_before: '2015-04-09T00:00:00.000Z',
common_name: '*.badssl.com',
},
],
}}
/>
);
expect(getByText('BadSSL Expired')).toBeInTheDocument();
});
});

View file

@ -0,0 +1,120 @@
/*
* 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 { Direction, EuiBasicTable } from '@elastic/eui';
import { Cert, CertMonitor, CertResult } from '../../../../../common/runtime_types';
import { CertStatus } from './cert_status';
import { CertMonitors } from './cert_monitors';
import * as labels from './translations';
import { FingerprintCol } from './fingerprint_col';
import { LOADING_CERTIFICATES, NO_CERTS_AVAILABLE } from './translations';
interface Page {
index: number;
size: number;
}
export type CertFields =
| 'sha256'
| 'sha1'
| 'issuer'
| 'common_name'
| 'monitors'
| 'not_after'
| 'not_before';
export interface CertSort {
field: CertFields;
direction: Direction;
}
interface Props {
page: Page;
sort: CertSort;
onChange: (page: Page, sort: CertSort) => void;
certificates: CertResult & { loading?: boolean };
}
export const CertificateList: React.FC<Props> = ({ page, certificates, sort, onChange }) => {
const onTableChange = (newVal: Partial<Props>) => {
onChange(newVal.page as Page, newVal.sort as CertSort);
};
const pagination = {
pageIndex: page.index,
pageSize: page.size,
totalItemCount: certificates?.total ?? 0,
pageSizeOptions: [10, 25, 50, 100],
showPerPageOptions: true,
};
const columns = [
{
field: 'not_after',
name: labels.STATUS_COL,
sortable: true,
render: (val: string, item: Cert) => <CertStatus cert={item} />,
},
{
name: labels.COMMON_NAME_COL,
field: 'common_name',
sortable: true,
},
{
name: labels.MONITORS_COL,
field: 'monitors',
render: (monitors: CertMonitor[]) => <CertMonitors monitors={monitors} />,
},
{
name: labels.ISSUED_BY_COL,
field: 'issuer',
sortable: true,
},
{
name: labels.VALID_UNTIL_COL,
field: 'not_after',
sortable: true,
render: (value: string) => moment(value).format('L LT'),
},
{
name: labels.AGE_COL,
field: 'not_before',
sortable: true,
render: (value: string) => moment().diff(moment(value), 'days') + ' ' + labels.DAYS,
},
{
name: labels.FINGERPRINTS_COL,
field: 'sha256',
render: (val: string, item: Cert) => <FingerprintCol cert={item} />,
},
];
return (
<EuiBasicTable
loading={certificates.loading}
columns={columns}
items={certificates?.certs ?? []}
pagination={pagination}
onChange={onTableChange}
sorting={{
sort: {
field: sort.field,
direction: sort.direction,
},
}}
noItemsMessage={
certificates.loading ? (
LOADING_CERTIFICATES
) : (
<span data-test-subj="uptimeCertsEmptyMessage">{NO_CERTS_AVAILABLE}</span>
)
}
/>
);
};

View file

@ -0,0 +1,34 @@
/*
* 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 { FingerprintCol } from './fingerprint_col';
import { render } from '../../utils/testing';
describe('FingerprintCol', () => {
const cert = {
monitors: [{ name: '', id: 'github', url: 'https://github.com/' }],
not_after: '2020-05-08T00:00:00.000Z',
not_before: '2018-05-08T00:00:00.000Z',
issuer: 'DigiCert SHA2 Extended Validation Server CA',
sha1: 'ca06f56b258b7a0d4f2b05470939478651151984'.toUpperCase(),
sha256: '3111500c4a66012cdae333ec3fca1c9dde45c954440e7ee413716bff3663c074'.toUpperCase(),
common_name: 'github.com',
};
it('renders expected elements for valid props', async () => {
cert.not_after = moment().add('4', 'months').toISOString();
const { findByText, findByTestId } = render(<FingerprintCol cert={cert} />);
expect(await findByText('SHA 1')).toBeInTheDocument();
expect(await findByText('SHA 256')).toBeInTheDocument();
expect(await findByTestId(cert.sha1)).toBeInTheDocument();
expect(await findByTestId(cert.sha256)).toBeInTheDocument();
});
});

View file

@ -0,0 +1,54 @@
/*
* 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 { EuiButtonEmpty, EuiButtonIcon, EuiCopy, EuiToolTip } from '@elastic/eui';
import styled from 'styled-components';
import { Cert } from '../../../../../common/runtime_types';
import { COPY_FINGERPRINT } from './translations';
const EmptyButton = styled(EuiButtonEmpty)`
.euiButtonEmpty__content {
padding-right: 0px;
}
`;
const StyledSpan = styled.span`
margin-right: 8px;
`;
interface Props {
cert: Cert;
}
export const FingerprintCol: React.FC<Props> = ({ cert }) => {
const ShaComponent = ({ text, val }: { text: string; val: string }) => {
return (
<StyledSpan data-test-subj={val} className="eui-textNoWrap">
<EuiToolTip content={val}>
<EmptyButton>{text} </EmptyButton>
</EuiToolTip>
<EuiCopy textToCopy={val ?? ''}>
{(copy) => (
<EuiButtonIcon
aria-label={COPY_FINGERPRINT}
onClick={copy}
iconType="copy"
title={COPY_FINGERPRINT}
/>
)}
</EuiCopy>
</StyledSpan>
);
};
return (
<span>
<ShaComponent text="SHA 1" val={cert?.sha1?.toUpperCase() ?? ''} />
<ShaComponent text="SHA 256" val={cert?.sha256?.toUpperCase() ?? ''} />
</span>
);
};

View file

@ -0,0 +1,12 @@
/*
* 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.
*/
export * from './cert_monitors';
export * from './cert_search';
export * from './cert_status';
export * from './certificates_list';
export * from './fingerprint_col';

View file

@ -0,0 +1,26 @@
/*
* 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, { FC } from 'react';
import { EuiLink } from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public';
interface DetailPageLinkProps {
configId: string;
}
export const MonitorPageLink: FC<DetailPageLinkProps> = ({ children, configId }) => {
const basePath = useKibana().services.http?.basePath.get();
return (
<EuiLink
data-test-subj="syntheticsMonitorPageLinkLink"
href={`${basePath}/app/synthetics/monitor/${configId}`}
>
{children}
</EuiLink>
);
};

View file

@ -0,0 +1,104 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const OK = i18n.translate('xpack.synthetics.certs.ok', {
defaultMessage: 'OK',
});
export const EXPIRED = i18n.translate('xpack.synthetics.certs.expired', {
defaultMessage: 'Expired',
});
export const EXPIRES_SOON = i18n.translate('xpack.synthetics.certs.expireSoon', {
defaultMessage: 'Expires soon',
});
export const EXPIRES = i18n.translate('xpack.synthetics.certs.expires', {
defaultMessage: 'Expires',
});
export const SEARCH_CERTS = i18n.translate('xpack.synthetics.certs.searchCerts', {
defaultMessage: 'Search certificates',
});
export const STATUS_COL = i18n.translate('xpack.synthetics.certs.list.status', {
defaultMessage: 'Status',
});
export const TOO_OLD = i18n.translate('xpack.synthetics.certs.list.status.old', {
defaultMessage: 'Too old',
});
export const COMMON_NAME_COL = i18n.translate('xpack.synthetics.certs.list.commonName', {
defaultMessage: 'Common name',
});
export const MONITORS_COL = i18n.translate('xpack.synthetics.certs.list.monitors', {
defaultMessage: 'Monitors',
});
export const ISSUED_BY_COL = i18n.translate('xpack.synthetics.certs.list.issuedBy', {
defaultMessage: 'Issued by',
});
export const VALID_UNTIL_COL = i18n.translate('xpack.synthetics.certs.list.validUntil', {
defaultMessage: 'Valid until',
});
export const AGE_COL = i18n.translate('xpack.synthetics.certs.list.ageCol', {
defaultMessage: 'Age',
});
export const DAYS = i18n.translate('xpack.synthetics.certs.list.days', {
defaultMessage: 'days',
});
export const FINGERPRINTS_COL = i18n.translate('xpack.synthetics.certs.list.expirationDate', {
defaultMessage: 'Fingerprints',
});
export const COPY_FINGERPRINT = i18n.translate('xpack.synthetics.certs.list.copyFingerprint', {
defaultMessage: 'Click to copy fingerprint value',
});
export const NO_CERTS_AVAILABLE = i18n.translate('xpack.synthetics.certs.list.noCerts', {
defaultMessage: 'No Certificates found.',
});
export const LOADING_CERTIFICATES = i18n.translate('xpack.synthetics.certificates.loading', {
defaultMessage: 'Loading certificates ...',
});
export const REFRESH_CERT = i18n.translate('xpack.synthetics.certificates.refresh', {
defaultMessage: 'Refresh',
});
export const settings = {
breadcrumbText: i18n.translate('xpack.synthetics.settingsBreadcrumbText', {
defaultMessage: 'Settings',
}),
editNoticeTitle: i18n.translate('xpack.synthetics.settings.cannotEditTitle', {
defaultMessage: 'You do not have permission to edit settings.',
}),
editNoticeText: i18n.translate('xpack.synthetics.settings.cannotEditText', {
defaultMessage:
"Your user currently has 'Read' permissions for the Uptime app. Enable a permissions-level of 'All' to edit these settings.",
}),
mustBeNumber: i18n.translate('xpack.synthetics.settings.blankNumberField.error', {
defaultMessage: 'Must be a number.',
}),
};
export const BLANK_STR = i18n.translate('xpack.synthetics.settings.blank.error', {
defaultMessage: 'May not be blank.',
});
export const SPACE_STR = i18n.translate('xpack.synthetics.settings.noSpace.error', {
defaultMessage: 'Index names must not contain space',
});

View file

@ -0,0 +1,57 @@
/*
* 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 { useContext } from 'react';
import { createEsParams, useEsSearch } from '@kbn/observability-shared-plugin/public';
import { SYNTHETICS_INDEX_PATTERN } from '../../../../../common/constants';
import {
DEFAULT_DIRECTION,
DEFAULT_FROM,
DEFAULT_SIZE,
DEFAULT_SORT,
DEFAULT_TO,
getCertsRequestBody,
processCertsResult,
} from '../../../../../common/requests/get_certs_request_body';
import { CertResult, GetCertsParams, Ping } from '../../../../../common/runtime_types';
import { SyntheticsRefreshContext } from '../../contexts';
export const useCertSearch = ({
pageIndex,
size = DEFAULT_SIZE,
search,
sortBy = DEFAULT_SORT,
direction = DEFAULT_DIRECTION,
}: GetCertsParams): CertResult & { loading?: boolean } => {
const { lastRefresh } = useContext(SyntheticsRefreshContext);
const searchBody = getCertsRequestBody({
pageIndex,
size,
search,
sortBy,
direction,
to: DEFAULT_TO,
from: DEFAULT_FROM,
});
const esParams = createEsParams({
index: SYNTHETICS_INDEX_PATTERN,
body: searchBody,
});
const { data: result, loading } = useEsSearch<Ping, typeof esParams>(
esParams,
[size, pageIndex, lastRefresh, search, sortBy, direction],
{
name: 'getTLSCertificates',
}
);
return result ? { ...processCertsResult(result), loading } : { certs: [], total: 0, loading };
};

View file

@ -0,0 +1,39 @@
/*
* 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 moment from 'moment';
import { CERT_STATUS, DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../common/constants';
export const useCertStatus = (expiryDate?: string, issueDate?: string) => {
const expiryThreshold = DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold;
const ageThreshold = DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold;
const certValidityDate = new Date(expiryDate ?? '');
const isValidDate = !isNaN(certValidityDate.valueOf());
if (!isValidDate) {
return false;
}
const isExpiringSoon = moment(certValidityDate).diff(moment(), 'days') < expiryThreshold!;
const isTooOld = moment().diff(moment(issueDate), 'days') > ageThreshold!;
const isExpired = moment(certValidityDate) < moment();
if (isExpired) {
return CERT_STATUS.EXPIRED;
}
return isExpiringSoon
? CERT_STATUS.EXPIRING_SOON
: isTooOld
? CERT_STATUS.TOO_OLD
: CERT_STATUS.OK;
};

View file

@ -18,6 +18,8 @@ import { APP_WRAPPER_CLASS } from '@kbn/core/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public';
import { useInspectorContext } from '@kbn/observability-shared-plugin/public';
import { CertificateTitle } from './components/certificates/certificate_title';
import { CertRefreshBtn } from './components/certificates/cert_refresh_btn';
import { useSyntheticsPrivileges } from './hooks/use_synthetics_priviliges';
import { ClientPluginsStart } from '../../plugin';
import { getMonitorsRoute } from './components/monitors_page/route_config';
@ -36,6 +38,7 @@ import {
MonitorTypePortalNode,
} from './components/monitor_add_edit/portals';
import {
CERTIFICATES_ROUTE,
GETTING_STARTED_ROUTE,
MONITOR_ADD_ROUTE,
MONITOR_EDIT_ROUTE,
@ -44,6 +47,7 @@ import {
import { PLUGIN } from '../../../common/constants/plugin';
import { apiService } from '../../utils/api_service';
import { getErrorDetailsRouteConfig } from './components/error_details/route_config';
import { CertificatesPage } from './components/certificates/certificates';
export type RouteProps = LazyObservabilityPageTemplateProps & {
path: string;
@ -165,6 +169,19 @@ const getRoutes = (
),
},
},
{
title: i18n.translate('xpack.synthetics.certificatesRoute.title', {
defaultMessage: `Certificates | {baseTitle}`,
values: { baseTitle },
}),
path: CERTIFICATES_ROUTE,
component: CertificatesPage,
dataTestSubj: 'uptimeCertificatesPage',
pageHeader: {
pageTitle: <CertificateTitle />,
rightSideItems: [<CertRefreshBtn />],
},
},
];
};

View file

@ -0,0 +1,31 @@
/*
* 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 { Action, createAction, handleActions } from 'redux-actions';
import { AppState } from '..';
export const setCertificatesTotalAction = createAction<CertificatesState>('SET_CERTIFICATES_TOTAL');
export interface CertificatesState {
total: number;
}
const initialState = {
total: 0,
};
export const certificatesReducer = handleActions<CertificatesState>(
{
[String(setCertificatesTotalAction)]: (state, action: Action<CertificatesState>) => ({
...state,
total: action.payload.total,
}),
},
initialState
);
export const certificatesSelector = ({ certificates }: AppState) => certificates.total;

View file

@ -7,6 +7,7 @@
import { combineReducers } from '@reduxjs/toolkit';
import { certificatesReducer, CertificatesState } from './certificates/certificates';
import { globalParamsReducer, GlobalParamsState } from './global_params';
import { overviewStatusReducer, OverviewStatusStateReducer } from './overview_status';
import { browserJourneyReducer } from './browser_journey';
@ -37,6 +38,7 @@ export interface SyntheticsAppState {
elasticsearch: QueriesState;
monitorList: MonitorListState;
overview: MonitorOverviewState;
certificates: CertificatesState;
globalParams: GlobalParamsState;
networkEvents: NetworkEventsState;
agentPolicies: AgentPoliciesState;
@ -68,4 +70,5 @@ export const rootReducer = combineReducers<SyntheticsAppState>({
dynamicSettings: dynamicSettingsReducer,
serviceLocations: serviceLocationsReducer,
syntheticsEnablement: syntheticsEnablementReducer,
certificates: certificatesReducer,
});

View file

@ -153,6 +153,9 @@ export const mockState: SyntheticsAppState = {
addError: null,
editError: null,
},
certificates: {
total: 0,
},
};
function getBrowserJourneyMockSlice() {

View file

@ -21,7 +21,7 @@ export const CertMonitors: React.FC<Props> = ({ monitors }) => {
<span key={mon.id}>
{ind > 0 && ', '}
<EuiToolTip content={mon.url}>
<MonitorPageLink monitorId={mon.id!} configId={mon.configId} linkParameters={''}>
<MonitorPageLink monitorId={mon.id!} linkParameters={''}>
{mon.name || mon.id}
</MonitorPageLink>
</EuiToolTip>

View file

@ -6,8 +6,6 @@
*/
import React, { FC } from 'react';
import { EuiLink } from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { ReactRouterEuiLink } from './react_router_helpers';
interface DetailPageLinkProps {
@ -15,7 +13,6 @@ interface DetailPageLinkProps {
* MonitorId to be used to redirect to detail page
*/
monitorId: string;
configId?: string;
/**
* Link parameters usually filter states
*/
@ -25,21 +22,8 @@ interface DetailPageLinkProps {
export const MonitorPageLink: FC<DetailPageLinkProps> = ({
children,
monitorId,
configId,
linkParameters,
}) => {
const basePath = useKibana().services.http?.basePath.get();
if (configId) {
return (
<EuiLink
data-test-subj="syntheticsMonitorPageLinkLink"
href={`${basePath}/app/synthetics/monitor/${configId}`}
>
{children}
</EuiLink>
);
}
const getLocationTo = () => {
// encode monitorId param as 64 base string to make it a valid URL, since it can be a url
return linkParameters

View file

@ -335,6 +335,14 @@ function registerSyntheticsRoutesWithNavigation(
ignoreTrailingSlash: true,
isNewFeature: true,
},
{
label: i18n.translate('xpack.synthetics.certificatesPage.heading', {
defaultMessage: 'TLS Certificates',
}),
app: 'synthetics',
path: '/certificates',
matchFullPath: true,
},
],
},
];