This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 393a58451d7 Add Dag Page header (#43362)
393a58451d7 is described below
commit 393a58451d7b2f6f3d5b3ab853486cb7900ae5b1
Author: Brent Bovenzi <[email protected]>
AuthorDate: Mon Oct 28 04:41:13 2024 -0400
Add Dag Page header (#43362)
* Add ability to select a dag from dags list
* Fix event propagation and onRowClicked type
* Remove div links and only show error stack trace in dev
* use get dag details to build header for Dag Page
* Rebase fix and move semantic color gen to a function
---
airflow/ui/src/pages/DagsList/Dag/Dag.tsx | 69 +++++++++++++-
airflow/ui/src/pages/DagsList/Dag/Header.tsx | 120 +++++++++++++++++++++++++
airflow/ui/src/pages/DagsList/DagCard.test.tsx | 2 +-
airflow/ui/src/pages/DagsList/DagCard.tsx | 50 ++++-------
airflow/ui/src/pages/DagsList/DagTags.tsx | 55 ++++++++++++
airflow/ui/src/pages/DagsList/DagsFilters.tsx | 88 ++++++++----------
airflow/ui/src/theme.ts | 27 +++---
7 files changed, 311 insertions(+), 100 deletions(-)
diff --git a/airflow/ui/src/pages/DagsList/Dag/Dag.tsx
b/airflow/ui/src/pages/DagsList/Dag/Dag.tsx
index 79290803e70..52f8934e899 100644
--- a/airflow/ui/src/pages/DagsList/Dag/Dag.tsx
+++ b/airflow/ui/src/pages/DagsList/Dag/Dag.tsx
@@ -16,11 +16,72 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Box } from "@chakra-ui/react";
-import { useParams } from "react-router-dom";
+import {
+ Box,
+ Button,
+ Progress,
+ Tab,
+ TabList,
+ TabPanel,
+ TabPanels,
+ Tabs,
+} from "@chakra-ui/react";
+import { FiChevronsLeft } from "react-icons/fi";
+import { Link as RouterLink, useParams } from "react-router-dom";
+
+import { useDagServiceGetDagDetails } from "openapi/queries";
+import { ErrorAlert } from "src/components/ErrorAlert";
+
+import { Header } from "./Header";
export const Dag = () => {
- const params = useParams();
+ const { dagId } = useParams();
+
+ const {
+ data: dag,
+ error,
+ isLoading,
+ } = useDagServiceGetDagDetails({
+ dagId: dagId ?? "",
+ });
+
+ return (
+ <Box>
+ <Button
+ as={RouterLink}
+ color="blue.400"
+ leftIcon={<FiChevronsLeft />}
+ to="/dags"
+ variant="link"
+ >
+ Back to all dags
+ </Button>
+ <Header dag={dag} dagId={dagId} />
+ <ErrorAlert error={error} />
+ <Progress
+ isIndeterminate
+ size="xs"
+ visibility={isLoading ? "visible" : "hidden"}
+ />
+ <Tabs>
+ <TabList>
+ <Tab>Overview</Tab>
+ <Tab>Runs</Tab>
+ <Tab>Tasks</Tab>
+ </TabList>
- return <Box>{params.dagId}</Box>;
+ <TabPanels>
+ <TabPanel>
+ <p>one!</p>
+ </TabPanel>
+ <TabPanel>
+ <p>two!</p>
+ </TabPanel>
+ <TabPanel>
+ <p>three!</p>
+ </TabPanel>
+ </TabPanels>
+ </Tabs>
+ </Box>
+ );
};
diff --git a/airflow/ui/src/pages/DagsList/Dag/Header.tsx
b/airflow/ui/src/pages/DagsList/Dag/Header.tsx
new file mode 100644
index 00000000000..25d674e1441
--- /dev/null
+++ b/airflow/ui/src/pages/DagsList/Dag/Header.tsx
@@ -0,0 +1,120 @@
+/*!
+ * 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,
+ Button,
+ Flex,
+ Heading,
+ HStack,
+ SimpleGrid,
+ Text,
+ Tooltip,
+ useColorModeValue,
+ VStack,
+} from "@chakra-ui/react";
+import { FiCalendar, FiPlay } from "react-icons/fi";
+
+import type { DAGResponse } from "openapi/requests/types.gen";
+import { DagIcon } from "src/assets/DagIcon";
+import Time from "src/components/Time";
+import { TogglePause } from "src/components/TogglePause";
+
+import { DagTags } from "../DagTags";
+
+export const Header = ({
+ dag,
+ dagId,
+}: {
+ readonly dag?: DAGResponse;
+ readonly dagId?: string;
+}) => {
+ const grayBg = useColorModeValue("gray.100", "gray.900");
+ const grayBorder = useColorModeValue("gray.200", "gray.700");
+
+ return (
+ <Box
+ borderColor={grayBorder}
+ borderRadius={8}
+ borderWidth={1}
+ overflow="hidden"
+ >
+ <Box p={2}>
+ <Flex alignItems="center" justifyContent="space-between">
+ <HStack alignItems="center" spacing={2}>
+ <DagIcon height={8} width={8} />
+ <Heading size="lg">{dag?.dag_display_name ?? dagId}</Heading>
+ {dag !== undefined && (
+ <TogglePause dagId={dag.dag_id} isPaused={dag.is_paused} />
+ )}
+ </HStack>
+ <Flex>
+ <Button colorScheme="blue" isDisabled leftIcon={<FiPlay />}>
+ Trigger
+ </Button>
+ </Flex>
+ </Flex>
+ <SimpleGrid columns={4} height={8} my={4} spacing={4}>
+ <VStack align="flex-start" spacing={1}>
+ <Heading color="gray.500" fontSize="xs">
+ Last Run
+ </Heading>
+ </VStack>
+ <VStack align="flex-start" spacing={1}>
+ <Heading color="gray.500" fontSize="xs">
+ Next Run
+ </Heading>
+ {Boolean(dag?.next_dagrun) ? (
+ <Text fontSize="sm">
+ <Time datetime={dag?.next_dagrun} />
+ </Text>
+ ) : undefined}
+ </VStack>
+ <VStack align="flex-start" spacing={1}>
+ <Heading color="gray.500" fontSize="xs">
+ Schedule
+ </Heading>
+ {Boolean(dag?.timetable_summary) ? (
+ <Tooltip hasArrow label={dag?.timetable_description}>
+ <Text fontSize="sm">
+ <FiCalendar style={{ display: "inline" }} />{" "}
+ {dag?.timetable_summary}
+ </Text>
+ </Tooltip>
+ ) : undefined}
+ </VStack>
+ <div />
+ </SimpleGrid>
+ </Box>
+ <Flex
+ alignItems="center"
+ bg={grayBg}
+ borderTopColor={grayBorder}
+ borderTopWidth={1}
+ color="gray.400"
+ fontSize="sm"
+ justifyContent="space-between"
+ px={2}
+ py={1}
+ >
+ <Text>Owner: {dag?.owners.join(", ")}</Text>
+ <DagTags tags={dag?.tags ?? []} />
+ </Flex>
+ </Box>
+ );
+};
diff --git a/airflow/ui/src/pages/DagsList/DagCard.test.tsx
b/airflow/ui/src/pages/DagsList/DagCard.test.tsx
index 3ae6e4fceea..b039d7c90b4 100644
--- a/airflow/ui/src/pages/DagsList/DagCard.test.tsx
+++ b/airflow/ui/src/pages/DagsList/DagCard.test.tsx
@@ -82,6 +82,6 @@ describe("DagCard", () => {
render(<DagCard dag={expandedMockDag} />, { wrapper: Wrapper });
expect(screen.getByTestId("dag-tag")).toBeInTheDocument();
- expect(screen.getByText("+1 more")).toBeInTheDocument();
+ expect(screen.getByText(", +1 more")).toBeInTheDocument();
});
});
diff --git a/airflow/ui/src/pages/DagsList/DagCard.tsx
b/airflow/ui/src/pages/DagsList/DagCard.tsx
index c222ac67897..e3b933c8ebb 100644
--- a/airflow/ui/src/pages/DagsList/DagCard.tsx
+++ b/airflow/ui/src/pages/DagsList/DagCard.tsx
@@ -17,7 +17,6 @@
* under the License.
*/
import {
- Badge,
Box,
Flex,
HStack,
@@ -28,22 +27,22 @@ import {
VStack,
Link,
} from "@chakra-ui/react";
-import { FiCalendar, FiTag } from "react-icons/fi";
+import { FiCalendar } from "react-icons/fi";
import { Link as RouterLink } from "react-router-dom";
import type { DAGResponse } from "openapi/requests/types.gen";
import Time from "src/components/Time";
import { TogglePause } from "src/components/TogglePause";
+import { DagTags } from "./DagTags";
+
type Props = {
readonly dag: DAGResponse;
};
-const MAX_TAGS = 3;
-
export const DagCard = ({ dag }: Props) => (
<Box
- borderColor="blue.minimal"
+ borderColor="gray.emphasized"
borderRadius={8}
borderWidth={1}
overflow="hidden"
@@ -67,42 +66,26 @@ export const DagCard = ({ dag }: Props) => (
{dag.dag_display_name}
</Link>
</Tooltip>
- {dag.tags.length ? (
- <HStack spacing={1}>
- <FiTag data-testid="dag-tag" />
- {dag.tags.slice(0, MAX_TAGS).map((tag) => (
- <Badge key={tag.name}>{tag.name}</Badge>
- ))}
- {dag.tags.length > MAX_TAGS && (
- <Tooltip
- hasArrow
- label={
- <VStack p={1} spacing={1}>
- {dag.tags.slice(MAX_TAGS).map((tag) => (
- <Badge key={tag.name}>{tag.name}</Badge>
- ))}
- </VStack>
- }
- >
- <Badge>+{dag.tags.length - MAX_TAGS} more</Badge>
- </Tooltip>
- )}
- </HStack>
- ) : undefined}
+ <DagTags tags={dag.tags} />
+ </HStack>
+ <HStack>
+ <TogglePause dagId={dag.dag_id} isPaused={dag.is_paused} />
</HStack>
- <TogglePause dagId={dag.dag_id} isPaused={dag.is_paused} />
</Flex>
<SimpleGrid columns={4} height={20} px={3} py={2} spacing={4}>
<div />
+ <VStack align="flex-start" spacing={1}>
+ <Heading color="gray.500" fontSize="xs">
+ Last Run
+ </Heading>
+ </VStack>
<VStack align="flex-start" spacing={1}>
<Heading color="gray.500" fontSize="xs">
Next Run
</Heading>
- {Boolean(dag.next_dagrun) ? (
- <Text fontSize="sm">
- <Time datetime={dag.next_dagrun} />
- </Text>
- ) : undefined}
+ <Text fontSize="sm">
+ <Time datetime={dag.next_dagrun} />
+ </Text>
{Boolean(dag.timetable_summary) ? (
<Tooltip hasArrow label={dag.timetable_description}>
<Text fontSize="sm">
@@ -114,7 +97,6 @@ export const DagCard = ({ dag }: Props) => (
) : undefined}
</VStack>
<div />
- <div />
</SimpleGrid>
</Box>
);
diff --git a/airflow/ui/src/pages/DagsList/DagTags.tsx
b/airflow/ui/src/pages/DagsList/DagTags.tsx
new file mode 100644
index 00000000000..2a3cb3f76be
--- /dev/null
+++ b/airflow/ui/src/pages/DagsList/DagTags.tsx
@@ -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 { Flex, Text, Tooltip, VStack } from "@chakra-ui/react";
+import { FiTag } from "react-icons/fi";
+
+import type { DagTagPydantic } from "openapi/requests/types.gen";
+
+const MAX_TAGS = 3;
+
+type Props = {
+ readonly tags: Array<DagTagPydantic>;
+};
+
+export const DagTags = ({ tags }: Props) =>
+ tags.length ? (
+ <Flex alignItems="center" ml={2}>
+ <FiTag data-testid="dag-tag" />
+ <Text ml={1}>
+ {tags
+ .slice(0, MAX_TAGS)
+ .map(({ name }) => name)
+ .join(", ")}
+ </Text>
+ {tags.length > MAX_TAGS && (
+ <Tooltip
+ hasArrow
+ label={
+ <VStack p={1} spacing={1}>
+ {tags.slice(MAX_TAGS).map((tag) => (
+ <Text key={tag.name}>{tag.name}</Text>
+ ))}
+ </VStack>
+ }
+ >
+ <Text>, +{tags.length - MAX_TAGS} more</Text>
+ </Tooltip>
+ )}
+ </Flex>
+ ) : undefined;
diff --git a/airflow/ui/src/pages/DagsList/DagsFilters.tsx
b/airflow/ui/src/pages/DagsList/DagsFilters.tsx
index 0b2ff79fb0e..376b9220d23 100644
--- a/airflow/ui/src/pages/DagsList/DagsFilters.tsx
+++ b/airflow/ui/src/pages/DagsList/DagsFilters.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { HStack, Select, Text, Box } from "@chakra-ui/react";
+import { HStack, Select } from "@chakra-ui/react";
import { Select as ReactSelect } from "chakra-react-select";
import type { MultiValue } from "chakra-react-select";
import { useCallback } from "react";
@@ -106,55 +106,45 @@ export const DagsFilters = () => {
return (
<HStack justifyContent="space-between">
<HStack spacing={4}>
- <Box>
- <Text fontSize="sm" fontWeight={200} mb={1}>
- State:
- </Text>
- <HStack>
- <QuickFilterButton
- isActive={isAll}
- onClick={handleStateChange}
- value="all"
- >
- All
- </QuickFilterButton>
- <QuickFilterButton
- isActive={isFailed}
- onClick={handleStateChange}
- value="failed"
- >
- Failed
- </QuickFilterButton>
- <QuickFilterButton
- isActive={isRunning}
- onClick={handleStateChange}
- value="running"
- >
- Running
- </QuickFilterButton>
- <QuickFilterButton
- isActive={isSuccess}
- onClick={handleStateChange}
- value="success"
- >
- Successful
- </QuickFilterButton>
- </HStack>
- </Box>
- <Box>
- <Text fontSize="sm" fontWeight={200} mb={1}>
- Active:
- </Text>
- <Select
- onChange={handlePausedChange}
- value={showPaused ?? undefined}
- variant="flushed"
+ <HStack>
+ <QuickFilterButton
+ isActive={isAll}
+ onClick={handleStateChange}
+ value="all"
>
- <option>All</option>
- <option value="false">Enabled DAGs</option>
- <option value="true">Disabled DAGs</option>
- </Select>
- </Box>
+ All
+ </QuickFilterButton>
+ <QuickFilterButton
+ isActive={isFailed}
+ onClick={handleStateChange}
+ value="failed"
+ >
+ Failed
+ </QuickFilterButton>
+ <QuickFilterButton
+ isActive={isRunning}
+ onClick={handleStateChange}
+ value="running"
+ >
+ Running
+ </QuickFilterButton>
+ <QuickFilterButton
+ isActive={isSuccess}
+ onClick={handleStateChange}
+ value="success"
+ >
+ Successful
+ </QuickFilterButton>
+ </HStack>
+ <Select
+ onChange={handlePausedChange}
+ value={showPaused ?? undefined}
+ variant="flushed"
+ >
+ <option>All</option>
+ <option value="false">Enabled DAGs</option>
+ <option value="true">Disabled DAGs</option>
+ </Select>
</HStack>
<ReactSelect
aria-label="Filter Dags by tag"
diff --git a/airflow/ui/src/theme.ts b/airflow/ui/src/theme.ts
index 08e300a9bb4..ea6bc397940 100644
--- a/airflow/ui/src/theme.ts
+++ b/airflow/ui/src/theme.ts
@@ -52,6 +52,19 @@ const baseStyle = definePartsStyle(() => ({
export const tableTheme = defineMultiStyleConfig({ baseStyle });
+const generateSemanticColors = (color: string) => ({
+ /* eslint-disable perfectionist/sort-objects */
+ contrast: { _dark: `${color}.200`, _light: `${color}.600` },
+ focusRing: `${color}.500`,
+ fg: { _dark: `${color}.600`, _light: `${color}.400` },
+ emphasized: { _dark: `${color}.700`, _light: `${color}.300` },
+ solid: { _dark: `${color}.800`, _light: `${color}.200` },
+ muted: { _dark: `${color}.900`, _light: `${color}.100` },
+ subtle: { _dark: `${color}.950`, _light: `${color}.50` },
+ minimal: { _dark: "gray.900", _light: `${color}.50` },
+ /* eslint-enable perfectionist/sort-objects */
+});
+
const theme = extendTheme({
colors: {
blue: {
@@ -67,18 +80,8 @@ const theme = extendTheme({
},
semanticTokens: {
colors: {
- blue: {
- /* eslint-disable perfectionist/sort-objects */
- contrast: { _dark: "blue.200", _light: "blue.600" },
- focusRing: "blue.500",
- fg: { _dark: "blue.600", _light: "blue.400" },
- emphasized: { _dark: "blue.700", _light: "blue.300" },
- solid: { _dark: "blue.800", _light: "blue.200" },
- muted: { _dark: "blue.900", _light: "blue.100" },
- subtle: { _dark: "blue.950", _light: "blue.50" },
- minimal: { _dark: "gray.900", _light: "blue.50" },
- /* eslint-enable perfectionist/sort-objects */
- },
+ blue: generateSemanticColors("blue"),
+ gray: generateSemanticColors("gray"),
},
},
styles: {