apache-airflow-providers-edge3 1.2.0__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. airflow/providers/edge3/__init__.py +1 -1
  2. airflow/providers/edge3/cli/edge_command.py +43 -0
  3. airflow/providers/edge3/cli/worker.py +40 -40
  4. airflow/providers/edge3/models/edge_worker.py +13 -8
  5. airflow/providers/edge3/openapi/v2-edge-generated.yaml +249 -0
  6. airflow/providers/edge3/plugins/www/dist/main.umd.cjs +53 -19
  7. airflow/providers/edge3/plugins/www/openapi-gen/queries/common.ts +7 -0
  8. airflow/providers/edge3/plugins/www/openapi-gen/queries/queries.ts +44 -1
  9. airflow/providers/edge3/plugins/www/openapi-gen/requests/schemas.gen.ts +14 -0
  10. airflow/providers/edge3/plugins/www/openapi-gen/requests/services.gen.ts +158 -1
  11. airflow/providers/edge3/plugins/www/openapi-gen/requests/types.gen.ts +155 -0
  12. airflow/providers/edge3/plugins/www/package.json +14 -10
  13. airflow/providers/edge3/plugins/www/pnpm-lock.yaml +601 -457
  14. airflow/providers/edge3/plugins/www/src/components/AddQueueButton.tsx +138 -0
  15. airflow/providers/edge3/plugins/www/src/components/MaintenanceEditCommentButton.tsx +106 -0
  16. airflow/providers/edge3/plugins/www/src/components/MaintenanceEnterButton.tsx +102 -0
  17. airflow/providers/edge3/plugins/www/src/components/MaintenanceExitButton.tsx +92 -0
  18. airflow/providers/edge3/plugins/www/src/components/RemoveQueueButton.tsx +151 -0
  19. airflow/providers/edge3/plugins/www/src/components/WorkerDeleteButton.tsx +104 -0
  20. airflow/providers/edge3/plugins/www/src/components/WorkerOperations.tsx +85 -0
  21. airflow/providers/edge3/plugins/www/src/components/WorkerShutdownButton.tsx +104 -0
  22. airflow/providers/edge3/plugins/www/src/components/WorkerStateBadge.tsx +33 -0
  23. airflow/providers/edge3/plugins/www/src/components/ui/ScrollToAnchor.tsx +49 -0
  24. airflow/providers/edge3/plugins/www/src/components/ui/createToaster.ts +24 -0
  25. airflow/providers/edge3/plugins/www/src/components/ui/index.ts +2 -0
  26. airflow/providers/edge3/plugins/www/src/context/colorMode/ColorModeProvider.tsx +1 -2
  27. airflow/providers/edge3/plugins/www/src/context/colorMode/useColorMode.tsx +2 -5
  28. airflow/providers/edge3/plugins/www/src/pages/JobsPage.tsx +52 -15
  29. airflow/providers/edge3/plugins/www/src/pages/WorkerPage.tsx +52 -22
  30. airflow/providers/edge3/plugins/www/src/theme.ts +378 -130
  31. airflow/providers/edge3/plugins/www/vite.config.ts +2 -0
  32. airflow/providers/edge3/worker_api/datamodels_ui.py +12 -0
  33. airflow/providers/edge3/worker_api/routes/ui.py +193 -3
  34. {apache_airflow_providers_edge3-1.2.0.dist-info → apache_airflow_providers_edge3-1.3.0.dist-info}/METADATA +6 -6
  35. {apache_airflow_providers_edge3-1.2.0.dist-info → apache_airflow_providers_edge3-1.3.0.dist-info}/RECORD +37 -27
  36. {apache_airflow_providers_edge3-1.2.0.dist-info → apache_airflow_providers_edge3-1.3.0.dist-info}/WHEEL +0 -0
  37. {apache_airflow_providers_edge3-1.2.0.dist-info → apache_airflow_providers_edge3-1.3.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,85 @@
1
+ /*!
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ import { Box, Flex, VStack } from "@chakra-ui/react";
20
+ import type { Worker } from "openapi/requests/types.gen";
21
+
22
+ import { toaster } from "src/components/ui";
23
+
24
+ import { AddQueueButton } from "./AddQueueButton";
25
+ import { MaintenanceEditCommentButton } from "./MaintenanceEditCommentButton";
26
+ import { MaintenanceEnterButton } from "./MaintenanceEnterButton";
27
+ import { MaintenanceExitButton } from "./MaintenanceExitButton";
28
+ import { RemoveQueueButton } from "./RemoveQueueButton";
29
+ import { WorkerDeleteButton } from "./WorkerDeleteButton";
30
+ import { WorkerShutdownButton } from "./WorkerShutdownButton";
31
+
32
+ interface WorkerOperationsProps {
33
+ onOperations: () => void;
34
+ worker: Worker;
35
+ }
36
+
37
+ export const WorkerOperations = ({ onOperations, worker }: WorkerOperationsProps) => {
38
+ const workerName = worker.worker_name;
39
+ const state = worker.state;
40
+
41
+ const onWorkerChange = (toast: Record<string, string>) => {
42
+ toaster.create(toast);
43
+ onOperations();
44
+ };
45
+
46
+ if (state === "idle" || state === "running") {
47
+ return (
48
+ <Flex justifyContent="end" gap={2}>
49
+ <AddQueueButton onQueueUpdate={onWorkerChange} workerName={workerName} />
50
+ <RemoveQueueButton onQueueUpdate={onWorkerChange} worker={worker} />
51
+ <MaintenanceEnterButton onEnterMaintenance={onWorkerChange} workerName={workerName} />
52
+ <WorkerShutdownButton onShutdown={onWorkerChange} workerName={workerName} />
53
+ </Flex>
54
+ );
55
+ } else if (
56
+ state === "maintenance pending" ||
57
+ state === "maintenance mode" ||
58
+ state === "maintenance request" ||
59
+ state === "offline maintenance"
60
+ ) {
61
+ return (
62
+ <VStack gap={2} align="stretch">
63
+ <Box fontSize="sm" whiteSpace="pre-wrap">
64
+ {worker.maintenance_comments || "No comment"}
65
+ </Box>
66
+ <Flex justifyContent="end" gap={2}>
67
+ <MaintenanceEditCommentButton onEditComment={onWorkerChange} workerName={workerName} />
68
+ <MaintenanceExitButton onExitMaintenance={onWorkerChange} workerName={workerName} />
69
+ {state === "offline maintenance" ? (
70
+ <WorkerDeleteButton onDelete={onWorkerChange} workerName={workerName} />
71
+ ) : (
72
+ <WorkerShutdownButton onShutdown={onWorkerChange} workerName={workerName} />
73
+ )}
74
+ </Flex>
75
+ </VStack>
76
+ );
77
+ } else if (state === "offline" || state === "unknown") {
78
+ return (
79
+ <Flex justifyContent="end">
80
+ <WorkerDeleteButton onDelete={onWorkerChange} workerName={workerName} />
81
+ </Flex>
82
+ );
83
+ }
84
+ return null;
85
+ };
@@ -0,0 +1,104 @@
1
+ /*!
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ import { Button, CloseButton, Dialog, IconButton, Portal, Text, useDisclosure } from "@chakra-ui/react";
20
+ import { useUiServiceRequestWorkerShutdown } from "openapi/queries";
21
+ import { FaPowerOff } from "react-icons/fa";
22
+
23
+ interface WorkerShutdownButtonProps {
24
+ onShutdown: (toast: Record<string, string>) => void;
25
+ workerName: string;
26
+ }
27
+
28
+ export const WorkerShutdownButton = ({ onShutdown, workerName }: WorkerShutdownButtonProps) => {
29
+ const { onClose, onOpen, open } = useDisclosure();
30
+
31
+ const shutdownMutation = useUiServiceRequestWorkerShutdown({
32
+ onError: (error) => {
33
+ onShutdown({
34
+ description: `Unable to request shutdown for worker ${workerName}: ${error}`,
35
+ title: "Shutdown Request Failed",
36
+ type: "error",
37
+ });
38
+ },
39
+ onSuccess: () => {
40
+ onShutdown({
41
+ description: `Worker ${workerName} was requested to shutdown.`,
42
+ title: "Shutdown Request Sent",
43
+ type: "success",
44
+ });
45
+ onClose();
46
+ },
47
+ });
48
+
49
+ const handleShutdown = () => {
50
+ shutdownMutation.mutate({ workerName });
51
+ };
52
+
53
+ return (
54
+ <>
55
+ <IconButton
56
+ size="sm"
57
+ variant="ghost"
58
+ onClick={onOpen}
59
+ aria-label="Shutdown Worker"
60
+ title="Shutdown Worker"
61
+ colorPalette="danger"
62
+ >
63
+ <FaPowerOff />
64
+ </IconButton>
65
+
66
+ <Dialog.Root onOpenChange={onClose} open={open} size="md">
67
+ <Portal>
68
+ <Dialog.Backdrop />
69
+ <Dialog.Positioner>
70
+ <Dialog.Content>
71
+ <Dialog.Header>
72
+ <Dialog.Title>Shutdown worker {workerName}</Dialog.Title>
73
+ </Dialog.Header>
74
+ <Dialog.Body>
75
+ <Text>Are you sure you want to request shutdown for worker {workerName}?</Text>
76
+ <Text fontSize="sm" color="red.500" mt={2}>
77
+ This stops the worker on the remote edge. You can't restart it from the UI — you need to
78
+ start it remotely instead.
79
+ </Text>
80
+ </Dialog.Body>
81
+ <Dialog.Footer>
82
+ <Dialog.ActionTrigger asChild>
83
+ <Button variant="outline">Cancel</Button>
84
+ </Dialog.ActionTrigger>
85
+ <Button
86
+ onClick={handleShutdown}
87
+ colorPalette="danger"
88
+ loading={shutdownMutation.isPending}
89
+ loadingText="Shutting down..."
90
+ >
91
+ <FaPowerOff style={{ marginRight: "8px" }} />
92
+ Shutdown Worker
93
+ </Button>
94
+ </Dialog.Footer>
95
+ <Dialog.CloseTrigger asChild>
96
+ <CloseButton size="sm" />
97
+ </Dialog.CloseTrigger>
98
+ </Dialog.Content>
99
+ </Dialog.Positioner>
100
+ </Portal>
101
+ </Dialog.Root>
102
+ </>
103
+ );
104
+ };
@@ -48,6 +48,38 @@ const state2Color = (state: EdgeWorkerState | null | undefined) => {
48
48
  }
49
49
  };
50
50
 
51
+ const state2TooltipText = (state: EdgeWorkerState | null | undefined) => {
52
+ switch (state) {
53
+ // see enum mapping from providers/edge3/src/airflow/providers/edge3/models/edge_worker.py:EdgeWorkerState
54
+ case "starting":
55
+ return "Edge Worker is in initialization.";
56
+ case "running":
57
+ return "Edge Worker is actively running a task.";
58
+ case "idle":
59
+ return "Edge Worker is active and waiting for a task.";
60
+ case "shutdown request":
61
+ return "Request to shutdown Edge Worker is issued. It will be picked-up on the next heartbeat, tasks will drain and then worker will terminate.";
62
+ case "terminating":
63
+ return "Edge Worker is completing work (draining running tasks) and stopping.";
64
+ case "offline":
65
+ return "Edge Worker was shut down.";
66
+ case "unknown":
67
+ return "No heartbeat signal from worker for some time, Edge Worker probably down or got disconnected.";
68
+ case "maintenance request":
69
+ return "Worker was requested to enter maintenance mode. Once worker receives this message it will pause fetching tasks and drain tasks.";
70
+ case "maintenance pending":
71
+ return "Edge Worker received the request for maintenance, waiting for tasks to finish. Once tasks are finished will move to 'maintenance mode'.";
72
+ case "maintenance mode":
73
+ return "Edge Worker is in maintenance mode. It is online but pauses fetching tasks.";
74
+ case "maintenance exit":
75
+ return "Request Worker is requested to exit maintenance mode. Once the worker receives this state it will un-pause and fetch new tasks.";
76
+ case "offline maintenance":
77
+ return "Worker was shut down in maintenance mode. It will be in maintenance mode when restarted.";
78
+ default:
79
+ return undefined;
80
+ }
81
+ };
82
+
51
83
  export type Props = {
52
84
  readonly state?: EdgeWorkerState | null;
53
85
  } & BadgeProps;
@@ -61,6 +93,7 @@ export const WorkerStateBadge = React.forwardRef<HTMLDivElement, Props>(
61
93
  px={children === undefined ? 1 : 2}
62
94
  py={1}
63
95
  ref={ref}
96
+ title={state2TooltipText(state)}
64
97
  variant="solid"
65
98
  {...rest}
66
99
  >
@@ -0,0 +1,49 @@
1
+ /*!
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ import { useEffect, useState } from "react";
20
+
21
+ interface ScrollToAnchorProps {
22
+ inline?: ScrollLogicalPosition;
23
+ block?: ScrollLogicalPosition;
24
+ }
25
+
26
+ export const ScrollToAnchor = ({ block = "start", inline = "nearest" }: ScrollToAnchorProps): null => {
27
+ const [hash, setHash] = useState(() => window.location.hash);
28
+
29
+ useEffect(() => {
30
+ const onHashChange = () => setHash(window.location.hash);
31
+ window.addEventListener("hashchange", onHashChange);
32
+ return () => window.removeEventListener("hashchange", onHashChange);
33
+ }, []);
34
+
35
+ useEffect(() => {
36
+ if (hash) {
37
+ const element = document.getElementById(hash.slice(1));
38
+ if (element) {
39
+ element.scrollIntoView({
40
+ behavior: "auto",
41
+ block: block,
42
+ inline: inline,
43
+ });
44
+ }
45
+ }
46
+ }, [hash, block, inline]);
47
+
48
+ return null;
49
+ };
@@ -0,0 +1,24 @@
1
+ /*!
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ import { createToaster } from "@chakra-ui/react";
20
+
21
+ export const toaster = createToaster({
22
+ pauseOnPageIdle: true,
23
+ placement: "bottom-end",
24
+ });
@@ -18,3 +18,5 @@
18
18
  */
19
19
 
20
20
  export * from "./Alert";
21
+ export * from "./createToaster";
22
+ export * from "./ScrollToAnchor";
@@ -16,8 +16,7 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
- import { ThemeProvider } from "next-themes";
20
- import type { ThemeProviderProps } from "next-themes/dist/types";
19
+ import { ThemeProvider, type ThemeProviderProps } from "next-themes";
21
20
 
22
21
  export const ColorModeProvider = (props: ThemeProviderProps) => (
23
22
  <ThemeProvider attribute="class" disableTransitionOnChange {...props} />
@@ -19,14 +19,11 @@
19
19
  import { useTheme } from "next-themes";
20
20
 
21
21
  export const useColorMode = () => {
22
- const { resolvedTheme, setTheme } = useTheme();
23
- const toggleColorMode = () => {
24
- setTheme(resolvedTheme === "light" ? "dark" : "light");
25
- };
22
+ const { resolvedTheme, setTheme, theme } = useTheme();
26
23
 
27
24
  return {
28
25
  colorMode: resolvedTheme as "dark" | "light" | undefined,
26
+ selectedTheme: theme as "dark" | "light" | "system" | undefined,
29
27
  setColorMode: setTheme,
30
- toggleColorMode,
31
28
  };
32
29
  };
@@ -16,8 +16,10 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
- import { Box, Table } from "@chakra-ui/react";
19
+ import { Box, Link, Table, Text } from "@chakra-ui/react";
20
20
  import { useUiServiceJobs } from "openapi/queries";
21
+ import { Link as RouterLink } from "react-router-dom";
22
+ import TimeAgo from "react-timeago";
21
23
 
22
24
  import { ErrorAlert } from "src/components/ErrorAlert";
23
25
  import { StateBadge } from "src/components/StateBadge";
@@ -33,9 +35,8 @@ export const JobsPage = () => {
33
35
  // Use DataTable as component from Airflow-Core UI
34
36
  // Add sorting
35
37
  // Add filtering
36
- // Add links to see job details / jobs list
37
- // Translation
38
- if (data)
38
+ // Translation?
39
+ if (data?.jobs && data.jobs.length > 0)
39
40
  return (
40
41
  <Box p={2}>
41
42
  <Table.Root size="sm" interactive stickyHeader striped>
@@ -58,31 +59,67 @@ export const JobsPage = () => {
58
59
  <Table.Row
59
60
  key={`${job.dag_id}.${job.run_id}.${job.task_id}.${job.map_index}.${job.try_number}`}
60
61
  >
61
- <Table.Cell>{job.dag_id}</Table.Cell>
62
- <Table.Cell>{job.run_id}</Table.Cell>
63
- <Table.Cell>{job.task_id}</Table.Cell>
64
- <Table.Cell>{job.map_index}</Table.Cell>
62
+ <Table.Cell>
63
+ {/* TODO Check why <Link to={`/dags/${job.dag_id}`}> is not working via react-router-dom! */}
64
+ <Link href={`../dags/${job.dag_id}`}>{job.dag_id}</Link>
65
+ </Table.Cell>
66
+ <Table.Cell>
67
+ <Link href={`../dags/${job.dag_id}/runs/${job.run_id}`}>{job.run_id}</Link>
68
+ </Table.Cell>
69
+ <Table.Cell>
70
+ {job.map_index >= 0 ? (
71
+ <Link
72
+ href={`../dags/${job.dag_id}/runs/${job.run_id}/tasks/${job.task_id}/mapped/${job.map_index}?try_number=${job.try_number}`}
73
+ >
74
+ {job.task_id}
75
+ </Link>
76
+ ) : (
77
+ <Link
78
+ href={`../dags/${job.dag_id}/runs/${job.run_id}/tasks/${job.task_id}?try_number=${job.try_number}`}
79
+ >
80
+ {job.task_id}
81
+ </Link>
82
+ )}
83
+ </Table.Cell>
84
+ <Table.Cell>{job.map_index >= 0 ? job.map_index : "-"}</Table.Cell>
65
85
  <Table.Cell>{job.try_number}</Table.Cell>
66
86
  <Table.Cell>
67
87
  <StateBadge state={job.state}>{job.state}</StateBadge>
68
88
  </Table.Cell>
69
89
  <Table.Cell>{job.queue}</Table.Cell>
70
- <Table.Cell>{job.queued_dttm}</Table.Cell>
71
- <Table.Cell>{job.edge_worker}</Table.Cell>
72
- <Table.Cell>{job.last_update}</Table.Cell>
90
+ <Table.Cell>
91
+ {job.queued_dttm ? <TimeAgo date={job.queued_dttm} live={false} /> : undefined}
92
+ </Table.Cell>
93
+ <Table.Cell>
94
+ <RouterLink to={`/plugin/edge_worker#${job.edge_worker}`}>{job.edge_worker}</RouterLink>
95
+ </Table.Cell>
96
+ <Table.Cell>
97
+ {job.last_update ? <TimeAgo date={job.last_update} live={false} /> : undefined}
98
+ </Table.Cell>
73
99
  </Table.Row>
74
100
  ))}
75
101
  </Table.Body>
76
102
  </Table.Root>
77
103
  </Box>
78
104
  );
105
+ if (data) {
106
+ return (
107
+ <Text as="div" pl={4} pt={1}>
108
+ Currently no jobs running. Start a Dag and then all active jobs should show up here. Note that after
109
+ some (configurable) time, jobs are purged from the list.
110
+ </Text>
111
+ );
112
+ }
79
113
  if (error) {
80
114
  return (
81
- <Box p={2}>
82
- <p>Unable to load data:</p>
115
+ <Text as="div" pl={4} pt={1}>
83
116
  <ErrorAlert error={error} />
84
- </Box>
117
+ </Text>
85
118
  );
86
119
  }
87
- return <Box p={2}>Loading...</Box>;
120
+ return (
121
+ <Text as="div" pl={4} pt={1}>
122
+ Loading...
123
+ </Text>
124
+ );
88
125
  };
@@ -16,27 +16,31 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
- import { Box, Table } from "@chakra-ui/react";
19
+ import { Box, Code, Link, List, Table, Text } from "@chakra-ui/react";
20
20
  import { useUiServiceWorker } from "openapi/queries";
21
+ import { LuExternalLink } from "react-icons/lu";
22
+ import TimeAgo from "react-timeago";
21
23
 
22
24
  import { ErrorAlert } from "src/components/ErrorAlert";
25
+ import { WorkerOperations } from "src/components/WorkerOperations";
23
26
  import { WorkerStateBadge } from "src/components/WorkerStateBadge";
27
+ import { ScrollToAnchor } from "src/components/ui";
24
28
  import { autoRefreshInterval } from "src/utils";
25
29
 
26
30
  export const WorkerPage = () => {
27
- const { data, error } = useUiServiceWorker(undefined, {
31
+ const { data, error, refetch } = useUiServiceWorker(undefined, {
28
32
  enabled: true,
29
33
  refetchInterval: autoRefreshInterval,
30
34
  });
31
35
 
32
36
  // TODO to make it proper
33
37
  // Use DataTable as component from Airflow-Core UI
34
- // Add actions for maintenance / delete of orphan worker
35
38
  // Add sorting
36
39
  // Add filtering
37
- // Add links to see jobs on worker
38
- // Translation
39
- if (data)
40
+ // Add links with filter to see jobs on worker
41
+ // Add time zone support for time display
42
+ // Translation?
43
+ if (data?.workers && data.workers.length > 0)
40
44
  return (
41
45
  <Box p={2}>
42
46
  <Table.Root size="sm" interactive stickyHeader striped>
@@ -54,52 +58,78 @@ export const WorkerPage = () => {
54
58
  </Table.Header>
55
59
  <Table.Body>
56
60
  {data.workers.map((worker) => (
57
- <Table.Row key={worker.worker_name}>
61
+ <Table.Row key={worker.worker_name} id={worker.worker_name}>
58
62
  <Table.Cell>{worker.worker_name}</Table.Cell>
59
63
  <Table.Cell>
60
64
  <WorkerStateBadge state={worker.state}>{worker.state}</WorkerStateBadge>
61
65
  </Table.Cell>
62
66
  <Table.Cell>
63
67
  {worker.queues ? (
64
- <ul>
68
+ <List.Root>
65
69
  {worker.queues.map((queue) => (
66
- <li key={queue}>{queue}</li>
70
+ <List.Item key={queue}>{queue}</List.Item>
67
71
  ))}
68
- </ul>
72
+ </List.Root>
69
73
  ) : (
70
- "(default)"
74
+ "(all queues)"
71
75
  )}
72
76
  </Table.Cell>
73
- <Table.Cell>{worker.first_online}</Table.Cell>
74
- <Table.Cell>{worker.last_heartbeat}</Table.Cell>
77
+ <Table.Cell>
78
+ {worker.first_online ? <TimeAgo date={worker.first_online} live={false} /> : undefined}
79
+ </Table.Cell>
80
+ <Table.Cell>
81
+ {worker.last_heartbeat ? <TimeAgo date={worker.last_heartbeat} live={false} /> : undefined}
82
+ </Table.Cell>
75
83
  <Table.Cell>{worker.jobs_active}</Table.Cell>
76
84
  <Table.Cell>
77
85
  {worker.sysinfo ? (
78
- <ul>
86
+ <List.Root>
79
87
  {Object.entries(worker.sysinfo).map(([key, value]) => (
80
- <li key={key}>
88
+ <List.Item key={key}>
81
89
  {key}: {value}
82
- </li>
90
+ </List.Item>
83
91
  ))}
84
- </ul>
92
+ </List.Root>
85
93
  ) : (
86
94
  "N/A"
87
95
  )}
88
96
  </Table.Cell>
89
- <Table.Cell>{worker.maintenance_comments}</Table.Cell>
97
+ <Table.Cell>
98
+ <WorkerOperations worker={worker} onOperations={refetch} />
99
+ </Table.Cell>
90
100
  </Table.Row>
91
101
  ))}
92
102
  </Table.Body>
93
103
  </Table.Root>
104
+ <ScrollToAnchor />
94
105
  </Box>
95
106
  );
107
+ if (data) {
108
+ return (
109
+ <Text as="div" pl={4} pt={1}>
110
+ No known workers. Start one via <Code>airflow edge worker [...]</Code>. See{" "}
111
+ <Link
112
+ target="_blank"
113
+ variant="underline"
114
+ color="fg.info"
115
+ href="https://airflow.apache.org/docs/apache-airflow-providers-edge3/stable/deployment.html"
116
+ >
117
+ Edge Worker Deployment docs <LuExternalLink />
118
+ </Link>{" "}
119
+ how to deploy a new worker.
120
+ </Text>
121
+ );
122
+ }
96
123
  if (error) {
97
124
  return (
98
- <Box p={2}>
99
- <p>Unable to load data:</p>
125
+ <Text as="div" pl={4} pt={1}>
100
126
  <ErrorAlert error={error} />
101
- </Box>
127
+ </Text>
102
128
  );
103
129
  }
104
- return <Box p={2}>Loading...</Box>;
130
+ return (
131
+ <Text as="div" pl={4} pt={1}>
132
+ Loading...
133
+ </Text>
134
+ );
105
135
  };