diff --git a/plugins/xcmetrics/src/api/XcmetricsClient.ts b/plugins/xcmetrics/src/api/XcmetricsClient.ts index 0d4ed0dee1..19e4043e5e 100644 --- a/plugins/xcmetrics/src/api/XcmetricsClient.ts +++ b/plugins/xcmetrics/src/api/XcmetricsClient.ts @@ -16,9 +16,11 @@ import { DiscoveryApi } from '@backstage/core-plugin-api'; import { ResponseError } from '@backstage/errors'; +import { DateTime } from 'luxon'; import { Build, BuildCount, + BuildStatus, BuildStatusResult, BuildTime, PaginationResult, @@ -58,6 +60,38 @@ export class XcmetricsClient implements XcmetricsApi { return ((await response.json()) as PaginationResult).items; } + async getFilteredBuilds( + from: string, + to: string, + status?: BuildStatus, + page?: number, + perPage?: number, + ): Promise> { + const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`; + const response = await fetch(`${baseUrl}/build/filter`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + from: DateTime.fromISO(from) + .startOf('day') + .toISO({ suppressMilliseconds: true }), + to: DateTime.fromISO(to) + .endOf('day') + .startOf('second') + .toISO({ suppressMilliseconds: true }), + status, + page, + per: perPage, + }), + }); + + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + + return (await response.json()) as PaginationResult; + } + async getBuildCounts(days: number): Promise { const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`; const response = await fetch( diff --git a/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts b/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts index 5d53f41860..869ffe96f0 100644 --- a/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts +++ b/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Build, XcmetricsApi } from '../types'; +import { Build, BuildStatus, XcmetricsApi } from '../types'; export const mockBuild = { userid: 'userid1', @@ -61,6 +61,22 @@ export const XcmetricsClient: XcmetricsApi = { { ...mockBuild, id: '2', userid: 'userid2' }, ]); }, + getFilteredBuilds: ( + from: string, + to: string, + status?: BuildStatus, + page?: number, + perPage?: number, + ) => { + return Promise.resolve({ + items: [mockBuild], + metadata: { + per: 10, + total: 1, + page: 1, + }, + }); + }, getBuildCounts: () => { return Promise.resolve([mockBuildCount, mockBuildCount]); }, diff --git a/plugins/xcmetrics/src/api/types.ts b/plugins/xcmetrics/src/api/types.ts index 48bee5fd38..640951da7e 100644 --- a/plugins/xcmetrics/src/api/types.ts +++ b/plugins/xcmetrics/src/api/types.ts @@ -70,7 +70,22 @@ export type PaginationResult = { export interface XcmetricsApi { getBuild(id: string): Promise; - getBuilds(): Promise; + getBuilds(limit?: number): Promise; + + /** + * Get builds filtered by the provided parameters + * + * @param from Builds after this date. An ISO date in a string (e.g. "2020-01-01") + * @param to Builds before this date. An ISO date in a string (e.g. "2021-01-01") + * @param status Builds with this status + */ + getFilteredBuilds( + from: string, + to: string, + status?: BuildStatus, + page?: number, + perPage?: number, + ): Promise>; getBuildCounts(days: number): Promise; getBuildTimes(days: number): Promise; getBuildStatuses(limit: number): Promise; diff --git a/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx b/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx new file mode 100644 index 0000000000..c54bd7d6cc --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx @@ -0,0 +1,107 @@ +/* + * 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, { useEffect, useRef, useState } from 'react'; +import { Table, TableColumn } from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import { Build, xcmetricsApiRef } from '../../api'; +import { formatDuration, formatTime } from '../../utils'; +import { Chip, Grid } from '@material-ui/core'; +import { + ActiveFilters, + BuildListFilterComponent as Filters, +} from '../BuildListFilterComponent'; +import { DateTime } from 'luxon'; + +const columns: TableColumn[] = [ + { + title: 'Status', + field: 'buildStatus', + }, + { + title: 'Project', + field: 'projectName', + }, + { + title: 'Schema', + field: 'schema', + }, + { + title: 'Started', + field: 'startedAt', + searchable: false, + render: data => formatTime(data.startTimestamp), + }, + { + title: 'Duration', + field: 'duration', + render: data => formatDuration(data.duration), + }, + { + title: 'User', + field: 'userid', + }, + { + field: 'isCI', + render: data => data.isCi && , + width: '10', + sorting: false, + }, +]; + +export const BuildListComponent = () => { + const initDates = { + from: DateTime.now().minus({ year: 1 }).toISODate(), + to: DateTime.now().toISODate(), + }; + const client = useApi(xcmetricsApiRef); + const tableRef = useRef(); + const [filters, setFilters] = useState(initDates); + + useEffect(() => tableRef.current?.onQueryChange(), [filters]); + + return ( + + + { + return new Promise((resolve, reject) => { + if (!query) return; + client + .getFilteredBuilds( + filters.from, + filters.to, + filters.buildStatus, + query.page + 1, // Page starts at 1 in API + query.pageSize, + ) + .then(result => { + resolve({ + data: result.items, + page: result.metadata.page - 1, + totalCount: result.metadata.total, + }); + }) + .catch(reason => reject(reason)); + }); + }} + columns={columns} + title="Builds" + /> + + ); +}; diff --git a/plugins/xcmetrics/src/components/BuildListComponent/index.ts b/plugins/xcmetrics/src/components/BuildListComponent/index.ts new file mode 100644 index 0000000000..3fb881458d --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildListComponent/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 './BuildListComponent'; diff --git a/plugins/xcmetrics/src/components/BuildListFilterComponent/BuildListFilterComponent.tsx b/plugins/xcmetrics/src/components/BuildListFilterComponent/BuildListFilterComponent.tsx new file mode 100644 index 0000000000..6fc2094a70 --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildListFilterComponent/BuildListFilterComponent.tsx @@ -0,0 +1,136 @@ +/* + * 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 { + makeStyles, + useTheme, + IconButton, + Grid, + Button, +} from '@material-ui/core'; +import FilterList from '@material-ui/icons/FilterList'; +import React, { useEffect, useState } from 'react'; +import { InfoCard, Select } from '@backstage/core-components'; +import { BackstageTheme } from '@backstage/theme'; +import { BuildStatus } from '../../api'; +import { DatePickerComponent } from '../DatePickerComponent'; + +export type ActiveFilters = { + from: string; + to: string; + buildStatus?: BuildStatus; +}; + +const useStyles = makeStyles(theme => ({ + filtersContent: { + padding: theme.spacing(2, 2, 2, 2.5), + }, +})); + +type FilterOption = T | 'all'; + +interface FiltersProps { + initDates: { from: string; to: string }; + onFilterChange: (filters: ActiveFilters) => void; +} + +export const BuildListFilterComponent = ({ + onFilterChange, + initDates, +}: FiltersProps) => { + const classes = useStyles(useTheme()); + const [status, setStatus] = useState(); + const [from, setFrom] = useState(initDates.from); + const [to, setTo] = useState(initDates.to); + const [open, setOpen] = useState(false); + + useEffect(() => onFilterChange({ from, to, buildStatus: status }), [ + onFilterChange, + from, + to, + status, + ]); + + const numFilters = + Number(!!status) + + Number(from !== initDates.from) + + Number(to !== initDates.to); + + const clear = () => { + setStatus(undefined); + setFrom(initDates.from); + setTo(initDates.to); + }; + + const title = ( + <> + setOpen(!open)} aria-label="filter list"> + + + Filters ({numFilters}) + {!!numFilters && ( + + )} + + ); + + const statusItems: { label: string; value: FilterOption }[] = [ + { label: 'All', value: 'all' }, + { label: 'Succeeded', value: 'succeeded' }, + { label: 'Failed', value: 'failed' }, + { label: 'Stopped', value: 'stopped' }, + ]; + + const content = ( + + + + + + + + +