Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add time field filter #1313

Merged
merged 7 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@
import { useMemo } from 'react';
import { Button } from '@inngest/components/Button';
import StatusFilter from '@inngest/components/Filter/StatusFilter';
import { type FunctionRunStatus } from '@inngest/components/types/functionRun';
import TimeFilter from '@inngest/components/Filter/TimeFilter';
// import { SelectGroup } from '@inngest/components/Select/Select';
import {
type FunctionRunStatus,
type FunctionRunTimeField,
} from '@inngest/components/types/functionRun';
import { RiLoopLeftLine } from '@remixicon/react';
import { useQuery } from 'urql';

import { useEnvironment } from '@/app/(organization-active)/(dashboard)/env/[environmentSlug]/environment-context';
import { graphql } from '@/gql';
import { useStringArraySearchParam } from '@/utils/useSearchParam';
import { FunctionRunTimeFieldV2 } from '@/gql/graphql';
import { useSearchParam, useStringArraySearchParam } from '@/utils/useSearchParam';
import RunsTable from './RunsTable';
import { toRunStatuses } from './utils';
import { toRunStatuses, toTimeField } from './utils';

const TimeFilterDefault = FunctionRunTimeFieldV2.QueuedAt;

const GetRunsDocument = graphql(`
query GetRuns($environmentID: ID!, $startTime: Time!, $status: [FunctionRunStatus!]) {
Expand Down Expand Up @@ -42,11 +50,19 @@ const renderSubComponent = ({ id }: { id: string }) => {
export default function RunsPage() {
const [rawFilteredStatus, setFilteredStatus, removeFilteredStatus] =
useStringArraySearchParam('filterStatus');
const [rawTimeField, setTimeField] = useSearchParam('timeField');

const filteredStatus = useMemo(() => {
return toRunStatuses(rawFilteredStatus ?? []);
}, [rawFilteredStatus]);

const timeField = useMemo(() => {
if (!rawTimeField) {
return TimeFilterDefault;
}
return toTimeField(rawTimeField);
}, [rawTimeField]);

function handleStatusesChange(value: FunctionRunStatus[]) {
if (value.length > 0) {
setFilteredStatus(value);
Expand All @@ -55,13 +71,20 @@ export default function RunsPage() {
}
}

function handleTimeFieldChange(value: FunctionRunTimeField) {
if (value.length > 0) {
setTimeField(value);
}
}

const environment = useEnvironment();
const [{ data, fetching: fetchingRuns }, refetch] = useQuery({
query: GetRunsDocument,
variables: {
environmentID: environment.id,
startTime: '2024-04-19T11:26:03.203Z',
status: filteredStatus.length > 0 ? filteredStatus : null,
timeField: timeField ?? TimeFilterDefault,
},
});

Expand All @@ -79,7 +102,16 @@ export default function RunsPage() {
return (
<main className="h-full min-h-0 overflow-y-auto bg-white">
<div className="flex items-center justify-between gap-2 bg-slate-50 px-8 py-2">
<StatusFilter selectedStatuses={filteredStatus} onStatusesChange={handleStatusesChange} />
<div className="flex items-center gap-2">
{/* <SelectGroup> */}
<TimeFilter
selectedTimeField={timeField ?? TimeFilterDefault}
onTimeFieldChange={handleTimeFieldChange}
/>
{/* TODO: Add date filter here */}
{/* </SelectGroup> */}
<StatusFilter selectedStatuses={filteredStatus} onStatusesChange={handleStatusesChange} />
</div>
{/* TODO: wire button */}
<Button label="Refresh" appearance="text" btnAction={refetch} icon={<RiLoopLeftLine />} />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { isFunctionRunStatus, type FunctionRunStatus } from '@inngest/components/types/functionRun';

import { FunctionRunStatus as FunctionRunStatusEnum } from '@/gql/graphql';
import {
FunctionRunStatus as FunctionRunStatusEnum,
FunctionRunTimeFieldV2 as FunctionRunTimeFieldEnum,
} from '@/gql/graphql';

/**
* Convert a run status union type into an enum. This is necessary because
Expand Down Expand Up @@ -38,3 +41,20 @@ export function toRunStatuses(statuses: string[]): FunctionRunStatusEnum[] {

return newValue;
}

/**
* Convert a time field union type into an enum. This is necessary because
* TypeScript treats as enums as nominal types, which causes silly type errors.
*/
export function toTimeField(time: string): FunctionRunTimeFieldEnum | undefined {
switch (time) {
case 'ENDED_AT':
return FunctionRunTimeFieldEnum.EndedAt;
case 'QUEUED_AT':
return FunctionRunTimeFieldEnum.QueuedAt;
case 'STARTED_AT':
return FunctionRunTimeFieldEnum.StartedAt;
default:
console.error(`unexpected time field: ${time}`);
}
}
34 changes: 10 additions & 24 deletions ui/packages/components/src/Filter/StatusFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default function StatusFilter({ selectedStatuses, onStatusesChange }: Sta

return (
<Select
multiple
defaultValue={selectedStatuses}
onChange={(value: string[]) => {
const newValue: FunctionRunStatus[] = [];
Expand All @@ -43,33 +44,18 @@ export default function StatusFilter({ selectedStatuses, onStatusesChange }: Sta
}}
label="Status"
>
<Select.Button>{statusDots}</Select.Button>
<Select.Button>
{selectedStatuses.length > 0 && <span className="pr-2">{statusDots}</span>}
</Select.Button>
<Select.Options>
{functionRunStatuses.map((option) => {
return (
<Select.CustomOption
key={option}
value={option}
className="ui-active:bg-blue-50 flex select-none items-center justify-between px-2 py-1.5 focus:outline-none"
>
{({ selected }: { selected: boolean }) => (
<span className="inline-flex items-center gap-2 lowercase">
<span className="inline-flex items-center gap-2 lowercase">
<input
type="checkbox"
id={option}
checked={selected}
readOnly
className="h-[15px] w-[15px] rounded border-slate-300 text-indigo-500 drop-shadow-sm checked:border-indigo-500 checked:drop-shadow-none"
/>
<span className="flex items-center gap-1">
<RunStatusIcon status={option} className="h-2 w-2" />
<label className="text-sm first-letter:capitalize">{option}</label>
</span>
</span>
</span>
)}
</Select.CustomOption>
<Select.CheckboxOption key={option} option={option}>
<span className="flex items-center gap-1 lowercase">
<RunStatusIcon status={option} className="h-2 w-2" />
<label className="text-sm first-letter:capitalize">{option}</label>
</span>
</Select.CheckboxOption>
);
})}
</Select.Options>
Expand Down
49 changes: 49 additions & 0 deletions ui/packages/components/src/Filter/TimeFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Select } from '../Select/Select';
import {
FunctionRunTimeFields,
isFunctionTimeField,
type FunctionRunTimeField,
} from '../types/functionRun';

type TimeFilterProps = {
selectedTimeField: FunctionRunTimeField;
onTimeFieldChange: (value: FunctionRunTimeField) => void;
};

function replaceUnderscoreWithSpace(option: FunctionRunTimeField) {
return option.replace(/_/g, ' ');
}

export default function TimeFilter({ selectedTimeField, onTimeFieldChange }: TimeFilterProps) {
return (
<Select
defaultValue={selectedTimeField}
onChange={(value: string) => {
if (isFunctionTimeField(value)) {
onTimeFieldChange(value);
}
}}
label="Status"
isLabelVisible={false}
>
<Select.Button>
<span className="pr-2 text-sm lowercase first-letter:capitalize">
{replaceUnderscoreWithSpace(selectedTimeField)}
</span>
</Select.Button>
<Select.Options>
{FunctionRunTimeFields.map((option) => {
return (
<Select.Option key={option} option={option}>
<span className="inline-flex items-center gap-2 lowercase">
<label className="text-sm first-letter:capitalize">
{replaceUnderscoreWithSpace(option)}
</label>
</span>
</Select.Option>
);
})}
</Select.Options>
</Select>
);
}
70 changes: 60 additions & 10 deletions ui/packages/components/src/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,40 @@ import { cn } from '../utils/classNames';

type SelectProps = {
label?: string;
defaultValue?: string[];
onChange: (value: string[]) => void;
isLabelVisible?: boolean;
children: React.ReactNode;
};

type MultiProps = {
onChange: (value: string[]) => void;
defaultValue?: string[];
multiple: true;
};

type SingleProps = {
onChange: (value: string) => void;
defaultValue?: string;
multiple?: false;
};

export function Select({
defaultValue,
label,
isLabelVisible = true,
children,
onChange,
}: SelectProps) {
multiple,
}: SelectProps & (MultiProps | SingleProps)) {
return (
<Listbox value={defaultValue} onChange={onChange} multiple>
<Listbox value={defaultValue} onChange={onChange} multiple={multiple}>
<span
className={cn(
isLabelVisible && 'divide-x divide-slate-300',
'flex items-center rounded-md border border-slate-300 bg-slate-50 text-sm'
)}
>
<Listbox.Label
className={cn(!isLabelVisible && 'sr-only', 'rounded-l-[5px] px-2 py-2.5 text-slate-600')}
className={cn(!isLabelVisible && 'sr-only', 'rounded-l-[5px] px-2 text-slate-600')}
>
{label}
</Listbox.Label>
Expand All @@ -45,11 +56,14 @@ function Button({
<Listbox.Button
className={cn(
!isLabelVisible && 'rounded-l-[5px]',
'flex items-center rounded-r-[5px] bg-white px-2 py-3'
'flex h-10 items-center rounded-r-[5px] bg-white px-2'
)}
>
{children}
<RiArrowDownSLine className="h-4 w-4 text-slate-500" aria-hidden="true" />
<RiArrowDownSLine
className="ui-open:-rotate-180 h-4 w-4 text-slate-500 transition-transform duration-500"
aria-hidden="true"
/>
</Listbox.Button>
);
}
Expand All @@ -67,16 +81,52 @@ function Options({ children }: React.PropsWithChildren) {
function Option({ children, option }: React.PropsWithChildren<{ option: string }>) {
return (
<Listbox.Option
className="ui-active:bg-blue-50 flex select-none items-center justify-between px-2 py-4 focus:outline-none"
className=" ui-selected:text-indigo-500 ui-selected:font-medium ui-active:bg-blue-50 flex select-none items-center justify-between focus:outline-none"
key={option}
value={option}
>
{children}
<div className="ui-selected:border-indigo-500 my-2 border-l-2 border-transparent pl-5 pr-4">
{children}
</div>
</Listbox.Option>
);
}

function CheckboxOption({ children, option }: React.PropsWithChildren<{ option: string }>) {
return (
<Listbox.Option
className=" ui-active:bg-blue-50 flex select-none items-center justify-between py-1.5 pl-2 pr-4 focus:outline-none"
key={option}
value={option}
>
{({ selected }: { selected: boolean }) => (
<span className="inline-flex items-center">
<span className="inline-flex items-center gap-2">
<input
type="checkbox"
id={option}
checked={selected}
readOnly
className="h-[15px] w-[15px] rounded border-slate-300 text-indigo-500 drop-shadow-sm checked:border-indigo-500 checked:drop-shadow-none"
/>
{children}
</span>
</span>
)}
</Listbox.Option>
);
}

Select.Button = Button;
Select.Options = Options;
Select.Option = Option;
Select.CustomOption = Listbox.Option;
Select.CheckboxOption = CheckboxOption;

// Used as a wrapper when we group select components in something similar to a button group
export function SelectGroup({ children }: React.PropsWithChildren) {
return (
<div className="flex items-center [&>*:first-child]:rounded-r-none [&>*:last-child]:rounded-l-none [&>*:not(:first-child)]:border-l-0">
{children}
</div>
);
}
6 changes: 6 additions & 0 deletions ui/packages/components/src/types/functionRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ export type FunctionRun = {
startedAt: Date | null;
status: FunctionRunStatus;
};

export const FunctionRunTimeFields = ['ENDED_AT', 'QUEUED_AT', 'STARTED_AT'] as const;
export type FunctionRunTimeField = (typeof FunctionRunTimeFields)[number];
export function isFunctionTimeField(s: string): s is FunctionRunTimeField {
return FunctionRunTimeFields.includes(s as FunctionRunTimeField);
}
Loading