diff --git a/plugins/xcmetrics/src/components/BuildTrendComponent/BuildTrendComponent.test.tsx b/plugins/xcmetrics/src/components/BuildTrendComponent/BuildTrendComponent.test.tsx new file mode 100644 index 0000000000..2e96a0fb4e --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildTrendComponent/BuildTrendComponent.test.tsx @@ -0,0 +1,31 @@ +/* + * Copyright 2021 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 React from 'react'; +import { BuildTrendComponent } from './BuildTrendComponent'; +import { renderInTestApp } from '@backstage/test-utils'; +import { BuildCount } from '../../api'; + +describe('BuildTrendComponent', () => { + it('should render', async () => { + const buildCounts: BuildCount[] = [ + { day: '2021-01-01', errors: 10, builds: 100 }, + ]; + const rendered = await renderInTestApp( + , + ); + expect(rendered.findAllByText('Build Count')).toBeTruthy(); + }); +}); diff --git a/plugins/xcmetrics/src/components/BuildTrendComponent/BuildTrendComponent.tsx b/plugins/xcmetrics/src/components/BuildTrendComponent/BuildTrendComponent.tsx new file mode 100644 index 0000000000..7403ec1c43 --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildTrendComponent/BuildTrendComponent.tsx @@ -0,0 +1,48 @@ +/* + * Copyright 2021 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 React from 'react'; +import { TrendLine } from '@backstage/core-components'; +import { BuildCount } from '../../api'; +import { Typography, useTheme } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; + +const TRENDLINE_TITLE = 'Build Count'; + +interface BuildTrendProps { + buildCounts: BuildCount[]; +} + +export const BuildTrendComponent = ({ buildCounts }: BuildTrendProps) => { + const theme = useTheme(); + + let max = 0; + const builds = buildCounts.map(counts => { + max = Math.max(max, counts.builds); + return counts.builds; + }); + + return ( + <> + {TRENDLINE_TITLE} + + + ); +}; diff --git a/plugins/xcmetrics/src/components/BuildTrendComponent/index.ts b/plugins/xcmetrics/src/components/BuildTrendComponent/index.ts new file mode 100644 index 0000000000..4f4ef09aca --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildTrendComponent/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 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. + */ +export * from './BuildTrendComponent'; diff --git a/plugins/xcmetrics/src/components/DataValueComponent/DataValueComponent.test.tsx b/plugins/xcmetrics/src/components/DataValueComponent/DataValueComponent.test.tsx new file mode 100644 index 0000000000..3681b7cf20 --- /dev/null +++ b/plugins/xcmetrics/src/components/DataValueComponent/DataValueComponent.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2021 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 React from 'react'; +import { DataValueComponent, DataValueGridItem } from './DataValueComponent'; +import { renderInTestApp } from '@backstage/test-utils'; + +describe('DataValueComponent', () => { + it('should render', async () => { + const field = 'Field'; + const value = 'Value'; + const rendered = await renderInTestApp( + , + ); + expect(rendered.getByText(field)).toBeInTheDocument(); + expect(rendered.getByText(value)).toBeInTheDocument(); + }); + + it('grid item should render', async () => { + const field = 'Field'; + const value = 'Value'; + const rendered = await renderInTestApp( + , + ); + expect(rendered.getByText(field)).toBeInTheDocument(); + expect(rendered.getByText(value)).toBeInTheDocument(); + }); +}); diff --git a/plugins/xcmetrics/src/components/DataValueComponent/DataValueComponent.tsx b/plugins/xcmetrics/src/components/DataValueComponent/DataValueComponent.tsx new file mode 100644 index 0000000000..52e80d3291 --- /dev/null +++ b/plugins/xcmetrics/src/components/DataValueComponent/DataValueComponent.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2021 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 { Grid, GridSize, Typography } from '@material-ui/core'; +import React from 'react'; + +interface DataValueProps { + field: string; + value: string | number | undefined; +} + +export const DataValueComponent = ({ field, value }: DataValueProps) => { + return ( +
+ {field} + {value} +
+ ); +}; + +interface GridProps { + xs?: GridSize; + md?: GridSize; +} + +export const DataValueGridItem = (props: DataValueProps & GridProps) => ( + + + +); diff --git a/plugins/xcmetrics/src/components/DataValueComponent/index.ts b/plugins/xcmetrics/src/components/DataValueComponent/index.ts new file mode 100644 index 0000000000..cbf0ffe851 --- /dev/null +++ b/plugins/xcmetrics/src/components/DataValueComponent/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 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. + */ +export * from './DataValueComponent'; diff --git a/plugins/xcmetrics/src/components/ErrorTrendComponent/ErrorTrendComponent.test.tsx b/plugins/xcmetrics/src/components/ErrorTrendComponent/ErrorTrendComponent.test.tsx new file mode 100644 index 0000000000..67b2b0ce90 --- /dev/null +++ b/plugins/xcmetrics/src/components/ErrorTrendComponent/ErrorTrendComponent.test.tsx @@ -0,0 +1,31 @@ +/* + * Copyright 2021 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 React from 'react'; +import { ErrorTrendComponent } from './ErrorTrendComponent'; +import { renderInTestApp } from '@backstage/test-utils'; +import { BuildCount } from '../../api'; + +describe('ErrorTrendComponent', () => { + it('should render', async () => { + const buildCounts: BuildCount[] = [ + { day: '2021-01-01', errors: 10, builds: 100 }, + ]; + const rendered = await renderInTestApp( + , + ); + expect(rendered.findAllByText('Error Rate')).toBeTruthy(); + }); +}); diff --git a/plugins/xcmetrics/src/components/ErrorTrendComponent/ErrorTrendComponent.tsx b/plugins/xcmetrics/src/components/ErrorTrendComponent/ErrorTrendComponent.tsx index b8f358a903..d6bf789f5b 100644 --- a/plugins/xcmetrics/src/components/ErrorTrendComponent/ErrorTrendComponent.tsx +++ b/plugins/xcmetrics/src/components/ErrorTrendComponent/ErrorTrendComponent.tsx @@ -14,32 +14,19 @@ * limitations under the License. */ import React from 'react'; -import { Progress, TrendLine } from '@backstage/core-components'; -import { useApi } from '@backstage/core-plugin-api'; -import { BuildCount, xcmetricsApiRef } from '../../api'; -import { useAsync } from 'react-use'; -import { Alert } from '@material-ui/lab'; +import { TrendLine } from '@backstage/core-components'; +import { BuildCount } from '../../api'; +import { Typography, useTheme } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; const TRENDLINE_TITLE = 'Error Rate'; interface ErrorTrendProps { - days: number; + buildCounts: BuildCount[]; } -export const ErrorTrendComponent = ({ days }: ErrorTrendProps) => { - const client = useApi(xcmetricsApiRef); - const { value: buildCounts, loading, error } = useAsync( - async (): Promise => client.getBuildCounts(days), - [], - ); - - if (loading) { - return ; - } else if (error) { - return {error.message}; - } else if (!buildCounts) { - return ; - } +export const ErrorTrendComponent = ({ buildCounts }: ErrorTrendProps) => { + const theme = useTheme(); let max = 0; const averageErrors = buildCounts.map(counts => { @@ -49,5 +36,15 @@ export const ErrorTrendComponent = ({ days }: ErrorTrendProps) => { return dayAverage; }); - return ; + return ( + <> + {TRENDLINE_TITLE} + + + ); }; diff --git a/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.test.tsx b/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.test.tsx index 545e9e8c1d..e5689869c1 100644 --- a/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.test.tsx +++ b/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.test.tsx @@ -14,47 +14,59 @@ * limitations under the License. */ import React from 'react'; -import { OverviewComponent } from './OverviewComponent'; import { renderInTestApp } from '@backstage/test-utils'; import { xcmetricsApiRef } from '../../api'; import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; -import { mockUserId, mockXcmetricsApi } from '../../test-utils'; +import { mockUserId, createMockXcmetricsApi } from '../../test-utils'; +import { OverviewComponent } from './OverviewComponent'; + +jest.mock('../OverviewTrendsComponent', () => ({ + OverviewTrendsComponent: () => 'OverviewTrendsComponent', +})); + +jest.mock('../StatusMatrixComponent', () => ({ + StatusMatrixComponent: () => 'StatusMatrixComponent', +})); describe('OverviewComponent', () => { it('should render', async () => { const rendered = await renderInTestApp( - + , ); + expect(rendered.getByText('XCMetrics Dashboard')).toBeInTheDocument(); expect(rendered.getByText(mockUserId)).toBeInTheDocument(); - expect(rendered.queryByText('CI')).toBeNull(); }); it('should render an empty state when no builds exist', async () => { - mockXcmetricsApi.getBuilds = jest.fn().mockResolvedValue([]); + const api = createMockXcmetricsApi(); + api.getBuilds = jest.fn().mockResolvedValue([]); const rendered = await renderInTestApp( - + , ); + expect(rendered.getByText('No builds to show')).toBeInTheDocument(); }); it('should show an error when API not responding', async () => { + const api = createMockXcmetricsApi(); const errorMessage = 'MockErrorMessage'; - mockXcmetricsApi.getBuilds = jest - .fn() - .mockRejectedValue({ message: errorMessage }); + api.getBuilds = jest.fn().mockRejectedValue({ message: errorMessage }); const rendered = await renderInTestApp( - + , ); + expect(rendered.getByText(errorMessage)).toBeInTheDocument(); }); }); diff --git a/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.tsx b/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.tsx index b9bd048ac1..3cb8b69be0 100644 --- a/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.tsx +++ b/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; +import React, { ReactChild } from 'react'; import { ContentHeader, SupportButton, @@ -31,33 +31,21 @@ import { Build, BuildStatus, xcmetricsApiRef } from '../../api'; import { useAsync } from 'react-use'; import { Alert } from '@material-ui/lab'; import { StatusMatrixComponent } from '../StatusMatrixComponent'; -import { formatDuration, formatStatus } from '../../utils'; -import { Chip, Grid, Typography } from '@material-ui/core'; -import { ErrorTrendComponent } from '../ErrorTrendComponent'; +import { formatTime } from '../../utils'; +import { Chip, Grid } from '@material-ui/core'; +import { OverviewTrendsComponent } from '../OverviewTrendsComponent'; -const Status = ({ - status, - warningCount, -}: { - status: BuildStatus; - warningCount: number; -}) => { - const statusIcons = { - succeeded: , - failed: , - stopped: , - }; - - return ( - <> - {statusIcons[status]} {formatStatus(status)} - {warningCount > 0 && ` with ${warningCount} warning`} - {warningCount > 1 && 's'} - - ); +const STATUS_ICONS: { [key in BuildStatus]: ReactChild } = { + succeeded: , + failed: , + stopped: , }; const columns: TableColumn[] = [ + { + field: 'buildStatus', + render: data => STATUS_ICONS[data.buildStatus], + }, { title: 'Project', field: 'projectName', @@ -67,23 +55,15 @@ const columns: TableColumn[] = [ field: 'schema', }, { - title: 'Duration', - field: 'duration', - type: 'time', + title: 'Started', + field: 'startedAt', searchable: false, - render: data => formatDuration(data.duration), + render: data => formatTime(data.startTimestamp), }, { title: 'User', field: 'userid', }, - { - title: 'Status', - field: 'buildStatus', - render: data => ( - - ), - }, { field: 'isCI', render: data => data.isCi && , @@ -121,7 +101,7 @@ export const OverviewComponent = () => { Dashboard for XCMetrics - + { } /> - + - Error Rate - + diff --git a/plugins/xcmetrics/src/components/OverviewTrendsComponent/OverviewTrendsComponent.test.tsx b/plugins/xcmetrics/src/components/OverviewTrendsComponent/OverviewTrendsComponent.test.tsx new file mode 100644 index 0000000000..957602384d --- /dev/null +++ b/plugins/xcmetrics/src/components/OverviewTrendsComponent/OverviewTrendsComponent.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright 2021 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 React from 'react'; +import { OverviewTrendsComponent } from './OverviewTrendsComponent'; +import { renderInTestApp } from '@backstage/test-utils'; +import { xcmetricsApiRef } from '../../api'; +import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { createMockXcmetricsApi } from '../../test-utils'; + +describe('OverviewTrendsComponent', () => { + it('should render', async () => { + const rendered = await renderInTestApp( + + + , + ); + expect(rendered.getByText('Last 14 Days')).toBeInTheDocument(); + }); + + it('should render empty state', async () => { + const api = createMockXcmetricsApi(); + api.getBuildCounts = jest.fn().mockResolvedValue([]); + + const rendered = await renderInTestApp( + + + , + ); + expect(rendered.getByText('No Trends Available')).toBeInTheDocument(); + }); + + it('should show an error when API not responding', async () => { + const api = createMockXcmetricsApi(); + const errorMessage = 'MockErrorMessage'; + + api.getBuildCounts = jest.fn().mockRejectedValue({ message: errorMessage }); + + const rendered = await renderInTestApp( + + + , + ); + expect(rendered.getByText(errorMessage)).toBeInTheDocument(); + }); +}); diff --git a/plugins/xcmetrics/src/components/OverviewTrendsComponent/OverviewTrendsComponent.tsx b/plugins/xcmetrics/src/components/OverviewTrendsComponent/OverviewTrendsComponent.tsx new file mode 100644 index 0000000000..cbc64a902e --- /dev/null +++ b/plugins/xcmetrics/src/components/OverviewTrendsComponent/OverviewTrendsComponent.tsx @@ -0,0 +1,85 @@ +/* + * Copyright 2021 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 { Grid, makeStyles, Typography } from '@material-ui/core'; +import React from 'react'; +import { Progress, TrendLine } from '@backstage/core-components'; +import { ErrorTrendComponent } from '../ErrorTrendComponent'; +import { Alert } from '@material-ui/lab'; +import { BuildCount, xcmetricsApiRef } from '../../api'; +import { useAsync } from 'react-use'; +import { useApi } from '@backstage/core-plugin-api'; +import { BuildTrendComponent } from '../BuildTrendComponent'; +import { DataValueGridItem } from '../DataValueComponent'; +import { formatPercentage } from '../../utils'; + +const useStyles = makeStyles({ + spacingTop: { + marginTop: 8, + }, +}); + +export const OverviewTrendsComponent = ({ days }: { days: number }) => { + const classes = useStyles(); + const client = useApi(xcmetricsApiRef); + const { value: buildCounts, loading, error } = useAsync( + async (): Promise => client.getBuildCounts(days), + [], + ); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } else if (!buildCounts || buildCounts.length === 0) { + return ( + <> + No Trends Available + + + ); + } + + const sumCount = buildCounts.reduce( + (sum, current) => sum + current.builds, + 0, + ); + const sumErrors = buildCounts.reduce( + (sum, current) => sum + current.errors, + 0, + ); + const errorRate = sumCount > 0 ? sumErrors / sumCount : 0; + + return ( + <> + Last {days} Days + + + + + + + + + ); +}; diff --git a/plugins/xcmetrics/src/components/OverviewTrendsComponent/index.ts b/plugins/xcmetrics/src/components/OverviewTrendsComponent/index.ts new file mode 100644 index 0000000000..778a4bf503 --- /dev/null +++ b/plugins/xcmetrics/src/components/OverviewTrendsComponent/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 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. + */ +export * from './OverviewTrendsComponent'; diff --git a/plugins/xcmetrics/src/components/StatusCellComponent/StatusCellComponent.test.tsx b/plugins/xcmetrics/src/components/StatusCellComponent/StatusCellComponent.test.tsx index 470f69fa8d..363486eb51 100644 --- a/plugins/xcmetrics/src/components/StatusCellComponent/StatusCellComponent.test.tsx +++ b/plugins/xcmetrics/src/components/StatusCellComponent/StatusCellComponent.test.tsx @@ -19,13 +19,19 @@ import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import userEvent from '@testing-library/user-event'; import { StatusCellComponent } from './StatusCellComponent'; import { xcmetricsApiRef } from '../../api'; -import { mockBuildId, mockStatus, mockXcmetricsApi } from '../../test-utils'; +import { + mockBuildId, + mockStatus, + createMockXcmetricsApi, +} from '../../test-utils'; import { formatStatus } from '../../utils'; describe('StatusCellComponent', () => { it('should render', async () => { const rendered = await renderInTestApp( - + { it('should render', async () => { const rendered = await renderInTestApp( - + , ); diff --git a/plugins/xcmetrics/src/test-utils/mockXcmetricsApi.ts b/plugins/xcmetrics/src/test-utils/mockXcmetricsApi.ts index 58419dfd48..b73b361926 100644 --- a/plugins/xcmetrics/src/test-utils/mockXcmetricsApi.ts +++ b/plugins/xcmetrics/src/test-utils/mockXcmetricsApi.ts @@ -19,7 +19,7 @@ export const mockUserId = 'user_id'; export const mockBuildId = 'build_id'; export const mockStatus = 'succeeded'; -export const mockXcmetricsApi: jest.Mocked = { +export const createMockXcmetricsApi = (): jest.Mocked => ({ getBuildStatuses: jest .fn() .mockResolvedValue([{ id: mockBuildId, status: mockStatus }]), @@ -40,5 +40,8 @@ export const mockXcmetricsApi: jest.Mocked = { schema: 'AppSchema', }, ]), - getBuildCounts: jest.fn(), -}; + getBuildCounts: jest.fn().mockResolvedValue([ + { day: '2021-07-10', builds: 10, errors: 1 }, + { day: '2021-07-09', builds: 11, errors: 2 }, + ]), +}); diff --git a/plugins/xcmetrics/src/utils/format.ts b/plugins/xcmetrics/src/utils/format.ts index d8c08d9367..20d8cc68db 100644 --- a/plugins/xcmetrics/src/utils/format.ts +++ b/plugins/xcmetrics/src/utils/format.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Duration } from 'luxon'; +import { DateTime, Duration } from 'luxon'; import { BuildStatus } from '../api'; export const formatDuration = (seconds: number) => @@ -21,5 +21,15 @@ export const formatDuration = (seconds: number) => suppressMilliseconds: true, }); +export const formatTime = (timestamp: string) => { + return DateTime.fromISO(timestamp).toLocaleString( + DateTime.DATETIME_SHORT_WITH_SECONDS, + ); +}; + +export const formatPercentage = (number: number) => { + return `${Math.round(number * 100)} %`; +}; + export const formatStatus = (status: BuildStatus) => status[0].toUpperCase() + status.slice(1);