Skip to content

Commit

Permalink
Fill out Dag Run and Task Instance Details pages with Grid and Gantt …
Browse files Browse the repository at this point in the history
…buttons. (apache#44656)

* Rebasing

* Handle mapped instances

* Handle null in Status component
  • Loading branch information
bbovenzi authored Dec 10, 2024
1 parent c291695 commit e122b20
Show file tree
Hide file tree
Showing 49 changed files with 1,078 additions and 322 deletions.
2 changes: 1 addition & 1 deletion airflow/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.3.1",
"react-hook-form": "^7.20.0",
"react-icons": "^5.3.0",
"react-icons": "^5.4.0",
"react-markdown": "^9.0.1",
"react-router-dom": "^6.26.2",
"react-syntax-highlighter": "^15.5.6",
Expand Down
10 changes: 5 additions & 5 deletions airflow/ui/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions airflow/ui/src/components/Stat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Heading, VStack } from "@chakra-ui/react";
import type { PropsWithChildren } from "react";

type Props = {
readonly label: string;
} & PropsWithChildren;

export const Stat = ({ children, label }: Props) => (
<VStack align="flex-start" gap={1}>
<Heading color="fg.muted" fontSize="xs">
{label}
</Heading>
{children}
</VStack>
);
55 changes: 55 additions & 0 deletions airflow/ui/src/components/ui/Breadcrumb/Root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Breadcrumb, type SystemStyleObject } from "@chakra-ui/react";
import React from "react";

export type BreadcrumbRootProps = {
separator?: React.ReactNode;
separatorGap?: SystemStyleObject["gap"];
} & Breadcrumb.RootProps;

export const Root = React.forwardRef<HTMLDivElement, BreadcrumbRootProps>(
(props, ref) => {
const { children, separator, separatorGap, ...rest } = props;

const validChildren = React.Children.toArray(children).filter(
React.isValidElement,
);

return (
<Breadcrumb.Root ref={ref} {...rest}>
<Breadcrumb.List gap={separatorGap}>
{validChildren.map((child, index) => {
const last = index === validChildren.length - 1;

return (
// eslint-disable-next-line react/no-array-index-key
<React.Fragment key={index}>
<Breadcrumb.Item>{child}</Breadcrumb.Item>
{!last && (
<Breadcrumb.Separator>{separator}</Breadcrumb.Separator>
)}
</React.Fragment>
);
})}
</Breadcrumb.List>
</Breadcrumb.Root>
);
},
);
26 changes: 26 additions & 0 deletions airflow/ui/src/components/ui/Breadcrumb/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Breadcrumb as ChakraBreadcrumb } from "@chakra-ui/react";

import { Root } from "./Root";

export const Breadcrumb = {
...ChakraBreadcrumb,
Root,
};
5 changes: 3 additions & 2 deletions airflow/ui/src/components/ui/Status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ import { stateColor } from "src/utils/stateColor";
type StatusValue = DagRunState | TaskInstanceState;

export type StatusProps = {
state?: StatusValue;
state: StatusValue | null;
} & ChakraStatus.RootProps;

export const Status = React.forwardRef<HTMLDivElement, StatusProps>(
({ children, state, ...rest }, ref) => {
const colorPalette = state === undefined ? "info" : stateColor[state];
// "null" is actually a string on stateColor
const colorPalette = stateColor[state ?? "null"];

return (
<ChakraStatus.Root ref={ref} {...rest}>
Expand Down
1 change: 1 addition & 0 deletions airflow/ui/src/components/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ export * from "./Accordion";
export * from "./Status";
export * from "./Button";
export * from "./Toaster";
export * from "./Breadcrumb";
105 changes: 105 additions & 0 deletions airflow/ui/src/layouts/Details/DagVizModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Button, Heading, HStack } from "@chakra-ui/react";
import { FaChartGantt } from "react-icons/fa6";
import { FiGrid } from "react-icons/fi";
import { Link as RouterLink, useSearchParams } from "react-router-dom";

import type { DAGResponse } from "openapi/requests/types.gen";
import { DagIcon } from "src/assets/DagIcon";
import { Dialog } from "src/components/ui";
import { capitalize } from "src/utils";

import { Gantt } from "./Gantt";
import { Graph } from "./Graph";
import { Grid } from "./Grid";

type TriggerDAGModalProps = {
dagDisplayName?: DAGResponse["dag_display_name"];
dagId?: DAGResponse["dag_id"];
onClose: () => void;
open: boolean;
};

const visualizationOptions = [
{
component: <Gantt />,
icon: <FaChartGantt height={5} width={5} />,
value: "gantt",
},
{
component: <Graph />,
icon: <DagIcon height={5} width={5} />,
value: "graph",
},
{ component: <Grid />, icon: <FiGrid height={5} width={5} />, value: "grid" },
];

export const DagVizModal: React.FC<TriggerDAGModalProps> = ({
dagDisplayName,
dagId,
onClose,
open,
}) => {
const [searchParams] = useSearchParams();

const activeViz = searchParams.get("modal") ?? "graph";
const params = new URLSearchParams(searchParams);

params.delete("modal");

return (
<Dialog.Root onOpenChange={onClose} open={open} size="full">
<Dialog.Content backdrop>
<Dialog.Header bg="blue.muted">
<HStack>
<Heading mr={3} size="xl">
{dagDisplayName ?? dagId}
</Heading>
{visualizationOptions.map(({ icon, value }) => (
<RouterLink
key={value}
to={{
search: `${params.toString()}&modal=${value}`,
}}
>
<Button
borderColor="colorPalette.fg"
borderRadius={20}
colorPalette="blue"
variant={activeViz === value ? "solid" : "outline"}
>
{icon}
{capitalize(value)}
</Button>
</RouterLink>
))}
</HStack>
<Dialog.CloseTrigger closeButtonProps={{ size: "xl" }} />
</Dialog.Header>
<Dialog.Body display="flex">
{dagId === undefined
? undefined
: visualizationOptions.find((viz) => viz.value === activeViz)
?.component}
</Dialog.Body>
</Dialog.Content>
</Dialog.Root>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,62 +17,70 @@
* under the License.
*/
import { Box, Button } from "@chakra-ui/react";
import type { PropsWithChildren } from "react";
import { FiChevronsLeft } from "react-icons/fi";
import { Outlet, Link as RouterLink, useParams } from "react-router-dom";

import {
useDagServiceGetDagDetails,
useDagsServiceRecentDagRuns,
} from "openapi/queries";
Outlet,
Link as RouterLink,
useParams,
useSearchParams,
} from "react-router-dom";

import type { DAGResponse } from "openapi/requests/types.gen";
import { ErrorAlert } from "src/components/ErrorAlert";
import { ProgressBar } from "src/components/ui";
import { Toaster } from "src/components/ui";
import { OpenGroupsProvider } from "src/context/openGroups";

import { Header } from "./Header";
import { DagTabs } from "./Tabs";
import { DagVizModal } from "./DagVizModal";
import { NavTabs } from "./NavTabs";

type Props = {
readonly dag?: DAGResponse;
readonly error?: unknown;
readonly isLoading?: boolean;
readonly tabs: Array<{ label: string; value: string }>;
} & PropsWithChildren;

export const Dag = () => {
const { dagId } = useParams();
export const DetailsLayout = ({
children,
dag,
error,
isLoading,
tabs,
}: Props) => {
const { dagId = "" } = useParams();

const {
data: dag,
error,
isLoading,
} = useDagServiceGetDagDetails({
dagId: dagId ?? "",
});
const [searchParams, setSearchParams] = useSearchParams();

// TODO: replace with with a list dag runs by dag id request
const {
data: runsData,
error: runsError,
isLoading: isLoadingRuns,
} = useDagsServiceRecentDagRuns({ dagIdPattern: dagId ?? "" }, undefined, {
enabled: Boolean(dagId),
});
const modal = searchParams.get("modal");

const runs =
runsData?.dags.find((dagWithRuns) => dagWithRuns.dag_id === dagId)
?.latest_dag_runs ?? [];
const isModalOpen = modal !== null;
const onClose = () => {
searchParams.delete("modal");
setSearchParams(searchParams);
};

return (
<OpenGroupsProvider dagId={dagId ?? ""}>
<OpenGroupsProvider dagId={dagId}>
<Box>
<Toaster />
<Button asChild colorPalette="blue" variant="ghost">
<RouterLink to="/dags">
<FiChevronsLeft />
Back to all dags
</RouterLink>
</Button>
<Header dag={dag} dagId={dagId} latestRun={runs[0]} />
<ErrorAlert error={error ?? runsError} />
<ProgressBar
size="xs"
visibility={isLoading || isLoadingRuns ? "visible" : "hidden"}
<Toaster />
{children}
<ErrorAlert error={error} />
<ProgressBar size="xs" visibility={isLoading ? "visible" : "hidden"} />
<NavTabs tabs={tabs} />
<DagVizModal
dagDisplayName={dag?.dag_display_name}
dagId={dag?.dag_id}
onClose={onClose}
open={isModalOpen}
/>
<DagTabs dag={dag} />
</Box>
<Box overflow="auto">
<Outlet />
Expand Down
Loading

0 comments on commit e122b20

Please sign in to comment.