mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Synthetics] TLS Certs page copied from uptime (#159541)
Co-authored-by: Justin Kambic <jk@elastic.co>
This commit is contained in:
parent
ceca2bdbd8
commit
d72524a72d
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,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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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',
|
||||
});
|
|
@ -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 };
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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 />],
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -153,6 +153,9 @@ export const mockState: SyntheticsAppState = {
|
|||
addError: null,
|
||||
editError: null,
|
||||
},
|
||||
certificates: {
|
||||
total: 0,
|
||||
},
|
||||
};
|
||||
|
||||
function getBrowserJourneyMockSlice() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue