Add reports list
Signed-off-by: Tomas Dabasinskas <tomas@dabasinskas.net>
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { PuppetDbApi, PuppetDbNode } from './types';
|
||||
import { PuppetDbApi, PuppetDbNode, PuppetDbReport } from './types';
|
||||
import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
|
||||
@@ -38,7 +38,7 @@ export class PuppetDbClient implements PuppetDbApi {
|
||||
): Promise<T | undefined> {
|
||||
const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/puppetdb`;
|
||||
const response = await this.fetchApi.fetch(
|
||||
`${apiUrl}/${path}?${new URLSearchParams(query).toString()}`,
|
||||
`${apiUrl}${path}?${new URLSearchParams(query).toString()}`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -63,4 +63,36 @@ export class PuppetDbClient implements PuppetDbApi {
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
async getPuppetDbReport(
|
||||
puppetDbReportHash: string,
|
||||
): Promise<PuppetDbReport | undefined> {
|
||||
if (!puppetDbReportHash) {
|
||||
throw new Error('PuppetDB report hash is required');
|
||||
}
|
||||
|
||||
const reports = (await this.callApi(`/pdb/query/v4/reports`, {
|
||||
query: `["=","hash","${puppetDbReportHash}"]`,
|
||||
})) as PuppetDbReport[];
|
||||
|
||||
if (!reports || reports.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return reports[0];
|
||||
}
|
||||
|
||||
async getPuppetDbNodeReports(
|
||||
puppetDbCertName: string,
|
||||
): Promise<PuppetDbReport[] | undefined> {
|
||||
if (!puppetDbCertName) {
|
||||
throw new Error('PuppetDB certname is required');
|
||||
}
|
||||
|
||||
return this.callApi(`/pdb/query/v4/reports`, {
|
||||
query: `["=","certname","${puppetDbCertName}"]`,
|
||||
order_by: `[{"field": "start_time", "order": "desc"},{"field": "end_time", "order": "desc"}]`,
|
||||
limit: 30,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export type { PuppetDbNode } from './types';
|
||||
export type { PuppetDbNode, PuppetDbReport } from './types';
|
||||
export { puppetDbApiRef } from './types';
|
||||
export { PuppetDbClient } from './PuppetDbClient';
|
||||
|
||||
@@ -85,10 +85,157 @@ export type PuppetDbNode = {
|
||||
expired?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Report from a PuppetDB.
|
||||
*/
|
||||
export type PuppetDbReport = {
|
||||
/**
|
||||
* The name of the node that the report was received from.
|
||||
*/
|
||||
certname: string;
|
||||
/**
|
||||
* The ID of the report.
|
||||
*/
|
||||
hash: string;
|
||||
/**
|
||||
* The environment assigned to the node that submitted the report.
|
||||
*/
|
||||
environment: string;
|
||||
/**
|
||||
* The status associated to report's node.
|
||||
*/
|
||||
status: string;
|
||||
/**
|
||||
* The job id associated with the report.
|
||||
*/
|
||||
job_id?: string;
|
||||
/**
|
||||
* A flag indicating whether the report was produced by a noop run.
|
||||
*/
|
||||
noop: boolean;
|
||||
/**
|
||||
* A flag indicating whether the report contains noop events.
|
||||
*/
|
||||
noop_pending?: boolean;
|
||||
/**
|
||||
* The version of Puppet that generated the report.
|
||||
*/
|
||||
puppet_version: string;
|
||||
/**
|
||||
* The version number of the report format that Puppet used to generate the original report data.
|
||||
*/
|
||||
report_format: number;
|
||||
/**
|
||||
* An identifier string that Puppet uses to match a specific catalog for a node to a specific Puppet run.
|
||||
*/
|
||||
configuration_version: string;
|
||||
/**
|
||||
* The time on the agent at which the Puppet run began.
|
||||
*/
|
||||
start_time: string;
|
||||
/**
|
||||
* The time on the agent at which the Puppet run ended.
|
||||
*/
|
||||
end_time: string;
|
||||
/**
|
||||
* The time of catalog submission from the Puppet Server to PuppetDB.
|
||||
*/
|
||||
producer_timestamp: string;
|
||||
/**
|
||||
* The time at which PuppetDB received the report.
|
||||
*/
|
||||
receive_time: string;
|
||||
/**
|
||||
* The certname of the Puppet Server that sent the report to PuppetDB.
|
||||
*/
|
||||
producer: string;
|
||||
/**
|
||||
* A string used to identify a Puppet run.
|
||||
*/
|
||||
transaction_uuid: string;
|
||||
/**
|
||||
* A string used to tie a catalog to a report to the catalog used from that Puppet run.
|
||||
*/
|
||||
catalog_uuid: string;
|
||||
/**
|
||||
* A string used to tie a catalog to the Puppet code which generated the catalog.
|
||||
*/
|
||||
code_id: string;
|
||||
/**
|
||||
* A string used to identify whether the Puppet run used a cached catalogs.
|
||||
*/
|
||||
cached_catalog_status: string;
|
||||
/**
|
||||
* Either "agent", "plan", or "any" to restrict the results to reports submitted from that source.
|
||||
*/
|
||||
type: string;
|
||||
/**
|
||||
* A flag indicating whether any of the report's events remediated configuration drift.
|
||||
*/
|
||||
corrective_change: boolean;
|
||||
/**
|
||||
* Report metrics.
|
||||
*/
|
||||
metrics: {
|
||||
/**
|
||||
* Metrics data.
|
||||
*/
|
||||
data: PuppetDbReportMetric[];
|
||||
/**
|
||||
* Link to the metrics endpoint.
|
||||
*/
|
||||
href: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A metric from a PuppetDB report.
|
||||
*/
|
||||
export type PuppetDbReportMetric = {
|
||||
/**
|
||||
* The name of the metric.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The value of the metric.
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* The category of the metric.
|
||||
*/
|
||||
category: string;
|
||||
};
|
||||
|
||||
export const puppetDbApiRef = createApiRef<PuppetDbApi>({
|
||||
id: 'plugin.puppetdb.service',
|
||||
});
|
||||
|
||||
/**
|
||||
* The API provided by the PuppetDB plugin.
|
||||
*/
|
||||
export type PuppetDbApi = {
|
||||
/**
|
||||
*
|
||||
* @param puppetDbCertName - The name of the PuppetDB node.
|
||||
* @returns A PuppetDB node.
|
||||
*/
|
||||
getPuppetDbNode(puppetDbCertName: string): Promise<PuppetDbNode | undefined>;
|
||||
/**
|
||||
* Get a list of PuppetDB reports for the specified node.
|
||||
*
|
||||
* @param puppetDbCertName - The name of the node that the report was received from.
|
||||
* @returns A list of PuppetDB reports for the specified node.
|
||||
*/
|
||||
getPuppetDbNodeReports(
|
||||
puppetDbCertName: string,
|
||||
): Promise<PuppetDbReport[] | undefined>;
|
||||
/**
|
||||
* Get a specific PuppetDB report.
|
||||
*
|
||||
* @param puppetDbReportHash - The ID of the report.
|
||||
* @returns A specific PuppetDB report.
|
||||
*/
|
||||
getPuppetDbReport(
|
||||
puppetDbReportHash: string,
|
||||
): Promise<PuppetDbReport | undefined>;
|
||||
};
|
||||
|
||||
+5
-8
@@ -18,18 +18,18 @@ import useAsync from 'react-use/lib/useAsync';
|
||||
import { Progress, ResponseErrorPanel } from '@backstage/core-components';
|
||||
import { InfoCard } from '@backstage/core-components';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { puppetDbApiRef } from '../../api';
|
||||
import { puppetDbApiRef } from '../../../api';
|
||||
|
||||
type PuppetDbNodeCardProps = {
|
||||
certName: string;
|
||||
};
|
||||
|
||||
export const PuppetDbNodeCard = (props: PuppetDbNodeCardProps) => {
|
||||
export const NodeCard = (props: PuppetDbNodeCardProps) => {
|
||||
const { certName } = props;
|
||||
const puppetDbApi = useApi(puppetDbApiRef);
|
||||
|
||||
const { value, loading, error } = useAsync(async () => {
|
||||
return puppetDbApi.getPuppetDbNode(certName);
|
||||
return puppetDbApi.getPuppetDbReport(certName);
|
||||
}, [puppetDbApi, certName]);
|
||||
|
||||
if (loading) {
|
||||
@@ -39,11 +39,8 @@ export const PuppetDbNodeCard = (props: PuppetDbNodeCardProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<InfoCard
|
||||
title="PuppetDB Node"
|
||||
subheader={`Details about PuppetDB node ${certName}`}
|
||||
>
|
||||
<text>${JSON.stringify(value)}</text>
|
||||
<InfoCard title={`Puppet node: ${certName}`}>
|
||||
<pre>{JSON.stringify(value, null, 2)}</pre>
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -13,4 +13,4 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { PuppetDbNodeCard } from './PuppetDbNodeCard';
|
||||
export { NodeCard } from './NodeCard/NodeCard';
|
||||
@@ -22,7 +22,8 @@ import {
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { isPuppetDbAvailable } from '../../plugin';
|
||||
import { ANNOTATION_PUPPET_CERTNAME } from '../../constants';
|
||||
import { PuppetDbNodeCard } from '../PuppetDbNodeCard';
|
||||
import { ReportsTable } from '../Reports/ReportsTable/ReportsTable';
|
||||
import { NodeCard } from '../Node';
|
||||
|
||||
export const PuppetDbTab = () => {
|
||||
const { entity } = useEntity();
|
||||
@@ -34,12 +35,13 @@ export const PuppetDbTab = () => {
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
// TODO(tdabasinskas): Remove the static value after the testing.
|
||||
const certName = entity.metadata.annotations[ANNOTATION_PUPPET_CERTNAME];
|
||||
|
||||
return (
|
||||
<Page themeId="tool">
|
||||
<Content>
|
||||
<PuppetDbNodeCard certName={certName} />
|
||||
<ReportsTable certName={certName} />
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { puppetDbApiRef, PuppetDbReport } from '../../../api';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import React from 'react';
|
||||
import {
|
||||
Link,
|
||||
ResponseErrorPanel,
|
||||
Table,
|
||||
StatusPending,
|
||||
StatusRunning,
|
||||
StatusOK,
|
||||
StatusAborted,
|
||||
StatusError,
|
||||
TableColumn,
|
||||
} from '@backstage/core-components';
|
||||
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { buildRouteRef } from '../../../routes';
|
||||
|
||||
type ReportsTableProps = {
|
||||
certName: string;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
empty: {
|
||||
padding: theme.spacing(2),
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
export const ReportsTable = (props: ReportsTableProps) => {
|
||||
const { certName } = props;
|
||||
const puppetDbApi = useApi(puppetDbApiRef);
|
||||
const routeLink = useRouteRef(buildRouteRef);
|
||||
const classes = useStyles();
|
||||
|
||||
const { value, loading, error } = useAsync(async () => {
|
||||
return puppetDbApi.getPuppetDbNodeReports(certName);
|
||||
}, [puppetDbApi, certName]);
|
||||
|
||||
if (error) {
|
||||
return <ResponseErrorPanel error={error} />;
|
||||
}
|
||||
|
||||
const columns: TableColumn<PuppetDbReport>[] = [
|
||||
{
|
||||
title: 'Configuration Version',
|
||||
field: 'configuration_version',
|
||||
render: rowData => (
|
||||
<Link component={RouterLink} to={routeLink({ hash: rowData.hash! })}>
|
||||
<Typography noWrap>{rowData.configuration_version}</Typography>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Start Time',
|
||||
field: 'start_time',
|
||||
align: 'center',
|
||||
render: rowData => (
|
||||
<Typography>
|
||||
{new Date(Date.parse(rowData.start_time)).toLocaleString()}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'End Time',
|
||||
field: 'end_time',
|
||||
align: 'center',
|
||||
render: rowData => (
|
||||
<Typography>
|
||||
{new Date(Date.parse(rowData.end_time)).toLocaleString()}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Run Duration',
|
||||
align: 'center',
|
||||
render: rowData => {
|
||||
const start_date = new Date(Date.parse(rowData.start_time));
|
||||
const end_date = new Date(Date.parse(rowData.end_time));
|
||||
const duration = new Date(end_date.getTime() - start_date.getTime());
|
||||
return (
|
||||
<Typography noWrap>
|
||||
{duration.getUTCHours().toString().padStart(2, '0')}:
|
||||
{duration.getUTCMinutes().toString().padStart(2, '0')}:
|
||||
{duration.getUTCSeconds().toString().padStart(2, '0')}.
|
||||
{duration.getUTCMilliseconds().toString().padStart(4, '0')}
|
||||
</Typography>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Environment',
|
||||
field: 'environment',
|
||||
},
|
||||
{
|
||||
title: 'Mode',
|
||||
field: 'noop',
|
||||
align: 'center',
|
||||
render: rowData =>
|
||||
rowData.noop ? (
|
||||
<Typography>NOOP</Typography>
|
||||
) : (
|
||||
<Typography>NO-NOOP</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
field: 'status',
|
||||
align: 'center',
|
||||
render: rowData => {
|
||||
if (rowData.noop) {
|
||||
return (
|
||||
<>
|
||||
<StatusAborted />
|
||||
{rowData.status.charAt(0).toLocaleUpperCase() +
|
||||
rowData.status.slice(1)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
switch (rowData.status) {
|
||||
case 'failed':
|
||||
return (
|
||||
<>
|
||||
<StatusError />
|
||||
{rowData.status.charAt(0).toLocaleUpperCase() +
|
||||
rowData.status.slice(1)}
|
||||
</>
|
||||
);
|
||||
case 'changed':
|
||||
return (
|
||||
<>
|
||||
<StatusRunning />
|
||||
{rowData.status.charAt(0).toLocaleUpperCase() +
|
||||
rowData.status.slice(1)}
|
||||
</>
|
||||
);
|
||||
case 'unchanged':
|
||||
return (
|
||||
<>
|
||||
<StatusPending />
|
||||
{rowData.status.charAt(0).toLocaleUpperCase() +
|
||||
rowData.status.slice(1)}
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<StatusOK />
|
||||
{rowData.status.charAt(0).toLocaleUpperCase() +
|
||||
rowData.status.slice(1)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
options={{
|
||||
sorting: true,
|
||||
actionsColumnIndex: -1,
|
||||
loadingType: 'linear',
|
||||
padding: 'dense',
|
||||
showEmptyDataSourceMessage: !loading,
|
||||
showTitle: true,
|
||||
toolbar: true,
|
||||
pageSize: 10,
|
||||
pageSizeOptions: [10],
|
||||
}}
|
||||
emptyContent={
|
||||
<Typography color="textSecondary" className={classes.empty}>
|
||||
No reports
|
||||
</Typography>
|
||||
}
|
||||
title={`Latest PuppetDB reports from node ${certName}`}
|
||||
columns={columns}
|
||||
data={value || []}
|
||||
isLoading={loading}
|
||||
/>
|
||||
);
|
||||
|
||||
/*
|
||||
return (
|
||||
|
||||
|
||||
|
||||
<TableContainer className={classes.table}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Start Time</TableCell>
|
||||
<TableCell>End Time</TableCell>
|
||||
<TableCell>Duration</TableCell>
|
||||
<TableCell>Environment</TableCell>
|
||||
<TableCell>Configuration Version</TableCell>
|
||||
<TableCell>Puppet Version</TableCell>
|
||||
<TableCell>NoOp</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{value?.map((report) => {
|
||||
const start_date = new Date(Date.parse(report.start_time));
|
||||
const end_date = new Date(Date.parse(report.end_time));
|
||||
const duration = new Date(end_date.getTime() - start_date.getTime());
|
||||
|
||||
return (<TableRow>
|
||||
<TableCell>{start_date.toLocaleString()}</TableCell>
|
||||
<TableCell>{end_date.toLocaleString()}</TableCell>
|
||||
<TableCell>
|
||||
{duration.getUTCHours().toString().padStart(2, '0')}:
|
||||
{duration.getUTCMinutes().toString().padStart(2, '0')}:
|
||||
{duration.getUTCSeconds().toString().padStart(2, '0')}.
|
||||
{duration.getUTCMilliseconds().toString().padStart(4, '0')}
|
||||
</TableCell>
|
||||
<TableCell>{report.environment}</TableCell>
|
||||
<TableCell>{report.configuration_version}</TableCell>
|
||||
<TableCell>{report.puppet_version}</TableCell>
|
||||
<TableCell>{report.noop ? 'Yes' : 'No'}</TableCell>
|
||||
<TableCell>{report.status}</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);*/
|
||||
};
|
||||
@@ -13,8 +13,14 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createRouteRef } from '@backstage/core-plugin-api';
|
||||
import { createRouteRef, createSubRouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
id: 'puppetdb',
|
||||
});
|
||||
|
||||
export const buildRouteRef = createSubRouteRef({
|
||||
id: 'puppetdb/report',
|
||||
path: '/:hash',
|
||||
parent: rootRouteRef,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user