Skip to content

Commit

Permalink
Init Dag Overview page with time range selector and failed tasks
Browse files Browse the repository at this point in the history
  • Loading branch information
bbovenzi committed Nov 15, 2024
1 parent feb6f7f commit 7407304
Show file tree
Hide file tree
Showing 9 changed files with 342 additions and 749 deletions.
6 changes: 4 additions & 2 deletions airflow/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,20 @@
},
"dependencies": {
"@chakra-ui/anatomy": "^2.2.2",
"@chakra-ui/react": "^3.0.2",
"@chakra-ui/react": "^3.1.1",
"@codemirror/lang-json": "^6.0.1",
"@emotion/react": "^11.13.3",
"@tanstack/react-query": "^5.52.1",
"@tanstack/react-table": "^8.20.1",
"@uiw/react-codemirror": "^4.23.5",
"@uiw/codemirror-themes-all": "^4.23.5",
"@uiw/react-codemirror": "^4.23.5",
"axios": "^1.7.7",
"chakra-react-select": "6.0.0-next.2",
"chart.js": "^4.4.6",
"dayjs": "^1.11.13",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.3.1",
"react-hook-form": "^7.20.0",
"react-icons": "^5.3.0",
Expand Down
770 changes: 42 additions & 728 deletions airflow/ui/pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion airflow/ui/src/components/StateCircle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const StateCircle = ({
bg={stateColor[state]}
borderRadius="50%"
h={2}
maxW={2}
minW={2}
w={2}
/>
);
21 changes: 15 additions & 6 deletions airflow/ui/src/components/TimeRangeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
import { HStack, Text, type SelectValueChangeDetails } from "@chakra-ui/react";
import {
HStack,
Text,
type SelectValueChangeDetails,
createListCollection,
type ListCollection,
} from "@chakra-ui/react";
} from "@chakra-ui/react/collection";
import dayjs from "dayjs";
import { FiCalendar } from "react-icons/fi";

Expand All @@ -34,16 +33,26 @@ type Props = {
readonly setEndDate: (startDate: string) => void;
readonly setStartDate: (startDate: string) => void;
readonly startDate: string;
readonly timeOptions: ListCollection<{ label: string; value: string }>;
readonly timeOptions?: ListCollection<{ label: string; value: string }>;
};

const defaultTimeOptions = createListCollection({
items: [
{ label: "Last 1 hour", value: "1" },
{ label: "Last 8 hours", value: "8" },
{ label: "Last 12 hours", value: "12" },
{ label: "Last 24 hours", value: "24" },
{ label: "Last week", value: "168" },
],
});

const TimeRangeSelector = ({
defaultValue,
endDate,
setEndDate,
setStartDate,
startDate,
timeOptions,
timeOptions = defaultTimeOptions,
}: Props) => {
const handleTimeChange = ({
value,
Expand Down
160 changes: 160 additions & 0 deletions airflow/ui/src/pages/DagsList/Dag/Overview/Chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*!
* 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, useToken } from "@chakra-ui/react";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
type ChartOptions,
} from "chart.js";
import dayjs from "dayjs";
import { useMemo, useRef, useEffect } from "react";
import { Line } from "react-chartjs-2";

import { useColorMode } from "src/context/colorMode";

ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Filler,
Tooltip,
);

type Event = { timestamp: string };

const aggregateEventsIntoIntervals = (
events: Array<Event>,
startDate: string,
endDate: string,
) => {
const totalMinutes = dayjs(endDate).diff(startDate, "minutes");
const intervalSize = Math.floor(totalMinutes / 10);
const intervals = Array.from({ length: 10 }).fill(0) as Array<number>;

events.forEach((event) => {
const minutesSinceStart = dayjs(event.timestamp).diff(startDate, "minutes");
const intervalIndex = Math.min(
Math.floor(minutesSinceStart / intervalSize),
9,
);

if (intervals[intervalIndex] !== undefined) {
intervals[intervalIndex] += 1;
}
});

return intervals;
};

const options = {
layout: {
padding: {
bottom: 2,
top: 2,
},
},
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
},
},
responsive: true,
scales: {
x: {
display: false,
grid: {
display: false,
},
},
y: {
display: false,
grid: {
display: false,
},
},
},
} satisfies ChartOptions;

type Props = {
readonly endDate: string;
readonly events: Array<Event>;
readonly startDate: string;
};

export const Chart = ({ endDate, events, startDate }: Props) => {
const { colorMode } = useColorMode();
const chartRef = useRef<ChartJS<"line">>();

// Get raw color values instead of CSS variables
const [bgLight, bgDark, lineLight, lineDark] = useToken("colors", [
"red.100",
"red.800",
"red.500",
"red.400",
]);

const backgroundColor = colorMode === "light" ? bgLight : bgDark;
const lineColor = colorMode === "light" ? lineLight : lineDark;

const intervalData = useMemo(
() => aggregateEventsIntoIntervals(events, startDate, endDate),
[events, startDate, endDate],
);

// Cleanup chart instance on unmount
useEffect(
() => () => {
if (chartRef.current) {
chartRef.current.destroy();
}
},
[],
);

const data = {
datasets: [
{
backgroundColor,
borderColor: lineColor,
borderWidth: 2,
data: intervalData,
fill: true,
pointRadius: 0,
tension: 0.4,
},
],
labels: Array.from({ length: 10 }).fill(""),
};

return (
<Box h="25px" w="200px">
<Line data={data} options={options} ref={chartRef} />
</Box>
);
};
96 changes: 96 additions & 0 deletions airflow/ui/src/pages/DagsList/Dag/Overview/Overview.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 { Box, HStack, Badge, Text, Skeleton } from "@chakra-ui/react";
import dayjs from "dayjs";
import { useState } from "react";
import { Link, useLocation, useParams } from "react-router-dom";

import { useTaskInstanceServiceGetTaskInstances } from "openapi/queries";
import TimeRangeSelector from "src/components/TimeRangeSelector";
import { pluralize } from "src/utils";
import { stateColor } from "src/utils/stateColor";

import { Chart } from "./Chart";

const defaultHour = "8";

export const Overview = () => {
const { dagId } = useParams();

const now = dayjs();
const [startDate, setStartDate] = useState(
now.subtract(Number(defaultHour), "hour").toISOString(),
);
const [endDate, setEndDate] = useState(now.toISOString());

const { data: failedTasks, isLoading } =
useTaskInstanceServiceGetTaskInstances({
dagId: dagId ?? "",
dagRunId: "~",
logicalDateGte: startDate,
logicalDateLte: endDate,
state: ["failed"],
});

const location = useLocation();

// TODO actually link to task instances list
return (
<Box m={4}>
<Box my={2}>
<TimeRangeSelector
defaultValue={defaultHour}
endDate={endDate}
setEndDate={setEndDate}
setStartDate={setStartDate}
startDate={startDate}
/>
</Box>
{failedTasks?.total_entries !== undefined &&
failedTasks.total_entries > 0 ? (
// TODO: make sure url params pass correctly
<Link to={`${location.pathname}/tasks?state=failed`}>
<HStack borderRadius={4} borderWidth={1} p={3} width="max-content">
<Badge
borderRadius="50%"
colorPalette={stateColor.failed}
variant="solid"
>
{failedTasks.total_entries}
</Badge>
<Text fontSize="sm" fontWeight="bold">
Failed{" "}
{pluralize("Task", failedTasks.total_entries, undefined, true)}
</Text>
<Chart
endDate={endDate}
events={failedTasks.task_instances.map((ti) => ({
timestamp: ti.start_date ?? ti.logical_date,
}))}
startDate={startDate}
/>
</HStack>
</Link>
) : undefined}
{isLoading ? (
<Skeleton borderRadius={4} height="45px" width="350px" />
) : undefined}
</Box>
);
};
20 changes: 20 additions & 0 deletions airflow/ui/src/pages/DagsList/Dag/Overview/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*!
* 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.
*/

export * from "./Overview";
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Box, VStack, createListCollection } from "@chakra-ui/react";
import { Box, VStack } from "@chakra-ui/react";
import dayjs from "dayjs";
import { useState } from "react";

Expand All @@ -28,17 +28,9 @@ import { DagRunMetrics } from "./DagRunMetrics";
import { MetricSectionSkeleton } from "./MetricSectionSkeleton";
import { TaskInstanceMetrics } from "./TaskInstanceMetrics";

const timeOptions = createListCollection({
items: [
{ label: "Last 1 hour", value: "1" },
{ label: "Last 8 hours", value: "8" },
{ label: "Last 12 hours", value: "12" },
{ label: "Last 24 hours", value: "24" },
],
});
const defaultHour = "8";

export const HistoricalMetrics = () => {
const defaultHour = "8";
const now = dayjs();
const [startDate, setStartDate] = useState(
now.subtract(Number(defaultHour), "hour").toISOString(),
Expand Down Expand Up @@ -74,7 +66,6 @@ export const HistoricalMetrics = () => {
setEndDate={setEndDate}
setStartDate={setStartDate}
startDate={startDate}
timeOptions={timeOptions}
/>
{isLoading ? <MetricSectionSkeleton /> : undefined}
{!isLoading && data !== undefined && (
Expand Down
Loading

0 comments on commit 7407304

Please sign in to comment.