FE Polish V: Searchable workflows, additional metadata tab (#2342)

* feat: search for workflows

* fix: allow fts for workflow name

* feat: add back metadata tab to details

* feat: dynamic copy

* chore: lint
This commit is contained in:
matt
2025-09-25 10:58:48 -04:00
committed by GitHub
parent 1ee77b4f42
commit d6afe55338
7 changed files with 107 additions and 36 deletions

View File

@@ -451,4 +451,12 @@ pre.shiki > code > span::before {
align-items: center;
width: auto;
}
.side-sm\:hidden {
display: none;
}
.side-sm\:block {
display: block;
}
}

View File

@@ -37,6 +37,7 @@ export enum TabOption {
Input = 'input',
Logs = 'logs',
Waterfall = 'waterfall',
AdditionalMetadata = 'additional-metadata',
}
interface TaskRunDetailProps {
@@ -220,6 +221,18 @@ export const TaskRunDetail = ({
<TabsTrigger variant="underlined" value={TabOption.Logs}>
Logs
</TabsTrigger>
<TabsTrigger
variant="underlined"
value={TabOption.AdditionalMetadata}
className="side-responsive-layout"
>
<span className="flex side-responsive-inner">
<span className="block side-sm:hidden">Metadata</span>
<span className="hidden side-sm:block">
Additional Metadata
</span>
</span>
</TabsTrigger>
</TabsList>
<TabsContent value={TabOption.Output}>
<V1StepRunOutput taskRunId={taskRunId} />
@@ -256,6 +269,15 @@ export const TaskRunDetail = ({
<TabsContent value={TabOption.Logs}>
<StepRunLogs taskRun={taskRun} />
</TabsContent>
<TabsContent value={TabOption.AdditionalMetadata}>
<CodeHighlighter
className="my-4 h-[400px] max-h-[400px] overflow-y-auto"
maxHeight="400px"
minHeight="400px"
language="json"
code={JSON.stringify(taskRun.additionalMetadata ?? {}, null, 2)}
/>
</TabsContent>
</Tabs>
</TabsContent>
{isStandaloneTaskRun && (

View File

@@ -3,11 +3,21 @@ import { queries } from '@/lib/api';
import { usePagination } from '@/hooks/use-pagination';
import { useCurrentTenantId } from '@/hooks/use-tenant';
import { useRefetchInterval } from '@/contexts/refetch-interval-context';
import { z } from 'zod';
import { useZodColumnFilters } from '@/hooks/use-zod-column-filters';
import { nameKey } from '../components/workflow-columns';
import { useDebounce } from 'use-debounce';
type UseWorkflowsProps = {
key: string;
};
const workflowQuerySchema = z
.object({
s: z.string().optional(), // search
})
.default({ s: undefined });
export const useWorkflows = ({ key }: UseWorkflowsProps) => {
const { tenantId } = useCurrentTenantId();
const { refetchInterval } = useRefetchInterval();
@@ -16,10 +26,22 @@ export const useWorkflows = ({ key }: UseWorkflowsProps) => {
key,
});
const paramKey = `workflows-${key}`;
const {
state: { s: search },
columnFilters,
setColumnFilters,
resetFilters,
} = useZodColumnFilters(workflowQuerySchema, paramKey, { s: nameKey });
const [debouncedSearch] = useDebounce(search, 300);
const listWorkflowQuery = useQuery({
...queries.workflows.list(tenantId, {
limit,
offset,
name: debouncedSearch,
}),
refetchInterval,
placeholderData: (data) => data,
@@ -38,5 +60,8 @@ export const useWorkflows = ({ key }: UseWorkflowsProps) => {
pagination,
setPagination,
setPageSize,
columnFilters,
setColumnFilters,
resetFilters,
};
};

View File

@@ -1,22 +1,21 @@
import { useState } from 'react';
import { DataTable } from '@/components/v1/molecules/data-table/data-table.tsx';
import { Loading } from '@/components/v1/ui/loading.tsx';
import { SortingState, VisibilityState } from '@tanstack/react-table';
import { VisibilityState } from '@tanstack/react-table';
import { useCurrentTenantId } from '@/hooks/use-tenant';
import { columns, WorkflowColumn } from './components/workflow-columns';
import {
columns,
nameKey,
WorkflowColumn,
} from './components/workflow-columns';
import { useWorkflows } from './hooks/use-workflows';
import { DocsButton } from '@/components/v1/docs/docs-button';
import { docsPages } from '@/lib/generated/docs';
import { ToolbarType } from '@/components/v1/molecules/data-table/data-table-toolbar';
export default function WorkflowTable() {
const { tenantId } = useCurrentTenantId();
const [sorting, setSorting] = useState<SortingState>([
{
id: 'createdAt',
desc: true,
},
]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const {
@@ -28,6 +27,9 @@ export default function WorkflowTable() {
setPagination,
setPageSize,
refetch,
columnFilters,
setColumnFilters,
resetFilters,
} = useWorkflows({
key: 'workflows-table',
});
@@ -53,6 +55,13 @@ export default function WorkflowTable() {
</div>
</div>
}
filters={[
{
columnId: nameKey,
title: WorkflowColumn.name,
type: ToolbarType.Search,
},
]}
columnVisibility={columnVisibility}
setColumnVisibility={setColumnVisibility}
pagination={pagination}
@@ -60,17 +69,16 @@ export default function WorkflowTable() {
onSetPageSize={setPageSize}
showSelectedRows={false}
pageCount={numWorkflows}
sorting={sorting}
setSorting={setSorting}
isLoading={isLoading}
manualSorting={false}
manualFiltering={false}
showColumnToggle={true}
columnKeyToName={WorkflowColumn}
refetchProps={{
isRefetching,
onRefetch: refetch,
}}
columnFilters={columnFilters}
setColumnFilters={setColumnFilters}
onResetFilters={resetFilters}
/>
);
}

View File

@@ -1,14 +1,12 @@
-- name: CountWorkflows :one
SELECT
count(workflows) OVER() AS total
FROM
"Workflow" as workflows
SELECT COUNT(w.*)
FROM "Workflow" w
WHERE
workflows."tenantId" = $1 AND
workflows."deletedAt" IS NULL AND
(
sqlc.narg('eventKey')::text IS NULL OR
workflows."id" IN (
w."tenantId" = $1
AND w."deletedAt" IS NULL
AND (
sqlc.narg('eventKey')::TEXT IS NULL OR
w."id" IN (
SELECT
DISTINCT ON(t1."workflowId") t1."workflowId"
FROM
@@ -31,7 +29,12 @@ WHERE
ORDER BY
t1."workflowId" DESC, t1."order" DESC
)
);
)
AND (
sqlc.narg('search')::TEXT IS NULL
OR w.name ILIKE CONCAT('%', sqlc.narg('search')::TEXT, '%')
)
;
-- name: ListWorkflowsLatestRuns :many
SELECT

View File

@@ -181,16 +181,14 @@ func (q *Queries) CountWorkflowRunsRoundRobin(ctx context.Context, db DBTX, arg
}
const countWorkflows = `-- name: CountWorkflows :one
SELECT
count(workflows) OVER() AS total
FROM
"Workflow" as workflows
SELECT COUNT(w.*)
FROM "Workflow" w
WHERE
workflows."tenantId" = $1 AND
workflows."deletedAt" IS NULL AND
(
$2::text IS NULL OR
workflows."id" IN (
w."tenantId" = $1
AND w."deletedAt" IS NULL
AND (
$2::TEXT IS NULL OR
w."id" IN (
SELECT
DISTINCT ON(t1."workflowId") t1."workflowId"
FROM
@@ -214,18 +212,23 @@ WHERE
t1."workflowId" DESC, t1."order" DESC
)
)
AND (
$3::TEXT IS NULL
OR w.name ILIKE CONCAT('%', $3::TEXT, '%')
)
`
type CountWorkflowsParams struct {
TenantId pgtype.UUID `json:"tenantId"`
EventKey pgtype.Text `json:"eventKey"`
Search pgtype.Text `json:"search"`
}
func (q *Queries) CountWorkflows(ctx context.Context, db DBTX, arg CountWorkflowsParams) (int64, error) {
row := db.QueryRow(ctx, countWorkflows, arg.TenantId, arg.EventKey)
var total int64
err := row.Scan(&total)
return total, err
row := db.QueryRow(ctx, countWorkflows, arg.TenantId, arg.EventKey, arg.Search)
var count int64
err := row.Scan(&count)
return count, err
}
const createJob = `-- name: CreateJob :one

View File

@@ -62,7 +62,9 @@ func (r *workflowAPIRepository) ListWorkflows(tenantId string, opts *repository.
}
if opts.Name != nil {
queryParams.Search = pgtype.Text{String: *opts.Name, Valid: true}
search := pgtype.Text{String: *opts.Name, Valid: true}
queryParams.Search = search
countParams.Search = search
}
orderByField := "createdAt"