Skip to content

Commit

Permalink
INN 2940 create select shared component (#1311)
Browse files Browse the repository at this point in the history
* Add new status icons

* Replace status icon in table cells

* Add Select component

* Change class order

* Draft

* Fix run status type errors

* Simplify Select

* Adjust styles

* Fixes

* Replace icon by remix

* Remove fragment

* Add styles

* Remove code from next PR

---------

Co-authored-by: Aaron Harper <aaron@inngest.com>
  • Loading branch information
anafilipadealmeida and amh4r authored Apr 30, 2024
1 parent 3c0fb1a commit 3b3e685
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Fragment, useMemo } from 'react';
import { FunctionRunStatusIcon } from '@inngest/components/FunctionRunStatusIcon';
import { Skeleton } from '@inngest/components/Skeleton';
import { IDCell, StatusCell, TextCell, TimeCell } from '@inngest/components/Table';
import { Time } from '@inngest/components/Time';
Expand Down Expand Up @@ -166,9 +165,7 @@ const columns = [

return (
<div className="flex items-center">
<StatusCell status={status}>
<FunctionRunStatusIcon status={status} className="h-5 w-5" />
</StatusCell>
<StatusCell status={status} />
</div>
);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
'use client';

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 { RiLoopLeftLine } from '@remixicon/react';
import { useQuery } from 'urql';

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

const GetRunsDocument = graphql(`
query GetRuns($environmentID: ID!, $startTime: Time!, $status: [FunctionRunStatus!]) {
Expand Down Expand Up @@ -38,12 +40,16 @@ const renderSubComponent = ({ id }: { id: string }) => {
};

export default function RunsPage() {
const [filteredStatus, setFilteredStatus, removeFilteredStatus] =
const [rawFilteredStatus, setFilteredStatus, removeFilteredStatus] =
useStringArraySearchParam('filterStatus');

function handleStatusesChange(statuses: FunctionRunStatus[]) {
if (statuses.length > 0) {
setFilteredStatus(statuses);
const filteredStatus = useMemo(() => {
return toRunStatuses(rawFilteredStatus ?? []);
}, [rawFilteredStatus]);

function handleStatusesChange(value: FunctionRunStatus[]) {
if (value.length > 0) {
setFilteredStatus(value);
} else {
removeFilteredStatus();
}
Expand All @@ -55,7 +61,7 @@ export default function RunsPage() {
variables: {
environmentID: environment.id,
startTime: '2024-04-19T11:26:03.203Z',
status: filteredStatus ? (filteredStatus as FunctionRunStatus[]) : null,
status: filteredStatus.length > 0 ? filteredStatus : null,
},
});

Expand All @@ -72,11 +78,8 @@ export default function RunsPage() {

return (
<main className="h-full min-h-0 overflow-y-auto bg-white">
<div className="flex justify-between gap-2 bg-slate-50 px-8 py-2">
<StatusFilter
selectedStatuses={filteredStatus ? (filteredStatus as FunctionRunStatus[]) : []}
onStatusesChange={handleStatusesChange}
/>
<div className="flex items-center justify-between gap-2 bg-slate-50 px-8 py-2">
<StatusFilter selectedStatuses={filteredStatus} onStatusesChange={handleStatusesChange} />
{/* 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
@@ -0,0 +1,40 @@
import { isFunctionRunStatus, type FunctionRunStatus } from '@inngest/components/types/functionRun';

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

/**
* Convert a run status union type into an enum. This is necessary because
* TypeScript treats as enums as nominal types, which causes silly type errors.
*/
function toRunStatus(status: FunctionRunStatus): FunctionRunStatusEnum {
switch (status) {
case 'CANCELLED':
return FunctionRunStatusEnum.Cancelled;
case 'COMPLETED':
return FunctionRunStatusEnum.Completed;
case 'FAILED':
return FunctionRunStatusEnum.Failed;
case 'QUEUED':
return FunctionRunStatusEnum.Queued;
case 'RUNNING':
return FunctionRunStatusEnum.Running;
}
}

/**
* Convert a run status string array into an enum array. Unrecognized statuses
* are logged and will not be in the returned array.
*/
export function toRunStatuses(statuses: string[]): FunctionRunStatusEnum[] {
const newValue: FunctionRunStatusEnum[] = [];

for (const status of statuses) {
if (isFunctionRunStatus(status)) {
newValue.push(toRunStatus(status));
} else {
console.error(`unexpected status: ${status}`);
}
}

return newValue;
}
1 change: 1 addition & 0 deletions ui/packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"test": "vitest"
},
"dependencies": {
"@headlessui/react": "1.7.18",
"@headlessui/tailwindcss": "0.1.2",
"@heroicons/react": "2.1.1",
"@monaco-editor/react": "4.6.0",
Expand Down
78 changes: 78 additions & 0 deletions ui/packages/components/src/Filter/StatusFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { RunStatusIcon, statusStyles } from '../FunctionRunStatusIcon/RunStatusIcons';
import { Select } from '../Select/Select';
import {
functionRunStatuses,
isFunctionRunStatus,
type FunctionRunStatus,
} from '../types/functionRun';
import { cn } from '../utils/classNames';

type StatusFilterProps = {
selectedStatuses: FunctionRunStatus[];
onStatusesChange: (value: FunctionRunStatus[]) => void;
};

export default function StatusFilter({ selectedStatuses, onStatusesChange }: StatusFilterProps) {
const statusDots = selectedStatuses.map((status) => {
const isSelected = selectedStatuses.includes(status);
return (
<span
key={status}
className={cn(
'inline-block h-[9px] w-[9px] flex-shrink-0 rounded-full border border-slate-50 bg-slate-50 ring-1 ring-inset ring-slate-300 group-hover:border-slate-100 [&:not(:first-child)]:-ml-1',
isSelected && [statusStyles[status], 'ring-0']
)}
aria-hidden="true"
/>
);
});

return (
<Select
defaultValue={selectedStatuses}
onChange={(value: string[]) => {
const newValue: FunctionRunStatus[] = [];
value.forEach((status) => {
if (isFunctionRunStatus(status)) {
newValue.push(status);
} else {
console.error(`invalid status: ${status}`);
}
});
onStatusesChange(newValue);
}}
label="Status"
>
<Select.Button>{statusDots}</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.Options>
</Select>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Props = {
className?: string;
};

/** @deprecated For new designs use RunStatusIcons instead. */
export function FunctionRunStatusIcon({ status, className }: Props) {
const Icon = icons[status] ?? IconStatusQueued;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { type FunctionRunStatus } from '@inngest/components/types/functionRun';

import { cn } from '../utils/classNames';

export const statusStyles: Record<string, string> = {
CANCELLED: 'bg-slate-300 border-slate-300',
COMPLETED: 'bg-teal-500 border-teal-500',
FAILED: 'bg-rose-500 border-rose-500',
RUNNING: 'bg-blue-200 border-sky-500',
QUEUED: 'bg-amber-100 border-amber-500',
} as const satisfies { [key in FunctionRunStatus]: string };

type Props = {
status: FunctionRunStatus;
className?: string;
};

export function RunStatusIcon({ status, className }: Props) {
const style = statusStyles[status] ?? statusStyles['CANCELLED'];

const title = 'Function ' + status.toLowerCase();
return <div className={cn('h-3.5 w-3.5 rounded-full border', style, className)} title={title} />;
}
82 changes: 82 additions & 0 deletions ui/packages/components/src/Select/Select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Listbox } from '@headlessui/react';
import { RiArrowDownSLine } from '@remixicon/react';

import { cn } from '../utils/classNames';

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

export function Select({
defaultValue,
label,
isLabelVisible = true,
children,
onChange,
}: SelectProps) {
return (
<Listbox value={defaultValue} onChange={onChange} 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')}
>
{label}
</Listbox.Label>
<span className="relative">{children}</span>
</span>
</Listbox>
);
}

function Button({
children,
isLabelVisible,
}: React.PropsWithChildren<{ isLabelVisible?: boolean }>) {
return (
<Listbox.Button
className={cn(
!isLabelVisible && 'rounded-l-[5px]',
'flex items-center rounded-r-[5px] bg-white px-2 py-3'
)}
>
{children}
<RiArrowDownSLine className="h-4 w-4 text-slate-500" aria-hidden="true" />
</Listbox.Button>
);
}

function Options({ children }: React.PropsWithChildren) {
return (
<Listbox.Options className="absolute mt-1 min-w-max">
<div className="overflow-hidden rounded-md border border-slate-200 bg-white py-1 drop-shadow-lg">
{children}
</div>
</Listbox.Options>
);
}

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"
key={option}
value={option}
>
{children}
</Listbox.Option>
);
}

Select.Button = Button;
Select.Options = Options;
Select.Option = Option;
Select.CustomOption = Listbox.Option;
20 changes: 15 additions & 5 deletions ui/packages/components/src/Table/Cell.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { cn } from '../utils/classNames';
import { RunStatusIcon } from '@inngest/components/FunctionRunStatusIcon/RunStatusIcons';
import { type FunctionRunStatus } from '@inngest/components/types/functionRun';
import { cn } from '@inngest/components/utils/classNames';

const cellStyles = 'text-slate-950 text-sm';

Expand All @@ -15,12 +17,20 @@ export function TimeCell({ children }: React.PropsWithChildren) {
return <span className={cn(cellStyles, 'font-medium')}>{children}</span>;
}

export function StatusCell({ status, children }: React.PropsWithChildren<{ status: string }>) {
// TODO: Use new runs circles and colors instead of passing FunctionRunStatusIcon as children
export function StatusCell({ status }: React.PropsWithChildren<{ status: FunctionRunStatus }>) {
const statusStyles: Record<string, string> = {
CANCELLED: 'text-neutral-600',
COMPLETED: 'text-teal-700',
FAILED: 'text-rose-500',
RUNNING: 'text-sky-500',
QUEUED: 'text-amber-500',
} as const satisfies { [key in FunctionRunStatus]: string };
const style = statusStyles[status] ?? statusStyles['CANCELLED'];

return (
<div className={cn(cellStyles, 'flex items-center gap-2.5 font-medium')}>
{children}
<p className="lowercase first-letter:capitalize">{status}</p>
<RunStatusIcon status={status} />
<p className={cn(style, 'lowercase first-letter:capitalize')}>{status}</p>
</div>
);
}
8 changes: 7 additions & 1 deletion ui/packages/components/src/types/functionRun.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import type { RawHistoryItem } from '@inngest/components/utils/historyParser';

const functionRunStatuses = ['CANCELLED', 'COMPLETED', 'FAILED', 'RUNNING', 'QUEUED'] as const;
export const functionRunStatuses = [
'FAILED',
'RUNNING',
'QUEUED',
'COMPLETED',
'CANCELLED',
] as const;
const FunctionRunEndedStatuses = ['CANCELLED', 'COMPLETED', 'FAILED'] as const;
export type FunctionRunStatus = (typeof functionRunStatuses)[number];
export type FunctionRunEndStatus = (typeof FunctionRunEndedStatuses)[number];
Expand Down
Loading

0 comments on commit 3b3e685

Please sign in to comment.