Merge pull request #6805 from ngranander/xcmetrics-refactor

Internal refactor of XCMetrics plugin
This commit is contained in:
Fredrik Adelöw
2021-08-13 11:24:36 +02:00
committed by GitHub
55 changed files with 294 additions and 302 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-xcmetrics': patch
---
Internal refactoring
@@ -15,13 +15,13 @@
*/
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { AccordionComponent } from './AccordionComponent';
import { Accordion } from './Accordion';
import userEvent from '@testing-library/user-event';
describe('AccordionComponent', () => {
describe('Accordion', () => {
it('should render', async () => {
const rendered = await renderInTestApp(
<AccordionComponent
<Accordion
id="accordionId"
heading="heading"
secondaryHeading="secondaryHeading"
@@ -34,13 +34,13 @@ describe('AccordionComponent', () => {
it('should show content when clicked', async () => {
const rendered = await renderInTestApp(
<AccordionComponent
<Accordion
id="accordionId"
heading="heading"
secondaryHeading="secondaryHeading"
>
Content
</AccordionComponent>,
</Accordion>,
);
expect(rendered.getByText('Content')).not.toBeVisible();
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import {
Accordion,
AccordionSummary,
Accordion as MuiAccordion,
AccordionSummary as MuiAccordionSummary,
Typography,
AccordionDetails,
makeStyles,
@@ -45,17 +45,15 @@ interface AccordionProps {
unmountOnExit?: boolean;
}
export const AccordionComponent = (
props: PropsWithChildren<AccordionProps>,
) => {
export const Accordion = (props: PropsWithChildren<AccordionProps>) => {
const classes = useStyles();
return (
<Accordion
<MuiAccordion
disabled={props.disabled}
TransitionProps={{ unmountOnExit: props.unmountOnExit ?? false }}
>
<AccordionSummary
<MuiAccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`${props.id}-content`}
id={`${props.id}-header`}
@@ -64,8 +62,8 @@ export const AccordionComponent = (
<Typography className={classes.secondaryHeading}>
{props.secondaryHeading}
</Typography>
</AccordionSummary>
</MuiAccordionSummary>
<AccordionDetails>{props.children}</AccordionDetails>
</Accordion>
</MuiAccordion>
);
};
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './BuildListComponent';
export { Accordion } from './Accordion';
@@ -16,29 +16,29 @@
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
import { BuildDetailsComponent, withRequest } from './BuildDetailsComponent';
import { BuildDetails, withRequest } from './BuildDetails';
import { xcmetricsApiRef } from '../../api';
jest.mock('../../api/XcmetricsClient');
const client = require('../../api/XcmetricsClient');
jest.mock('../AccordionComponent', () => ({
AccordionComponent: ({ heading }: { heading: string }) => (
jest.mock('../Accordion', () => ({
Accordion: ({ heading }: { heading: string }) => (
<div>accordion-{heading}</div>
),
}));
jest.mock('../BuildTimelineComponent', () => ({
BuildTimelineComponent: () => 'BuildTimelineComponent',
jest.mock('../BuildTimeline', () => ({
BuildTimeline: () => 'BuildTimeline',
}));
describe('BuildDetailsComponent', () => {
describe('BuildDetails', () => {
it('should render', async () => {
const rendered = await renderInTestApp(
<ApiProvider
apis={ApiRegistry.with(xcmetricsApiRef, client.XcmetricsClient)}
>
<BuildDetailsComponent buildData={client.mockBuildResponse} />
<BuildDetails buildData={client.mockBuildResponse} />
</ApiProvider>,
);
@@ -56,15 +56,15 @@ describe('BuildDetailsComponent', () => {
});
});
describe('BuildDetailsComponent with request', () => {
const BuildDetails = withRequest(BuildDetailsComponent);
describe('BuildDetails with request', () => {
const BuildDetailsWithRequest = withRequest(BuildDetails);
it('should fetch the build and render', async () => {
const rendered = await renderInTestApp(
<ApiProvider
apis={ApiRegistry.with(xcmetricsApiRef, client.XcmetricsClient)}
>
<BuildDetails buildId={client.mockBuild.id} />
<BuildDetailsWithRequest buildId={client.mockBuild.id} />
</ApiProvider>,
);
@@ -81,7 +81,7 @@ describe('BuildDetailsComponent with request', () => {
<ApiProvider
apis={ApiRegistry.with(xcmetricsApiRef, client.XcmetricsClient)}
>
<BuildDetails buildId={client.mockBuild.id} />
<BuildDetailsWithRequest buildId={client.mockBuild.id} />
</ApiProvider>,
);
@@ -95,7 +95,7 @@ describe('BuildDetailsComponent with request', () => {
<ApiProvider
apis={ApiRegistry.with(xcmetricsApiRef, client.XcmetricsClient)}
>
<BuildDetails buildId={client.mockBuild.id} />
<BuildDetailsWithRequest buildId={client.mockBuild.id} />
</ApiProvider>,
);
@@ -21,11 +21,11 @@ import { Alert } from '@material-ui/lab';
import { useAsync } from 'react-use';
import { useApi } from '@backstage/core-plugin-api';
import { formatDuration, formatStatus, formatTime } from '../../utils';
import { StatusIconComponent as StatusIcon } from '../StatusIconComponent';
import { StatusIcon } from '../StatusIcon';
import { BackstageTheme } from '@backstage/theme';
import { AccordionComponent } from '../AccordionComponent';
import { BuildTimelineComponent } from '../BuildTimelineComponent';
import { PreformattedTextComponent } from '../PreformattedTextComponent';
import { Accordion } from '../Accordion';
import { BuildTimeline } from '../BuildTimeline';
import { PreformattedText } from '../PreformattedText';
const useStyles = makeStyles((theme: BackstageTheme) =>
createStyles({
@@ -41,7 +41,7 @@ interface BuildDetailsProps {
showId?: boolean;
}
export const BuildDetailsComponent = ({
export const BuildDetails = ({
buildData: { build, targets, xcode },
showId,
}: BuildDetailsProps) => {
@@ -92,7 +92,7 @@ export const BuildDetailsComponent = ({
/>
</Grid>
<Grid item xs={8}>
<AccordionComponent
<Accordion
id="buildHost"
heading="Host"
secondaryHeading={build.machineName}
@@ -101,9 +101,9 @@ export const BuildDetailsComponent = ({
{!hostResult.loading && hostResult.value && (
<StructuredMetadataTable metadata={hostResult.value} />
)}
</AccordionComponent>
</Accordion>
<AccordionComponent
<Accordion
id="buildErrors"
heading="Errors"
secondaryHeading={build.errorCount}
@@ -114,7 +114,7 @@ export const BuildDetailsComponent = ({
{!errorsResult.loading &&
errorsResult.value?.map((error, idx) => (
<div key={error.id}>
<PreformattedTextComponent
<PreformattedText
title="Error Details"
text={error.detail}
maxChars={190}
@@ -126,9 +126,9 @@ export const BuildDetailsComponent = ({
</div>
))}
</div>
</AccordionComponent>
</Accordion>
<AccordionComponent
<Accordion
id="buildWarnings"
heading="Warnings"
secondaryHeading={build.warningCount}
@@ -139,7 +139,7 @@ export const BuildDetailsComponent = ({
{!warningsResult.loading &&
warningsResult.value?.map((warning, idx) => (
<div key={warning.id}>
<PreformattedTextComponent
<PreformattedText
title="Warning Details"
text={warning.detail ?? warning.title}
maxChars={190}
@@ -151,9 +151,9 @@ export const BuildDetailsComponent = ({
</div>
))}
</div>
</AccordionComponent>
</Accordion>
<AccordionComponent
<Accordion
id="buildMetadata"
heading="Metadata"
disabled={!metadataResult.loading && !metadataResult.value}
@@ -162,11 +162,11 @@ export const BuildDetailsComponent = ({
{!metadataResult.loading && metadataResult.value && (
<StructuredMetadataTable metadata={metadataResult.value} />
)}
</AccordionComponent>
</Accordion>
<AccordionComponent id="buildTimeline" heading="Timeline" unmountOnExit>
<BuildTimelineComponent targets={targets} />
</AccordionComponent>
<Accordion id="buildTimeline" heading="Timeline" unmountOnExit>
<BuildTimeline targets={targets} />
</Accordion>
</Grid>
</Grid>
);
@@ -177,7 +177,7 @@ type WithRequestProps = Omit<BuildDetailsProps, 'buildData'> & {
};
export const withRequest =
(Component: typeof BuildDetailsComponent) =>
(Component: typeof BuildDetails) =>
({ buildId, ...props }: WithRequestProps) => {
const client = useApi(xcmetricsApiRef);
const {
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { AccordionComponent } from './AccordionComponent';
export { BuildDetails, withRequest } from './BuildDetails';
@@ -1,16 +0,0 @@
/*
* 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 { BuildDetailsComponent, withRequest } from './BuildDetailsComponent';
@@ -16,29 +16,29 @@
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
import { BuildListComponent } from './BuildListComponent';
import { BuildList } from './BuildList';
import { xcmetricsApiRef } from '../../api';
import userEvent from '@testing-library/user-event';
jest.mock('../../api/XcmetricsClient');
const client = require('../../api/XcmetricsClient');
jest.mock('../BuildListFilterComponent', () => ({
BuildListFilterComponent: () => 'BuildListFilterComponent',
jest.mock('../BuildListFilter', () => ({
BuildListFilter: () => 'BuildListFilter',
}));
jest.mock('../BuildDetailsComponent', () => ({
jest.mock('../BuildDetails', () => ({
withRequest: (component: any) => component,
BuildDetailsComponent: () => 'BuildDetailsComponent',
BuildDetails: () => 'BuildDetails',
}));
describe('BuildListComponent', () => {
describe('BuildList', () => {
it('should render', async () => {
const rendered = await renderInTestApp(
<ApiProvider
apis={ApiRegistry.with(xcmetricsApiRef, client.XcmetricsClient)}
>
<BuildListComponent />
<BuildList />
</ApiProvider>,
);
@@ -53,16 +53,14 @@ describe('BuildListComponent', () => {
<ApiProvider
apis={ApiRegistry.with(xcmetricsApiRef, client.XcmetricsClient)}
>
<BuildListComponent />
<BuildList />
</ApiProvider>,
);
userEvent.click(
(await rendered.findAllByLabelText('Detail panel visiblity toggle'))[0],
);
expect(
await rendered.findByText('BuildDetailsComponent'),
).toBeInTheDocument();
expect(await rendered.findByText('BuildDetails')).toBeInTheDocument();
});
it('should show errors', async () => {
@@ -75,7 +73,7 @@ describe('BuildListComponent', () => {
<ApiProvider
apis={ApiRegistry.with(xcmetricsApiRef, client.XcmetricsClient)}
>
<BuildListComponent />
<BuildList />
</ApiProvider>,
);
@@ -18,10 +18,10 @@ import { Table } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { BuildFilters, xcmetricsApiRef } from '../../api';
import { createStyles, Grid, makeStyles } from '@material-ui/core';
import { BuildListFilterComponent as Filters } from '../BuildListFilterComponent';
import { BuildListFilter as Filters } from '../BuildListFilter';
import { DateTime } from 'luxon';
import { buildPageColumns } from '../BuildTableColumns';
import { BuildDetailsComponent, withRequest } from '../BuildDetailsComponent';
import { BuildDetails, withRequest } from '../BuildDetails';
import { BackstageTheme } from '@backstage/theme';
const useStyles = makeStyles((theme: BackstageTheme) =>
@@ -33,7 +33,7 @@ const useStyles = makeStyles((theme: BackstageTheme) =>
}),
);
export const BuildListComponent = () => {
export const BuildList = () => {
const classes = useStyles();
const client = useApi(xcmetricsApiRef);
const tableRef = useRef<any>();
@@ -81,10 +81,10 @@ export const BuildListComponent = () => {
});
}}
detailPanel={rowData => {
const BuildDetails = withRequest(BuildDetailsComponent);
const BuildDetailsWithRequest = withRequest(BuildDetails);
return (
<div className={classes.detailPanel}>
<BuildDetails buildId={(rowData as any).rowData.id} />
<BuildDetailsWithRequest buildId={(rowData as any).rowData.id} />
</div>
);
}}
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './DatePickerComponent';
export { BuildList } from './BuildList';
@@ -17,15 +17,15 @@ import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
import userEvent from '@testing-library/user-event';
import { BuildListFilterComponent } from './BuildListFilterComponent';
import { BuildListFilter } from './BuildListFilter';
import { BuildFilters, xcmetricsApiRef } from '../../api';
import { RenderResult } from '@testing-library/react';
jest.mock('../../api/XcmetricsClient');
const client = require('../../api/XcmetricsClient');
jest.mock('../DatePickerComponent', () => ({
DatePickerComponent: () => 'DatePickerComponent',
jest.mock('../DatePicker', () => ({
DatePicker: () => 'DatePicker',
}));
const initialValues = {
@@ -40,7 +40,7 @@ const renderWithFiltersVisible = async (
<ApiProvider
apis={ApiRegistry.with(xcmetricsApiRef, client.XcmetricsClient)}
>
<BuildListFilterComponent
<BuildListFilter
initialValues={initialValues}
onFilterChange={callback ?? jest.fn()}
/>
@@ -64,13 +64,13 @@ const setProjectFilter = async (rendered: RenderResult, option: string) => {
userEvent.click(options[options.length - 1]);
};
describe('BuildListFilterComponent', () => {
describe('BuildListFilter', () => {
it('should render', async () => {
const rendered = await renderInTestApp(
<ApiProvider
apis={ApiRegistry.with(xcmetricsApiRef, client.XcmetricsClient)}
>
<BuildListFilterComponent
<BuildListFilter
initialValues={initialValues}
onFilterChange={jest.fn()}
/>
@@ -83,14 +83,12 @@ describe('BuildListFilterComponent', () => {
it('should toggle between showing and hiding filters', async () => {
const rendered = await renderWithFiltersVisible();
expect(
(await rendered.findAllByText('DatePickerComponent')).length,
).toEqual(2);
expect((await rendered.findAllByText('DatePicker')).length).toEqual(2);
expect(await rendered.findByText('Status')).toBeInTheDocument();
expect(await rendered.findByText('Project')).toBeInTheDocument();
userEvent.click(rendered.getByLabelText('hide filters'));
expect(rendered.queryByText('DatePickerComponent')).toBeNull();
expect(rendered.queryByText('DatePicker')).toBeNull();
expect(rendered.queryByText('Status')).toBeNull();
expect(rendered.queryByText('Project')).toBeNull();
});
@@ -22,7 +22,7 @@ import { BackstageTheme } from '@backstage/theme';
import { useApi } from '@backstage/core-plugin-api';
import { useAsync } from 'react-use';
import { BuildFilters, BuildStatus, xcmetricsApiRef } from '../../api';
import { DatePickerComponent } from '../DatePickerComponent';
import { DatePicker } from '../DatePicker';
const toSelectItems = (strings: string[]) => {
return strings.map(str => ({ label: str, value: str }));
@@ -41,7 +41,7 @@ interface FiltersProps {
onFilterChange: (filters: BuildFilters) => void;
}
export const BuildListFilterComponent = ({
export const BuildListFilter = ({
onFilterChange,
initialValues,
}: FiltersProps) => {
@@ -93,14 +93,14 @@ export const BuildListFilterComponent = ({
className={classes.filtersContent}
>
<Grid item sm={6} md={4} lg={2}>
<DatePickerComponent
<DatePicker
label="From"
value={values.from}
onDateChange={date => setValues({ ...values, from: date })}
/>
</Grid>
<Grid item sm={6} md={4} lg={2}>
<DatePickerComponent
<DatePicker
label="To"
value={values.to}
onDateChange={date => setValues({ ...values, to: date })}
@@ -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 { BuildListFilter } from './BuildListFilter';
@@ -19,7 +19,7 @@ import React from 'react';
import { TableColumn } from '@backstage/core-components';
import { Build } from '../api';
import { formatTime, formatDuration } from '../utils';
import { StatusIconComponent as StatusIcon } from './StatusIconComponent';
import { StatusIcon } from './StatusIcon';
const baseColumns: TableColumn<Build>[] = [
{
@@ -15,19 +15,15 @@
*/
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { BuildTimelineComponent } from './BuildTimelineComponent';
import { BuildTimeline } from './BuildTimeline';
jest.mock('../../api/XcmetricsClient');
const client = require('../../api/XcmetricsClient');
describe('BuildTimelineComponent', () => {
describe('BuildTimeline', () => {
it('should render', async () => {
const rendered = await renderInTestApp(
<BuildTimelineComponent
targets={[client.mockTarget]}
height={100}
width={100}
/>,
<BuildTimeline targets={[client.mockTarget]} height={100} width={100} />,
);
expect(
await rendered.findByText(client.mockTarget.name),
@@ -35,9 +31,7 @@ describe('BuildTimelineComponent', () => {
});
it('should render a message if no targets are provided', async () => {
const rendered = await renderInTestApp(
<BuildTimelineComponent targets={[]} />,
);
const rendered = await renderInTestApp(<BuildTimeline targets={[]} />);
expect(rendered.getByText('No Targets')).toBeInTheDocument();
});
});
@@ -85,7 +85,7 @@ export interface BuildTimelineProps {
width?: number;
}
export const BuildTimelineComponent = ({
export const BuildTimeline = ({
targets,
height,
width,
@@ -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 { BuildTimeline } from './BuildTimeline';
@@ -1,16 +0,0 @@
/*
* 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 { BuildTimelineComponent } from './BuildTimelineComponent';
@@ -17,27 +17,27 @@ import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { BuildsPage } from './BuildsPage';
jest.mock('../BuildDetailsComponent', () => ({
jest.mock('../BuildDetails', () => ({
withRequest: (component: any) => component,
BuildDetailsComponent: () => 'BuildDetailsComponent',
BuildDetails: () => 'BuildDetails',
}));
jest.mock('../BuildListComponent', () => ({
BuildListComponent: () => 'BuildListComponent',
jest.mock('../BuildList', () => ({
BuildList: () => 'BuildList',
}));
describe('BuildPageComponent', () => {
it('should render BuildDetailsWithDataComponent if build id is provided in path', async () => {
describe('BuildPage', () => {
it('should render BuildDetails if build id is provided in path', async () => {
const rendered = await renderInTestApp(<BuildsPage />, {
routeEntries: [`/buildId`],
});
expect(rendered.getByText('BuildDetailsComponent')).toBeInTheDocument();
expect(rendered.getByText('BuildDetails')).toBeInTheDocument();
});
it('should render BuildListComponent if no build id is provided in path', async () => {
it('should render BuildList if no build id is provided in path', async () => {
const rendered = await renderInTestApp(<BuildsPage />);
expect(rendered.getByText('BuildListComponent')).toBeInTheDocument();
expect(rendered.getByText('BuildList')).toBeInTheDocument();
});
});
@@ -16,22 +16,22 @@
import React from 'react';
import { useRouteRefParams } from '@backstage/core-plugin-api';
import { buildsRouteRef } from '../../routes';
import { BuildListComponent } from '../BuildListComponent';
import { BuildDetailsComponent, withRequest } from '../BuildDetailsComponent';
import { BuildList } from '../BuildList';
import { BuildDetails, withRequest } from '../BuildDetails';
import { InfoCard } from '@backstage/core-components';
export const BuildsPage = () => {
const { '*': buildId } = useRouteRefParams(buildsRouteRef) ?? { '*': '' };
if (buildId) {
const BuildDetails = withRequest(BuildDetailsComponent);
const BuildDetailsWithRequest = withRequest(BuildDetails);
return (
<InfoCard title="Build Details" subheader={buildId}>
<BuildDetails buildId={buildId} showId={false} />
<BuildDetailsWithRequest buildId={buildId} showId={false} />
</InfoCard>
);
}
return <BuildListComponent />;
return <BuildList />;
};
@@ -14,15 +14,15 @@
* limitations under the License.
*/
import React from 'react';
import { DataValueComponent, DataValueGridItem } from './DataValueComponent';
import { DataValue, DataValueGridItem } from './DataValue';
import { renderInTestApp } from '@backstage/test-utils';
describe('DataValueComponent', () => {
describe('DataValue', () => {
it('should render', async () => {
const field = 'Field';
const value = 'Value';
const rendered = await renderInTestApp(
<DataValueComponent field={field} value={value} />,
<DataValue field={field} value={value} />,
);
expect(rendered.getByText(field)).toBeInTheDocument();
expect(rendered.getByText(value)).toBeInTheDocument();
@@ -30,9 +30,7 @@ describe('DataValueComponent', () => {
it('should render placeholder text when no value is present', async () => {
const field = 'Field';
const rendered = await renderInTestApp(
<DataValueComponent field={field} />,
);
const rendered = await renderInTestApp(<DataValue field={field} />);
expect(rendered.getByText(field)).toBeInTheDocument();
expect(rendered.getByText('--')).toBeInTheDocument();
});
@@ -21,7 +21,7 @@ interface DataValueProps {
value?: string | number | null | undefined;
}
export const DataValueComponent = ({ field, value }: DataValueProps) => {
export const DataValue = ({ field, value }: DataValueProps) => {
return (
<div>
<Typography variant="caption">{field}</Typography>
@@ -38,6 +38,6 @@ interface GridProps {
export const DataValueGridItem = (props: DataValueProps & GridProps) => (
<Grid item xs={props.xs ?? 6} md={props.md ?? 6} lg={props.lg ?? 4}>
<DataValueComponent {...props} />
<DataValue {...props} />
</Grid>
);
@@ -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 { DataValue, DataValueGridItem } from './DataValue';
@@ -15,15 +15,13 @@
*/
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { DatePickerComponent } from './DatePickerComponent';
import { DatePicker } from './DatePicker';
import userEvent from '@testing-library/user-event';
describe('DatePickerComponent', () => {
describe('DatePicker', () => {
it('should render', async () => {
const label = 'label';
const rendered = await renderInTestApp(
<DatePickerComponent label={label} />,
);
const rendered = await renderInTestApp(<DatePicker label={label} />);
expect(rendered.getByText(label)).toBeInTheDocument();
});
@@ -31,7 +29,7 @@ describe('DatePickerComponent', () => {
const label = 'label';
const callback = jest.fn();
const rendered = await renderInTestApp(
<DatePickerComponent label={label} onDateChange={callback} />,
<DatePicker label={label} onDateChange={callback} />,
);
const input = rendered.getByLabelText(label);
@@ -43,7 +41,7 @@ describe('DatePickerComponent', () => {
const label = 'label';
const callback = jest.fn();
const rendered = await renderInTestApp(
<DatePickerComponent label={label} onDateChange={callback} />,
<DatePicker label={label} onDateChange={callback} />,
);
const input = rendered.getByLabelText(label);
@@ -63,7 +63,7 @@ interface DatePickerProps {
onDateChange?: (date: string) => void;
}
export const DatePickerComponent = ({
export const DatePicker = ({
label,
onDateChange,
...inputProps
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './OverviewComponent';
export { DatePicker } from './DatePicker';
@@ -17,26 +17,26 @@ import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { xcmetricsApiRef } from '../../api';
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
import { OverviewComponent } from './OverviewComponent';
import { Overview } from './Overview';
jest.mock('../../api/XcmetricsClient');
const client = require('../../api/XcmetricsClient');
jest.mock('../OverviewTrendsComponent', () => ({
OverviewTrendsComponent: () => 'OverviewTrendsComponent',
jest.mock('../OverviewTrends', () => ({
OverviewTrends: () => 'OverviewTrends',
}));
jest.mock('../StatusMatrixComponent', () => ({
StatusMatrixComponent: () => 'StatusMatrixComponent',
jest.mock('../StatusMatrix', () => ({
StatusMatrix: () => 'StatusMatrix',
}));
describe('OverviewComponent', () => {
describe('Overview', () => {
it('should render', async () => {
const rendered = await renderInTestApp(
<ApiProvider
apis={ApiRegistry.with(xcmetricsApiRef, client.XcmetricsClient)}
>
<OverviewComponent />
<Overview />
</ApiProvider>,
);
@@ -50,7 +50,7 @@ describe('OverviewComponent', () => {
const rendered = await renderInTestApp(
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, api)}>
<OverviewComponent />
<Overview />
</ApiProvider>,
);
@@ -65,7 +65,7 @@ describe('OverviewComponent', () => {
const rendered = await renderInTestApp(
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, api)}>
<OverviewComponent />
<Overview />
</ApiProvider>,
);
@@ -26,12 +26,12 @@ import { useApi } from '@backstage/core-plugin-api';
import { xcmetricsApiRef } from '../../api';
import { useAsync } from 'react-use';
import { Alert } from '@material-ui/lab';
import { StatusMatrixComponent } from '../StatusMatrixComponent';
import { StatusMatrix } from '../StatusMatrix';
import { Grid } from '@material-ui/core';
import { OverviewTrendsComponent } from '../OverviewTrendsComponent';
import { OverviewTrends } from '../OverviewTrends';
import { overviewColumns } from '../BuildTableColumns';
export const OverviewComponent = () => {
export const Overview = () => {
const client = useApi(xcmetricsApiRef);
const {
value: builds,
@@ -74,14 +74,14 @@ export const OverviewComponent = () => {
title={
<>
Latest Builds
<StatusMatrixComponent />
<StatusMatrix />
</>
}
/>
</Grid>
<Grid item xs={12} md={4} lg={4} xl={3}>
<InfoCard>
<OverviewTrendsComponent />
<OverviewTrends />
</InfoCard>
</Grid>
</Grid>
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './DataValueComponent';
export { Overview } from './Overview';
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React from 'react';
import { OverviewTrendsComponent } from './OverviewTrendsComponent';
import { OverviewTrends } from './OverviewTrends';
import { renderInTestApp } from '@backstage/test-utils';
import { xcmetricsApiRef } from '../../api';
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
@@ -23,13 +23,13 @@ import userEvent from '@testing-library/user-event';
jest.mock('../../api/XcmetricsClient');
const client = require('../../api/XcmetricsClient');
describe('OverviewTrendsComponent', () => {
describe('OverviewTrends', () => {
it('should render', async () => {
const rendered = await renderInTestApp(
<ApiProvider
apis={ApiRegistry.with(xcmetricsApiRef, client.XcmetricsClient)}
>
<OverviewTrendsComponent />
<OverviewTrends />
</ApiProvider>,
);
expect(rendered.getByText('Trends for')).toBeInTheDocument();
@@ -43,7 +43,7 @@ describe('OverviewTrendsComponent', () => {
const rendered = await renderInTestApp(
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, api)}>
<OverviewTrendsComponent />
<OverviewTrends />
</ApiProvider>,
);
expect(rendered.getByText('--')).toBeInTheDocument();
@@ -54,7 +54,7 @@ describe('OverviewTrendsComponent', () => {
<ApiProvider
apis={ApiRegistry.with(xcmetricsApiRef, client.XcmetricsClient)}
>
<OverviewTrendsComponent />
<OverviewTrends />
</ApiProvider>,
);
@@ -77,7 +77,7 @@ describe('OverviewTrendsComponent', () => {
const rendered = await renderInTestApp(
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, api)}>
<OverviewTrendsComponent />
<OverviewTrends />
</ApiProvider>,
);
expect(rendered.getByText(buildCountError)).toBeInTheDocument();
@@ -16,12 +16,12 @@
import { Grid, makeStyles, useTheme } from '@material-ui/core';
import React, { useState } from 'react';
import { Progress, Select } from '@backstage/core-components';
import { TrendComponent } from '../TrendComponent';
import { Trend } from '../Trend';
import { Alert, AlertTitle } from '@material-ui/lab';
import { xcmetricsApiRef } from '../../api';
import { useAsync } from 'react-use';
import { useApi } from '@backstage/core-plugin-api';
import { DataValueGridItem } from '../DataValueComponent';
import { DataValueGridItem } from '../DataValue';
import {
formatDuration,
formatPercentage,
@@ -49,7 +49,7 @@ const DAYS_SELECT_ITEMS = [
{ label: '60 days', value: 60 },
];
export const OverviewTrendsComponent = () => {
export const OverviewTrends = () => {
const [days, setDays] = useState(14);
const theme = useTheme<BackstageTheme>();
const classes = useStyles();
@@ -103,17 +103,17 @@ export const OverviewTrendsComponent = () => {
)}
{(!buildCountsResult.error || !buildTimesResult.error) && (
<div className={classes.spacingVertical}>
<TrendComponent
<Trend
title="Build Time"
color={theme.palette.secondary.main}
data={getValues(e => e.durationP50, buildTimesResult.value)}
/>
<TrendComponent
<Trend
title="Error Rate"
color={theme.palette.status.warning}
data={getErrorRatios(buildCountsResult.value)}
/>
<TrendComponent
<Trend
title="Build Count"
color={theme.palette.primary.main}
data={getValues(e => e.builds, buildCountsResult.value)}
@@ -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 { OverviewTrends } from './OverviewTrends';
@@ -1,16 +0,0 @@
/*
* 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';
@@ -65,7 +65,7 @@ interface NonExpandableProps extends PreformattedTextProps {
title?: string;
}
export const PreformattedTextComponent = ({
export const PreformattedText = ({
text,
maxChars,
expandable,
@@ -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 { PreformattedText } from './PreformattedText';
@@ -1,16 +0,0 @@
/*
* 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 { PreformattedTextComponent } from './PreformattedTextComponent';
@@ -17,20 +17,20 @@ import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
import userEvent from '@testing-library/user-event';
import { StatusCellComponent } from './StatusCellComponent';
import { StatusCell } from './StatusCell';
import { xcmetricsApiRef } from '../../api';
import { formatDuration, formatStatus } from '../../utils';
jest.mock('../../api/XcmetricsClient');
const client = require('../../api/XcmetricsClient');
describe('StatusCellComponent', () => {
describe('StatusCell', () => {
it('should render', async () => {
const rendered = await renderInTestApp(
<ApiProvider
apis={ApiRegistry.with(xcmetricsApiRef, client.XcmetricsClient)}
>
<StatusCellComponent
<StatusCell
buildStatus={{
id: client.mockBuild.id,
buildStatus: client.mockBuild.buildStatus,
@@ -102,7 +102,7 @@ const useStyles = makeStyles<BackstageTheme, StatusCellProps>(theme => ({
} as StatusStyle),
}));
export const StatusCellComponent = (props: StatusCellProps) => {
export const StatusCell = (props: StatusCellProps) => {
const classes = useStyles(props);
const { buildStatus } = props;
@@ -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 { StatusCell } from './StatusCell';
@@ -1,16 +0,0 @@
/*
* 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 './StatusCellComponent';
@@ -16,23 +16,19 @@
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { StatusIconComponent } from './StatusIconComponent';
import { StatusIcon } from './StatusIcon';
describe('StatusIconComponent', () => {
describe('StatusIcon', () => {
it('should render', async () => {
let rendered = await renderInTestApp(
<StatusIconComponent buildStatus="succeeded" />,
<StatusIcon buildStatus="succeeded" />,
);
expect(rendered.getByLabelText('Status ok')).toBeInTheDocument();
rendered = await renderInTestApp(
<StatusIconComponent buildStatus="failed" />,
);
rendered = await renderInTestApp(<StatusIcon buildStatus="failed" />);
expect(rendered.getByLabelText('Status error')).toBeInTheDocument();
rendered = await renderInTestApp(
<StatusIconComponent buildStatus="stopped" />,
);
rendered = await renderInTestApp(<StatusIcon buildStatus="stopped" />);
expect(rendered.getByLabelText('Status warning')).toBeInTheDocument();
});
});
@@ -31,5 +31,5 @@ interface StatusIconProps {
buildStatus: BuildStatus;
}
export const StatusIconComponent = ({ buildStatus }: StatusIconProps) =>
export const StatusIcon = ({ buildStatus }: StatusIconProps) =>
STATUS_ICONS[buildStatus];
@@ -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 { StatusIcon } from './StatusIcon';
@@ -1,16 +0,0 @@
/*
* 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 { StatusIconComponent } from './StatusIconComponent';
@@ -16,19 +16,19 @@
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
import { StatusMatrixComponent } from './StatusMatrixComponent';
import { StatusMatrix } from './StatusMatrix';
import { xcmetricsApiRef } from '../../api';
jest.mock('../../api/XcmetricsClient');
const client = require('../../api/XcmetricsClient');
describe('StatusMatrixComponent', () => {
describe('StatusMatrix', () => {
it('should render', async () => {
const rendered = await renderInTestApp(
<ApiProvider
apis={ApiRegistry.with(xcmetricsApiRef, client.XcmetricsClient)}
>
<StatusMatrixComponent />
<StatusMatrix />
</ApiProvider>,
);
@@ -21,7 +21,7 @@ import { useAsync, useMeasure } from 'react-use';
import { cn } from '../../utils';
import { useApi } from '@backstage/core-plugin-api';
import { Alert } from '@material-ui/lab';
import { StatusCellComponent } from '../StatusCellComponent';
import { StatusCell } from '../StatusCell';
const CELL_SIZE = 12;
const CELL_MARGIN = 4;
@@ -44,7 +44,7 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
},
}));
export const StatusMatrixComponent = () => {
export const StatusMatrix = () => {
const classes = useStyles();
const [measureRef, { width: rootWidth }] = useMeasure<HTMLDivElement>();
const client = useApi(xcmetricsApiRef);
@@ -68,11 +68,7 @@ export const StatusMatrixComponent = () => {
{loading &&
[...new Array(cols * MAX_ROWS)].map((_, index) => {
return (
<StatusCellComponent
key={index}
size={CELL_SIZE}
spacing={CELL_MARGIN}
/>
<StatusCell key={index} size={CELL_SIZE} spacing={CELL_MARGIN} />
);
})}
@@ -80,7 +76,7 @@ export const StatusMatrixComponent = () => {
builds
.slice(0, cols * MAX_ROWS)
.map((buildStatus, index) => (
<StatusCellComponent
<StatusCell
key={index}
buildStatus={buildStatus}
size={CELL_SIZE}
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './BuildListFilterComponent';
export { StatusMatrix } from './StatusMatrix';
@@ -1,16 +0,0 @@
/*
* 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 './StatusMatrixComponent';
@@ -14,15 +14,15 @@
* limitations under the License.
*/
import React from 'react';
import { TrendComponent } from './TrendComponent';
import { Trend } from './Trend';
import { renderInTestApp } from '@backstage/test-utils';
describe('TrendComponent', () => {
describe('Trend', () => {
it('should render', async () => {
const data = [1, 2, 3, 4];
const title = 'testTitle';
const rendered = await renderInTestApp(
<TrendComponent data={data} title={title} color="#000" />,
<Trend data={data} title={title} color="#000" />,
);
expect(rendered.findAllByText('testTitle')).toBeTruthy();
});
@@ -30,7 +30,7 @@ describe('TrendComponent', () => {
it('should render empty state', async () => {
const title = 'testTitle';
const rendered = await renderInTestApp(
<TrendComponent title={title} color="#000" />,
<Trend title={title} color="#000" />,
);
expect(rendered.findAllByText('testTitle')).toBeTruthy();
});
@@ -23,7 +23,7 @@ interface TrendProps {
color: string;
}
export const TrendComponent = ({ data, title, color }: TrendProps) => {
export const Trend = ({ data, title, color }: TrendProps) => {
const emptyData = [0, 0];
const max = Math.max(...(data ?? emptyData));
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './TrendComponent';
export { Trend } from './Trend';
@@ -23,12 +23,12 @@ import userEvent from '@testing-library/user-event';
jest.mock('../../api/XcmetricsClient');
const client = require('../../api/XcmetricsClient');
jest.mock('../OverviewComponent', () => ({
OverviewComponent: () => 'OverviewComponent',
jest.mock('../Overview', () => ({
Overview: () => 'OverviewComponent',
}));
jest.mock('../BuildListComponent', () => ({
BuildListComponent: () => 'BuildListComponent',
jest.mock('../BuildList', () => ({
BuildList: () => 'BuildList',
}));
describe('XcmetricsLayout', () => {
@@ -45,7 +45,18 @@ describe('XcmetricsLayout', () => {
expect(rendered.getByText('Builds')).toBeInTheDocument();
expect(rendered.getByText('OverviewComponent')).toBeInTheDocument();
});
it('should show a list of builds when the Builds tab is selected', async () => {
const rendered = await renderInTestApp(
<ApiProvider
apis={ApiRegistry.with(xcmetricsApiRef, client.XcmetricsClient)}
>
<XcmetricsLayout />
</ApiProvider>,
);
userEvent.click(rendered.getByText('Builds'));
expect(await rendered.findByText('BuildListComponent')).toBeInTheDocument();
expect(await rendered.findByText('BuildList')).toBeInTheDocument();
});
});
@@ -21,7 +21,7 @@ import {
Page,
TabbedLayout,
} from '@backstage/core-components';
import { OverviewComponent } from '../OverviewComponent';
import { Overview } from '../Overview';
import { buildsRouteRef, rootRouteRef } from '../../routes';
import { RouteRef, SubRouteRef } from '@backstage/core-plugin-api';
import { BuildsPage } from '../BuildsPage';
@@ -36,7 +36,7 @@ const TABS: TabConfig[] = [
{
routeRef: rootRouteRef,
title: 'Overview',
component: <OverviewComponent />,
component: <Overview />,
},
{
routeRef: buildsRouteRef,
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './XcmetricsLayout';
export { XcmetricsLayout } from './XcmetricsLayout';