Implement filtering on project name of builds
Signed-off-by: Niklas Granander <ngranander@spotify.com>
This commit is contained in:
@@ -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]);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
+62
-42
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user