Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement task duration page in react. #35863

Merged
merged 2 commits into from Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 9 additions & 1 deletion airflow/www/static/js/components/ReactECharts.tsx
Expand Up @@ -28,6 +28,7 @@ export interface ReactEChartsProps {
settings?: SetOptionOpts;
style?: CSSProperties;
theme?: "light" | "dark";
events?: { [key: string]: (params: any) => void };
}

const ReactECharts = ({
Expand All @@ -36,6 +37,7 @@ const ReactECharts = ({
settings,
style,
theme,
events,
}: ReactEChartsProps) => {
const ref = useRef<HTMLDivElement>(null);

Expand Down Expand Up @@ -78,9 +80,15 @@ const ReactECharts = ({
const chartInstance = getInstanceByDom(ref.current);
if (chartInstance) {
chartInstance.setOption(option, settings);

if (events) {
Object.keys(events).forEach((key) => {
chartInstance.on(key, events[key]);
});
}
}
}
}, [option, settings, theme]);
}, [option, settings, theme, events]);

return (
<div
Expand Down
21 changes: 9 additions & 12 deletions airflow/www/static/js/dag/TaskName.test.tsx
Expand Up @@ -28,28 +28,25 @@ import TaskName from "./TaskName";

describe("Test TaskName", () => {
test("Displays a normal task name", () => {
const { getByText } = render(
<TaskName label="test" id="test" onToggle={() => {}} />,
{ wrapper: ChakraWrapper }
);
const { getByText } = render(<TaskName label="test" id="test" />, {
wrapper: ChakraWrapper,
});

expect(getByText("test")).toBeDefined();
});

test("Displays a mapped task name", () => {
const { getByText } = render(
<TaskName label="test" id="test" isMapped onToggle={() => {}} />,
{ wrapper: ChakraWrapper }
);
const { getByText } = render(<TaskName label="test" id="test" isMapped />, {
wrapper: ChakraWrapper,
});

expect(getByText("test [ ]")).toBeDefined();
});

test("Displays a group task name", () => {
const { getByText } = render(
<TaskName label="test" id="test" isGroup onToggle={() => {}} />,
{ wrapper: ChakraWrapper }
);
const { getByText } = render(<TaskName label="test" id="test" isGroup />, {
wrapper: ChakraWrapper,
});

expect(getByText("test")).toBeDefined();
});
Expand Down
51 changes: 25 additions & 26 deletions airflow/www/static/js/dag/TaskName.tsx
Expand Up @@ -17,14 +17,13 @@
* under the License.
*/

import React, { MouseEventHandler, CSSProperties } from "react";
import { Text, TextProps, useTheme } from "@chakra-ui/react";
import React, { CSSProperties } from "react";
import { Text, TextProps, useTheme, chakra } from "@chakra-ui/react";
import { FiChevronUp, FiArrowUpRight, FiArrowDownRight } from "react-icons/fi";

interface Props extends TextProps {
isGroup?: boolean;
isMapped?: boolean;
onToggle: MouseEventHandler<HTMLDivElement>;
isOpen?: boolean;
label: string;
id?: string;
Expand All @@ -35,12 +34,12 @@ interface Props extends TextProps {
const TaskName = ({
isGroup = false,
isMapped = false,
onToggle,
isOpen = false,
label,
id,
setupTeardownType,
isZoomedOut,
onClick,
...rest
}: Props) => {
const { colors } = useTheme();
Expand All @@ -51,34 +50,34 @@ const TaskName = ({
};
return (
<Text
cursor={isGroup ? "pointer" : undefined}
onClick={onToggle}
cursor="pointer"
data-testid={id}
width="100%"
color={colors.gray[800]}
fontSize={isZoomedOut ? 24 : undefined}
textAlign="justify"
{...rest}
>
{label}
{isMapped && " [ ]"}
{isGroup && (
<FiChevronUp
size={isZoomedOut ? 24 : 15}
strokeWidth={3}
style={{
transition: "transform 0.5s",
transform: `rotate(${isOpen ? 0 : 180}deg)`,
...iconStyle,
}}
/>
)}
{setupTeardownType === "setup" && (
<FiArrowUpRight size={isZoomedOut ? 24 : 15} style={iconStyle} />
)}
{setupTeardownType === "teardown" && (
<FiArrowDownRight size={isZoomedOut ? 24 : 15} style={iconStyle} />
)}
<chakra.span onClick={onClick}>
{label}
{isMapped && " [ ]"}
{isGroup && (
<FiChevronUp
size={isZoomedOut ? 24 : 15}
strokeWidth={3}
style={{
transition: "transform 0.5s",
transform: `rotate(${isOpen ? 0 : 180}deg)`,
...iconStyle,
}}
/>
)}
{setupTeardownType === "setup" && (
<FiArrowUpRight size={isZoomedOut ? 24 : 15} style={iconStyle} />
)}
{setupTeardownType === "teardown" && (
<FiArrowDownRight size={isZoomedOut ? 24 : 15} style={iconStyle} />
)}
</chakra.span>
</Text>
);
};
Expand Down
23 changes: 15 additions & 8 deletions airflow/www/static/js/dag/details/Header.tsx
Expand Up @@ -47,13 +47,18 @@ const Header = () => {
} = useSelection();
const dagRun = dagRuns.find((r) => r.runId === runId);

// clearSelection if the current selected dagRun is
// filtered out.
const group = getTask({ taskId, task: groups });

// If runId and/or taskId can't be found remove the selection
useEffect(() => {
if (runId && !dagRun) {
if (runId && !dagRun && taskId && !group) {
clearSelection();
} else if (runId && !dagRun) {
onSelect({ taskId });
} else if (taskId && !group) {
onSelect({ runId });
}
}, [clearSelection, dagRun, runId]);
}, [dagRun, taskId, group, runId, onSelect, clearSelection]);

let runLabel;
if (dagRun && runId) {
Expand All @@ -75,15 +80,13 @@ const Header = () => {
);
}

const group = getTask({ taskId, task: groups });

const lastIndex = taskId ? taskId.lastIndexOf(".") : null;
const taskName =
taskId && lastIndex ? taskId.substring(lastIndex + 1) : taskId;

const isDagDetails = !runId && !taskId;
const isRunDetails = !!(runId && !taskId);
const isTaskDetails = runId && taskId && mapIndex === undefined;
const isTaskDetails = !runId && taskId;
const isMappedTaskDetails = runId && taskId && mapIndex !== undefined;

return (
Expand All @@ -109,7 +112,11 @@ const Header = () => {
{taskId && (
<BreadcrumbItem isCurrentPage mt={4}>
<BreadcrumbLink
onClick={() => onSelect({ runId, taskId })}
onClick={() =>
mapIndex !== undefined
? onSelect({ runId, taskId })
: onSelect({ taskId })
}
_hover={isTaskDetails ? { cursor: "default" } : undefined}
>
<BreadcrumbText
Expand Down
2 changes: 1 addition & 1 deletion airflow/www/static/js/dag/details/graph/Node.tsx
Expand Up @@ -144,7 +144,7 @@ export const BaseNode = ({
label={taskName}
isOpen={isOpen}
isGroup={!!childCount}
onToggle={(e) => {
onClick={(e) => {
e.stopPropagation();
onToggleCollapse();
}}
Expand Down
2 changes: 1 addition & 1 deletion airflow/www/static/js/dag/details/graph/utils.ts
Expand Up @@ -62,7 +62,7 @@ export const flattenNodes = ({
if (!node.id.endsWith("join_id") && selected.runId) {
instance = group?.instances.find((ti) => ti.runId === selected.runId);
}
const isSelected = node.id === selected.taskId && !!instance;
const isSelected = node.id === selected.taskId;
const isActive =
instance && hoveredTaskState !== undefined
? hoveredTaskState === instance.state
Expand Down
40 changes: 31 additions & 9 deletions airflow/www/static/js/dag/details/index.tsx
Expand Up @@ -27,6 +27,7 @@ import {
TabPanels,
Tab,
Text,
Button,
} from "@chakra-ui/react";
import { useSearchParams } from "react-router-dom";

Expand All @@ -40,6 +41,7 @@ import {
MdCode,
MdOutlineViewTimeline,
MdSyncAlt,
MdHourglassBottom,
} from "react-icons/md";
import { BiBracket } from "react-icons/bi";
import URLSearchParamsWrapper from "src/utils/URLSearchParamWrapper";
Expand All @@ -60,6 +62,7 @@ import MarkRunAs from "./dagRun/MarkRunAs";
import ClearInstance from "./taskInstance/taskActions/ClearInstance";
import MarkInstanceAs from "./taskInstance/taskActions/MarkInstanceAs";
import XcomCollection from "./taskInstance/Xcom";
import TaskDetails from "./task";

const dagId = getMetaValue("dag_id")!;

Expand Down Expand Up @@ -92,7 +95,6 @@ const tabToIndex = (tab?: string) => {

const indexToTab = (
index: number,
taskId: string | null,
isTaskInstance: boolean,
isMappedTaskSummary: boolean
) => {
Expand Down Expand Up @@ -153,6 +155,7 @@ const Details = ({
!isGroup &&
!isMappedTaskSummary
);
const showTaskDetails = !!taskId && !runId;

const [searchParams, setSearchParams] = useSearchParams();
const tab = searchParams.get(TAB_PARAM) || undefined;
Expand All @@ -161,24 +164,20 @@ const Details = ({
const onChangeTab = useCallback(
(index: number) => {
const params = new URLSearchParamsWrapper(searchParams);
const newTab = indexToTab(
index,
taskId,
isTaskInstance,
isMappedTaskSummary
);
const newTab = indexToTab(index, isTaskInstance, isMappedTaskSummary);
if (newTab) params.set(TAB_PARAM, newTab);
else params.delete(TAB_PARAM);
setSearchParams(params);
},
[setSearchParams, searchParams, isTaskInstance, isMappedTaskSummary, taskId]
[setSearchParams, searchParams, isTaskInstance, isMappedTaskSummary]
);

useEffect(() => {
// Default to graph tab when navigating from a task instance to a group/dag/dagrun
const tabCount = runId && taskId && !isGroup ? 5 : 4;
if (tabCount === 4 && tabIndex > 3) {
onChangeTab(1);
if (!runId && taskId) onChangeTab(0);
else onChangeTab(1);
}
}, [runId, taskId, tabIndex, isGroup, onChangeTab]);

Expand Down Expand Up @@ -300,6 +299,28 @@ const Details = ({
</Text>
</Tab>
)}
{/* Match the styling of a tab but its actually a button */}
{!!taskId && !!runId && (
<Button
variant="unstyled"
display="flex"
alignItems="center"
fontSize="lg"
py={3}
// need to split pl and pr instead of px
pl={4}
pr={4}
mt="4px"
onClick={() => {
onSelect({ taskId });
}}
>
<MdHourglassBottom size={16} />
<Text as="strong" ml={1}>
Task Duration
</Text>
</Button>
)}
</TabList>
<TabPanels height="100%">
<TabPanel height="100%">
Expand All @@ -318,6 +339,7 @@ const Details = ({
/>
</>
)}
{showTaskDetails && <TaskDetails />}
</TabPanel>
<TabPanel p={0} height="100%">
<Graph
Expand Down