Implement filtering on project name of builds

Signed-off-by: Niklas Granander <ngranander@spotify.com>
This commit is contained in:
Niklas Granander
2021-07-29 18:51:16 +02:00
parent 4509e18356
commit 66f43e8180
6 changed files with 119 additions and 86 deletions
+17 -7
View File
@@ -20,7 +20,7 @@ import { DateTime } from 'luxon';
import {
Build,
BuildCount,
BuildStatus,
BuildFilters,
BuildStatusResult,
BuildTime,
PaginationResult,
@@ -61,9 +61,7 @@ export class XcmetricsClient implements XcmetricsApi {
}
async getFilteredBuilds(
from: string,
to: string,
status?: BuildStatus,
filters: BuildFilters,
page?: number,
perPage?: number,
): Promise<PaginationResult<Build>> {
@@ -72,14 +70,15 @@ export class XcmetricsClient implements XcmetricsApi {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
from: DateTime.fromISO(from)
from: DateTime.fromISO(filters.from)
.startOf('day')
.toISO({ suppressMilliseconds: true }),
to: DateTime.fromISO(to)
to: DateTime.fromISO(filters.to)
.endOf('day')
.startOf('second')
.toISO({ suppressMilliseconds: true }),
status,
status: filters.buildStatus,
projectName: filters.project,
page,
per: perPage,
}),
@@ -131,4 +130,15 @@ export class XcmetricsClient implements XcmetricsApi {
return ((await response.json()) as PaginationResult<BuildStatusResult>)
.items;
}
async getProjects(): Promise<string[]> {
const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`;
const response = await fetch(`${baseUrl}/build/project`);
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return (await response.json()) as string[];
}
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Build, BuildStatus, XcmetricsApi } from '../types';
import { Build, BuildFilters, XcmetricsApi } from '../types';
export const mockBuild = {
userid: 'userid1',
@@ -62,11 +62,9 @@ export const XcmetricsClient: XcmetricsApi = {
]);
},
getFilteredBuilds: (
from: string,
to: string,
status?: BuildStatus,
page?: number,
perPage?: number,
_filters: BuildFilters,
_page?: number,
_perPage?: number,
) => {
return Promise.resolve({
items: [mockBuild],
@@ -86,4 +84,7 @@ export const XcmetricsClient: XcmetricsApi = {
getBuildTimes: (days: number) => {
return Promise.resolve([mockBuildTime, mockBuildTime].slice(0, days));
},
getProjects: () => {
return Promise.resolve([mockBuild.projectName]);
},
};
+9 -11
View File
@@ -68,27 +68,25 @@ export type PaginationResult<T> = {
};
};
export type BuildFilters = {
from: string; // ISO Date (e.g. "2021-01-01")
to: string; // ISO Date (e.g. "2021-01-02")
buildStatus?: BuildStatus;
project?: string;
};
export interface XcmetricsApi {
getBuild(id: string): Promise<Build>;
getBuilds(limit?: number): Promise<Build[]>;
/**
* 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,
filters: BuildFilters,
page?: number,
perPage?: number,
): Promise<PaginationResult<Build>>;
getBuildCounts(days: number): Promise<BuildCount[]>;
getBuildTimes(days: number): Promise<BuildTime[]>;
getBuildStatuses(limit: number): Promise<BuildStatusResult[]>;
getProjects(): Promise<string[]>;
}
export const xcmetricsApiRef = createApiRef<XcmetricsApi>({
@@ -13,16 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useEffect, useRef, useState } from 'react';
import React, { useRef, useState } from 'react';
import { Table, TableColumn } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { Build, xcmetricsApiRef } from '../../api';
import { Build, BuildFilters, xcmetricsApiRef } from '../../api';
import { formatDuration, formatTime } from '../../utils';
import { Chip, Grid } from '@material-ui/core';
import {
ActiveFilters,
BuildListFilterComponent as Filters,
} from '../BuildListFilterComponent';
import { BuildListFilterComponent as Filters } from '../BuildListFilterComponent';
import { DateTime } from 'luxon';
const columns: TableColumn<Build>[] = [
@@ -62,31 +59,39 @@ const columns: TableColumn<Build>[] = [
];
export const BuildListComponent = () => {
const initDates = {
const client = useApi(xcmetricsApiRef);
const tableRef = useRef<any>();
const initialFilters = {
from: DateTime.now().minus({ year: 1 }).toISODate(),
to: DateTime.now().toISODate(),
};
const client = useApi(xcmetricsApiRef);
const tableRef = useRef<any>();
const [filters, setFilters] = useState<ActiveFilters>(initDates);
useEffect(() => tableRef.current?.onQueryChange(), [filters]);
const [filters, setFilters] = useState<BuildFilters>(initialFilters);
const handleFilterChange = (values: BuildFilters) => {
setFilters(values);
tableRef.current?.onQueryChange();
};
return (
<Grid container spacing={3} direction="column">
<Filters onFilterChange={setFilters} initDates={initDates} />
<Filters
onFilterChange={handleFilterChange}
initialValues={initialFilters}
/>
<Table
tableRef={tableRef}
title="Builds"
columns={columns}
options={{ paging: true, sorting: false, search: false, pageSize: 10 }}
tableRef={tableRef}
data={query => {
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
filters,
query.page + 1, // Page is 0-indexed in Table
query.pageSize,
)
.then(result => {
@@ -99,8 +104,6 @@ export const BuildListComponent = () => {
.catch(reason => reject(reason));
});
}}
columns={columns}
title="Builds"
/>
</Grid>
);
@@ -14,24 +14,18 @@
* 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 { makeStyles, IconButton, Grid, Button } from '@material-ui/core';
import FilterList from '@material-ui/icons/FilterList';
import { InfoCard, Select } from '@backstage/core-components';
import { BackstageTheme } from '@backstage/theme';
import { BuildStatus } from '../../api';
import { useApi } from '@backstage/core-plugin-api';
import { useAsync } from 'react-use';
import { BuildFilters, BuildStatus, xcmetricsApiRef } from '../../api';
import { DatePickerComponent } from '../DatePickerComponent';
export type ActiveFilters = {
from: string;
to: string;
buildStatus?: BuildStatus;
const toSelectItems = (strings: string[]) => {
return strings.map(str => ({ label: str, value: str }));
};
const useStyles = makeStyles<BackstageTheme>(theme => ({
@@ -43,37 +37,26 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
type FilterOption<T> = T | 'all';
interface FiltersProps {
initDates: { from: string; to: string };
onFilterChange: (filters: ActiveFilters) => void;
initialValues: BuildFilters;
onFilterChange: (filters: BuildFilters) => void;
}
export const BuildListFilterComponent = ({
onFilterChange,
initDates,
initialValues,
}: FiltersProps) => {
const classes = useStyles(useTheme<BackstageTheme>());
const [status, setStatus] = useState<BuildStatus | undefined>();
const [from, setFrom] = useState<string>(initDates.from);
const [to, setTo] = useState<string>(initDates.to);
const client = useApi(xcmetricsApiRef);
const classes = useStyles();
const [open, setOpen] = useState(false);
const [values, setValues] = useState(initialValues);
useEffect(() => onFilterChange({ from, to, buildStatus: status }), [
onFilterChange,
from,
to,
status,
]);
useEffect(() => onFilterChange(values), [onFilterChange, values]);
const numFilters =
Number(!!status) +
Number(from !== initDates.from) +
Number(to !== initDates.to);
const clear = () => {
setStatus(undefined);
setFrom(initDates.from);
setTo(initDates.to);
};
Number(values.from !== initialValues.from) +
Number(values.to !== initialValues.to) +
Number(!!values.buildStatus) +
Number(!!values.project);
const title = (
<>
@@ -82,7 +65,7 @@ export const BuildListFilterComponent = ({
</IconButton>
Filters ({numFilters})
{!!numFilters && (
<Button color="primary" onClick={clear}>
<Button color="primary" onClick={() => setValues(initialValues)}>
Clear all
</Button>
)}
@@ -96,6 +79,10 @@ export const BuildListFilterComponent = ({
{ label: 'Stopped', value: 'stopped' },
];
const { value: projects, loading } = useAsync(async () => {
return client.getProjects();
}, []);
const content = (
<Grid
container
@@ -104,21 +91,54 @@ export const BuildListFilterComponent = ({
className={classes.filtersContent}
>
<Grid item xs={2}>
<DatePickerComponent label="From" value={from} onDateChange={setFrom} />
<DatePickerComponent
label="From"
value={values.from}
onDateChange={date => setValues({ ...values, from: date })}
/>
</Grid>
<Grid item xs={2}>
<DatePickerComponent label="To" value={to} onDateChange={setTo} />
<DatePickerComponent
label="To"
value={values.to}
onDateChange={date => setValues({ ...values, to: date })}
/>
</Grid>
<Grid item xs={2}>
<Select
label="Status"
items={statusItems}
selected={!status ? 'all' : status}
onChange={arg =>
setStatus(arg === 'all' ? undefined : (arg as BuildStatus))
}
selected={!values.buildStatus ? 'all' : values.buildStatus}
onChange={selection => {
const buildStatus =
selection === 'all' ? undefined : (selection as BuildStatus);
setValues({ ...values, buildStatus });
}}
/>
</Grid>
<Grid item xs={2}>
{loading ? (
<Select
label="Project"
placeholder="Loading.."
items={[]}
onChange={() => undefined}
/>
) : (
<Select
label="Project"
items={toSelectItems(['All'].concat(projects ?? []))}
selected={values.project ? values.project : 'All'}
onChange={selection =>
setValues({
...values,
project:
selection === 'All' ? undefined : (selection as string),
})
}
/>
)}
</Grid>
</Grid>
);
@@ -65,6 +65,7 @@ interface DatePickerProps {
export const DatePickerComponent = ({
label,
onDateChange,
...inputProps
}: InputProps & DatePickerProps) => {
const classes = useStyles();
@@ -75,8 +76,8 @@ export const DatePickerComponent = ({
<BootstrapInput
type="date"
fullWidth
onChange={event => onDateChange?.(event.target.value)}
{...inputProps}
onChange={event => inputProps.onDateChange?.(event.target.value)}
/>
</div>
);