From 60c3df4dad7d2abb123e814a3a84e5365a9b778b Mon Sep 17 00:00:00 2001 From: breeg554 Date: Fri, 11 Oct 2024 17:14:52 +0200 Subject: [PATCH] extract log components and refactor run logs page --- .../pages/pipelines/components/RunLogs.tsx | 156 ++++++++++++++ .../pages/pipelines/runLogs/loader.server.tsx | 14 +- .../pages/pipelines/runLogs/page.tsx | 196 +++++------------- .../pipelines/runLogs/runLogs.reducer.ts | 56 +++++ .../components/pagination/LoadMoreButton.tsx | 4 +- 5 files changed, 277 insertions(+), 149 deletions(-) create mode 100644 apps/web-remix/app/components/pages/pipelines/components/RunLogs.tsx create mode 100644 apps/web-remix/app/components/pages/pipelines/runLogs/runLogs.reducer.ts diff --git a/apps/web-remix/app/components/pages/pipelines/components/RunLogs.tsx b/apps/web-remix/app/components/pages/pipelines/components/RunLogs.tsx new file mode 100644 index 000000000..b80d4f044 --- /dev/null +++ b/apps/web-remix/app/components/pages/pipelines/components/RunLogs.tsx @@ -0,0 +1,156 @@ +import { useEffect, useReducer } from 'react'; +import { useInView } from 'react-intersection-observer'; +import { useFetcher } from '@remix-run/react'; + +import type { IPipelineRunLog } from '~/api/pipeline/pipeline.contracts'; +import { SelectInput } from '~/components/form/inputs/select/select.input'; +import type { loader } from '~/components/pages/pipelines/runLogs/loader.server'; +import { + fetchOlder, + log, + runLogsReducer, +} from '~/components/pages/pipelines/runLogs/runLogs.reducer'; +import { usePipelineRunLogs } from '~/components/pages/pipelines/usePipelineRunLogs'; +import { LoadMoreButton } from '~/components/pagination/LoadMoreButton'; +import { routes } from '~/utils/routes.utils'; +import { buildUrlWithParams } from '~/utils/url'; + +interface LogsProps { + defaultLogs: IPipelineRunLog[]; + defaultAfter?: string | null; + blockName?: string; + runId: number; + pipelineId: number; + organizationId: number; +} + +export function RunLogs({ + defaultLogs, + defaultAfter, + blockName, + pipelineId, + organizationId, + runId, +}: LogsProps) { + const fetcher = useFetcher(); + const { ref: fetchNextRef, inView } = useInView(); + + const [state, dispatch] = useReducer(runLogsReducer, { + after: defaultAfter ?? null, + logs: defaultLogs.slice().reverse(), + }); + + const onLogs = (payload: any) => { + dispatch(log(payload.data)); + }; + + const onError = (error: string) => { + console.error(error); + }; + + const { status, listenToLogs, stopListening } = usePipelineRunLogs( + organizationId, + pipelineId, + runId, + () => {}, + onLogs, + onError, + ); + + const fetchNextPage = () => { + fetcher.load( + buildUrlWithParams( + routes.pipelineRunLogs(organizationId, pipelineId, runId), + { + after: state.after ?? undefined, + block_name: blockName, + }, + ), + ); + }; + + useEffect(() => { + if (fetcher.state === 'idle' && fetcher.data) { + dispatch(fetchOlder(fetcher.data.logs, fetcher.data.pagination.after)); + } + }, [fetcher.data]); + + useEffect(() => { + if (inView && state.after && fetcher.state === 'idle') { + fetchNextPage(); + } + }, [inView, state.after]); + + useEffect(() => { + if (status === 'open') { + listenToLogs({ + block_name: blockName, + }); + } + + return () => { + stopListening(); + }; + }, [status]); + + return ( +
+
    + {state.logs + .map((log) => ( +
  • + +
  • + )) + .slice() + .reverse()} +
+ +
+ +
+
+ ); +} + +interface LogsFilterProps { + value: string | null | undefined; + onSelect: (blockName: string) => void; + onClear: () => void; + options: { value: string; label: string }[]; +} + +export function RunLogsFilter({ + value, + onClear, + onSelect, + options, +}: LogsFilterProps) { + return ( + + ); +} + +const Log = ({ log }: { log: any }) => { + return ( +

+ {log?.created_at} + {log?.context} + {log?.block_name} + {log?.message} + {log?.message_types?.join(' -> ')} +

+ ); +}; diff --git a/apps/web-remix/app/components/pages/pipelines/runLogs/loader.server.tsx b/apps/web-remix/app/components/pages/pipelines/runLogs/loader.server.tsx index 19aaf3057..4d2ddc6fb 100644 --- a/apps/web-remix/app/components/pages/pipelines/runLogs/loader.server.tsx +++ b/apps/web-remix/app/components/pages/pipelines/runLogs/loader.server.tsx @@ -50,16 +50,18 @@ export async function loader(args: LoaderFunctionArgs) { }, ); - const [{ pipeline }, { pipelineRun }, pipelineRunLogs] = await Promise.all([ - pipelinePromise, - pipelineRunPromise, - pipelineRunLogsPromise, - ]); + const [{ pipeline }, { pipelineRun }, { data: pipelineRunLogs }] = + await Promise.all([ + pipelinePromise, + pipelineRunPromise, + pipelineRunLogsPromise, + ]); return json({ pipeline, pipelineRun, - pipelineRunLogs: pipelineRunLogs.data, + logs: pipelineRunLogs.data, + pagination: pipelineRunLogs.meta, blockName, }); })(args); diff --git a/apps/web-remix/app/components/pages/pipelines/runLogs/page.tsx b/apps/web-remix/app/components/pages/pipelines/runLogs/page.tsx index 53d03f2de..c9834bda6 100644 --- a/apps/web-remix/app/components/pages/pipelines/runLogs/page.tsx +++ b/apps/web-remix/app/components/pages/pipelines/runLogs/page.tsx @@ -1,48 +1,27 @@ -import { useEffect, useState } from 'react'; -import { useInView } from 'react-intersection-observer'; +import { useMemo } from 'react'; import type { MetaFunction } from '@remix-run/node'; -import { useFetcher, useLoaderData } from '@remix-run/react'; +import { useLoaderData, useNavigate } from '@remix-run/react'; -import type { IPipelineRunLog } from '~/api/pipeline/pipeline.contracts'; -import { SelectInput } from '~/components/form/inputs/select/select.input'; import { PageContentWrapper } from '~/components/layout/PageContentWrapper'; -import { LoadMoreButton } from '~/components/pagination/LoadMoreButton'; +import { BasicLink } from '~/components/link/BasicLink'; +import { EmptyMessage } from '~/components/list/ItemList'; +import { + RunLogs, + RunLogsFilter, +} from '~/components/pages/pipelines/components/RunLogs'; import { Label } from '~/components/ui/label'; import { metaWithDefaults } from '~/utils/metadata'; import { routes } from '~/utils/routes.utils'; import { buildUrlWithParams } from '~/utils/url'; -import { usePipelineRunLogs } from '../usePipelineRunLogs'; import type { loader } from './loader.server'; export function PipelineRunLogs() { - const fetcher = useFetcher(); - const { ref: fetchNextRef, inView } = useInView(); - const { pipeline, pipelineRun, pipelineRunLogs, blockName } = + const navigate = useNavigate(); + const { pipeline, blockName, pipelineRun, pagination, logs } = useLoaderData(); - const [liveLogs, setLiveLogs] = useState([]); - const [data, setData] = useState(pipelineRunLogs.data); - const [after, setAfter] = useState( - pipelineRunLogs.meta.after, - ); - const [selectedBlock, setSelectedBlock] = useState( - blockName, - ); - - const { status, listenToLogs, stopListening } = usePipelineRunLogs( - pipeline.organization_id, - pipeline.id, - pipelineRun.id, - () => {}, - (payload) => { - setLiveLogs((prev) => [...prev, payload.data]); - }, - (error) => console.error(error), - ); - - const fetchNextPage = () => { - if (fetcher.state !== 'idle') return; + const onFilter = (blockName?: string) => { const urlWithParams = buildUrlWithParams( routes.pipelineRunLogs( pipeline.organization_id, @@ -50,135 +29,68 @@ export function PipelineRunLogs() { pipelineRun.id, ), { - after: after ?? undefined, - block_name: selectedBlock ?? undefined, + block_name: blockName, }, ); - fetcher.load(urlWithParams); + navigate(urlWithParams); }; - useEffect(() => { - const newData = (fetcher.data as any)?.pipelineRunLogs; + const onBlockSelect = (blockName: string) => { + onFilter(blockName); + }; - if (newData) { - setAfter(newData.meta.after); - if (newData.data.length > 0) { - setData((prev) => [...prev, ...newData.data]); - } - } - }, [fetcher.data]); + const onClear = () => { + onFilter(); + }; - useEffect(() => { - if (inView && after) { - fetchNextPage(); - } - }, [inView, after]); + const options = useMemo(() => { + return pipelineRun.config.blocks.map((block) => ({ + value: block.name, + label: block.name, + })); + }, [pipelineRun.config.blocks]); - useEffect(() => { - if (status === 'open') { - listenToLogs({ - block_name: blockName, - }); - } - }, [status]); - - if (data.length === 0 && liveLogs.length === 0) { + if (pagination.total === 0) { return ( - -

- No logs found for this run. You can enable logs in the pipeline - settings. -

-
+ + No logs found for this run. You can enable logs in the workflow{' '} + + settings + + . + ); } return ( - - { - const urlWithParams = buildUrlWithParams( - routes.pipelineRunLogs( - pipeline.organization_id, - pipeline.id, - pipelineRun.id, - ), - ); - - stopListening().then(() => - listenToLogs({ - block_name: undefined, - }), - ); - setLiveLogs([]); - setData([]); - setSelectedBlock(undefined); - - fetcher.load(urlWithParams); - }} - placeholder="Select..." - options={pipelineRun.config.blocks.map((block) => ({ - id: block.name, - value: block.name, - label: block.name, - }))} - onSelect={(selected: any) => { - const urlWithParams = buildUrlWithParams( - routes.pipelineRunLogs( - pipeline.organization_id, - pipeline.id, - pipelineRun.id, - ), - { - block_name: selected ?? undefined, - }, - ); - - stopListening().then(() => - listenToLogs({ - block_name: selected ?? undefined, - }), - ); - setLiveLogs([]); - setData([]); - setSelectedBlock(selected); - - fetcher.load(urlWithParams); - }} + + + - -
- \{liveLogs.map((log) => ).reverse()} - {data.map((log) => ( - - ))} -
- -
-
); } -const Log = ({ log }: { log: any }) => { - return ( -

- {log?.created_at} - {log?.context} - {log?.block_name} - {log?.message} - {log?.message_types?.join(' -> ')} -

- ); -}; - export const meta: MetaFunction = metaWithDefaults(() => { return [ { diff --git a/apps/web-remix/app/components/pages/pipelines/runLogs/runLogs.reducer.ts b/apps/web-remix/app/components/pages/pipelines/runLogs/runLogs.reducer.ts new file mode 100644 index 000000000..a4cc9ccf7 --- /dev/null +++ b/apps/web-remix/app/components/pages/pipelines/runLogs/runLogs.reducer.ts @@ -0,0 +1,56 @@ +import type { IPipelineRunLog } from '~/api/pipeline/pipeline.contracts'; + +type Action = + | { + type: 'LOG'; + payload: { + data: IPipelineRunLog; + }; + } + | { + type: 'FETCH_OLDER'; + payload: { + data: IPipelineRunLog[]; + after: string | null; + }; + }; + +export type RunLogsReducerState = { + logs: IPipelineRunLog[]; + after: string | null; +}; + +export const runLogsReducer = ( + state: RunLogsReducerState, + action: Action, +): RunLogsReducerState => { + switch (action.type) { + case 'LOG': + return { ...state, logs: [...state.logs, action.payload.data] }; + case 'FETCH_OLDER': + return { + ...state, + logs: [...action.payload.data.slice().reverse(), ...state.logs], + after: action.payload.after, + }; + } +}; + +export const log = (log: IPipelineRunLog) => { + return { + type: 'LOG', + payload: { + data: log, + }, + } as const; +}; + +export const fetchOlder = (logs: IPipelineRunLog[], after?: string | null) => { + return { + type: 'FETCH_OLDER', + payload: { + data: logs, + after: after ?? null, + }, + } as const; +}; diff --git a/apps/web-remix/app/components/pagination/LoadMoreButton.tsx b/apps/web-remix/app/components/pagination/LoadMoreButton.tsx index 6abece928..dcbb3f9ad 100644 --- a/apps/web-remix/app/components/pagination/LoadMoreButton.tsx +++ b/apps/web-remix/app/components/pagination/LoadMoreButton.tsx @@ -7,6 +7,7 @@ interface LoadMoreButtonProps { hasNextPage: boolean; isFetching: boolean; className?: string; + disabled?: boolean; } export const LoadMoreButton: React.FC = ({ @@ -14,11 +15,12 @@ export const LoadMoreButton: React.FC = ({ onClick, hasNextPage, className, + disabled, }) => { return (