Skip to content

Commit

Permalink
Add Grid View to new UI (apache#45497)
Browse files Browse the repository at this point in the history
* Basic grid view

Add task selection

Refactor away from a table layout. Include pagination. Remove tooltips

Add date ticks, remove multiple nested tasks iteration

Add toggle groups, fix memos and date ticks

Add title as standin for tooltips

* Fix comment

* Rebase grid colors
  • Loading branch information
bbovenzi authored Jan 25, 2025
1 parent 2548731 commit 961ee6e
Show file tree
Hide file tree
Showing 17 changed files with 819 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,20 @@
* specific language governing permissions and limitations
* under the License.
*/
import { type LinkProps, Link, Text } from "@chakra-ui/react";
import { Text, type TextProps } from "@chakra-ui/react";
import type { CSSProperties } from "react";
import { FiArrowUpRight, FiArrowDownRight } from "react-icons/fi";
import { useParams, useSearchParams, Link as RouterLink } from "react-router-dom";

import type { NodeResponse } from "openapi/requests/types.gen";

type Props = {
readonly id: string;
export type TaskNameProps = {
readonly isGroup?: boolean;
readonly isMapped?: boolean;
readonly isOpen?: boolean;
readonly isZoomedOut?: boolean;
readonly label: string;
readonly setupTeardownType?: NodeResponse["setup_teardown_type"];
} & LinkProps;
} & TextProps;

const iconStyle: CSSProperties = {
display: "inline",
Expand All @@ -40,43 +38,31 @@ const iconStyle: CSSProperties = {
};

export const TaskName = ({
id,
isGroup = false,
isMapped = false,
isOpen = false,
isZoomedOut,
label,
setupTeardownType,
...rest
}: Props) => {
const { dagId = "", runId, taskId } = useParams();
const [searchParams] = useSearchParams();

}: TaskNameProps) => {
// We don't have a task group details page to link to
if (isGroup) {
return (
<Text fontSize="md" fontWeight="bold">
<Text fontSize="md" fontWeight="bold" {...rest}>
{label}
</Text>
);
}

return (
<Link asChild data-testid={id} fontSize={isZoomedOut ? "lg" : "md"} fontWeight="bold" {...rest}>
<RouterLink
to={{
// Do not include runId if there is no selected run, clicking a second time will deselect a task id
pathname: `/dags/${dagId}/${runId === undefined ? "" : `runs/${runId}/`}${taskId === id ? "" : `tasks/${id}`}`,
search: searchParams.toString(),
}}
>
{label}
{isMapped ? " [ ]" : undefined}
{setupTeardownType === "setup" && <FiArrowUpRight size={isZoomedOut ? 24 : 15} style={iconStyle} />}
{setupTeardownType === "teardown" && (
<FiArrowDownRight size={isZoomedOut ? 24 : 15} style={iconStyle} />
)}
</RouterLink>
</Link>
<Text fontSize={isZoomedOut ? "lg" : "md"} fontWeight="bold" {...rest}>
{label}
{isMapped ? " [ ]" : undefined}
{setupTeardownType === "setup" && <FiArrowUpRight size={isZoomedOut ? 24 : 15} style={iconStyle} />}
{setupTeardownType === "teardown" && (
<FiArrowDownRight size={isZoomedOut ? 24 : 15} style={iconStyle} />
)}
</Text>
);
};
6 changes: 4 additions & 2 deletions airflow/ui/src/layouts/Details/DagRunSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ export const DagRunSelect = forwardRef<HTMLDivElement>((_, ref) => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();

const offset = parseInt(searchParams.get("offset") ?? "0", 10);

const { data, isLoading } = useGridServiceGridData(
{
dagId,
limit: 14,
offset: 0,
limit: 25,
offset,
orderBy: "-start_date",
},
undefined,
Expand Down
2 changes: 2 additions & 0 deletions airflow/ui/src/layouts/Details/DagVizModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { DagRunSelect } from "./DagRunSelect";
import { Gantt } from "./Gantt";
import { Graph } from "./Graph";
import { Grid } from "./Grid";
import { ToggleGroups } from "./ToggleGroups";

type DAGVizModalProps = {
dagDisplayName?: DAGResponse["dag_display_name"];
Expand Down Expand Up @@ -89,6 +90,7 @@ export const DagVizModal: React.FC<DAGVizModalProps> = ({ dagDisplayName, dagId,
</RouterLink>
))}
<DagRunSelect ref={contentRef} />
<ToggleGroups />
</HStack>
<Dialog.CloseTrigger closeButtonProps={{ size: "xl" }} />
</Dialog.Header>
Expand Down
6 changes: 2 additions & 4 deletions airflow/ui/src/layouts/Details/DetailsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const DetailsLayout = ({ children, dag, error, isLoading, tabs }: Props)
<SearchDagsButton />
</HStack>
<Toaster />
{children}
{isModalOpen ? undefined : children}
<ErrorAlert error={error} />
<ProgressBar size="xs" visibility={isLoading ? "visible" : "hidden"} />
<NavTabs tabs={tabs} />
Expand All @@ -75,9 +75,7 @@ export const DetailsLayout = ({ children, dag, error, isLoading, tabs }: Props)
open={isModalOpen}
/>
</Box>
<Box overflow="auto">
<Outlet />
</Box>
<Box overflow="auto">{isModalOpen ? undefined : <Outlet />}</Box>
</OpenGroupsProvider>
);
};
2 changes: 1 addition & 1 deletion airflow/ui/src/layouts/Details/Graph/Graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export const Graph = () => {
const { data: gridData } = useGridServiceGridData(
{
dagId,
limit: 14,
limit: 25,
offset: 0,
orderBy: "-start_date",
},
Expand Down
50 changes: 50 additions & 0 deletions airflow/ui/src/layouts/Details/Graph/TaskLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*!
* 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 { Link } from "@chakra-ui/react";
import { useParams, useSearchParams, Link as RouterLink } from "react-router-dom";

import { TaskName, type TaskNameProps } from "src/components/TaskName";

type Props = {
readonly id: string;
} & TaskNameProps;

export const TaskLink = ({ id, isGroup, ...rest }: Props) => {
const { dagId = "", runId, taskId } = useParams();
const [searchParams] = useSearchParams();

// We don't have a task group details page to link to
if (isGroup) {
return <TaskName isGroup={true} {...rest} />;
}

return (
<Link asChild data-testid={id}>
<RouterLink
to={{
// Do not include runId if there is no selected run, clicking a second time will deselect a task id
pathname: `/dags/${dagId}/${runId === undefined ? "" : `runs/${runId}/`}${taskId === id ? "" : `tasks/${id}`}`,
search: searchParams.toString(),
}}
>
<TaskName {...rest} />
</RouterLink>
</Link>
);
};
4 changes: 2 additions & 2 deletions airflow/ui/src/layouts/Details/Graph/TaskNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { useOpenGroups } from "src/context/openGroups";
import { pluralize } from "src/utils";

import { NodeWrapper } from "./NodeWrapper";
import { TaskName } from "./TaskName";
import { TaskLink } from "./TaskLink";
import type { CustomNodeProps } from "./reactflowUtils";

export const TaskNode = ({
Expand Down Expand Up @@ -76,7 +76,7 @@ export const TaskNode = ({
width={`${width}px`}
>
<Box>
<TaskName
<TaskLink
id={id}
isGroup={isGroup}
isMapped={isMapped}
Expand Down
96 changes: 96 additions & 0 deletions airflow/ui/src/layouts/Details/Grid/Bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*!
* 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 { Flex, Box, VStack, Text } from "@chakra-ui/react";
import { useParams, useSearchParams } from "react-router-dom";

import { RunTypeIcon } from "src/components/RunTypeIcon";
import Time from "src/components/Time";

import { GridButton } from "./GridButton";
import { TaskInstances } from "./TaskInstances";
import type { GridTask, RunWithDuration } from "./utils";

const BAR_HEIGHT = 100;

type Props = {
readonly index: number;
readonly limit: number;
readonly max: number;
readonly nodes: Array<GridTask>;
readonly run: RunWithDuration;
};

export const Bar = ({ index, limit, max, nodes, run }: Props) => {
const { dagId = "", runId } = useParams();
const [searchParams] = useSearchParams();

const isSelected = runId === run.dag_run_id;

const search = searchParams.toString();

const shouldShowTick = index % 8 === 0 || index === limit - 1;

return (
<Box
_hover={{ bg: "blue.subtle" }}
bg={isSelected ? "blue.muted" : undefined}
position="relative"
transition="background-color 0.2s"
>
<Flex
alignItems="flex-end"
height={BAR_HEIGHT}
justifyContent="center"
pb="2px"
px="5px"
width="14px"
zIndex={1}
>
<GridButton
dagId={dagId}
height={`${(run.duration / max) * BAR_HEIGHT}px`}
justifyContent="flex-end"
label={run.dag_run_id}
minHeight="14px"
runId={run.dag_run_id}
searchParams={search}
state={run.state}
zIndex={1}
>
{run.run_type !== "scheduled" && <RunTypeIcon runType={run.run_type} size="8px" />}
</GridButton>
{shouldShowTick ? (
<VStack gap={0} left="8px" position="absolute" top={0} width={0} zIndex={-1}>
<Text
color="border.emphasized"
fontSize="xs"
mt="-35px !important"
transform="rotate(-40deg) translateX(28px)"
whiteSpace="nowrap"
>
<Time datetime={run.data_interval_start} format="MMM DD, HH:mm" />
</Text>
<Box borderLeftWidth={1} height="100px" opacity={0.7} zIndex={0} />
</VStack>
) : undefined}
</Flex>
<TaskInstances nodes={nodes} runId={run.dag_run_id} taskInstances={run.task_instances} />
</Box>
);
};
23 changes: 23 additions & 0 deletions airflow/ui/src/layouts/Details/Grid/DurationAxis.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*!
* 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 { Box, type BoxProps } from "@chakra-ui/react";

export const DurationAxis = (props: BoxProps) => (
<Box borderBottomWidth={1} left="30px" position="absolute" right="25px" zIndex={0} {...props} />
);
25 changes: 25 additions & 0 deletions airflow/ui/src/layouts/Details/Grid/DurationTick.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*!
* 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 { Text, type TextProps } from "@chakra-ui/react";

export const DurationTick = ({ children, ...rest }: TextProps) => (
<Text color="border.emphasized" fontSize="xs" position="absolute" right={1} whiteSpace="nowrap" {...rest}>
{children}
</Text>
);
Loading

0 comments on commit 961ee6e

Please sign in to comment.