Skip to content

Commit

Permalink
Datasets UI Improvements (#40871)
Browse files Browse the repository at this point in the history
* Update Datasets page UX

* Cleanup selected dataset event code

* Use DatasetEventCard everywhere, use RenderedJson for extra, add links
  • Loading branch information
bbovenzi committed Jul 24, 2024
1 parent 254a0d7 commit 25e1cf1
Show file tree
Hide file tree
Showing 34 changed files with 1,099 additions and 611 deletions.
2 changes: 2 additions & 0 deletions airflow/www/jest-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,5 @@ global.filtersOptions = {
global.moment = moment;

global.standaloneDagProcessor = true;

global.autoRefreshInterval = undefined;
13 changes: 7 additions & 6 deletions airflow/www/static/js/api/useTaskInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import axios, { AxiosResponse } from "axios";
import type { API } from "src/types";
import { useQuery } from "react-query";
import { useQuery, UseQueryOptions } from "react-query";
import { useAutoRefresh } from "src/context/autorefresh";

import { getMetaValue } from "src/utils";
Expand All @@ -29,21 +29,22 @@ const taskInstanceApi = getMetaValue("task_instance_api");

interface Props
extends SetOptional<API.GetMappedTaskInstanceVariables, "mapIndex"> {
enabled?: boolean;
options?: UseQueryOptions<API.TaskInstance>;
}

const useTaskInstance = ({
dagId,
dagRunId,
taskId,
mapIndex,
enabled,
options,
}: Props) => {
let url: string = "";
if (taskInstanceApi) {
url = taskInstanceApi
.replace("_DAG_ID_", dagId)
.replace("_DAG_RUN_ID_", dagRunId)
.replace("_TASK_ID_", taskId || "");
.replace("_TASK_ID_", taskId);
}

if (mapIndex !== undefined && mapIndex >= 0) {
Expand All @@ -52,12 +53,12 @@ const useTaskInstance = ({

const { isRefreshOn } = useAutoRefresh();

return useQuery(
return useQuery<API.TaskInstance>(
["taskInstance", dagId, dagRunId, taskId, mapIndex],
() => axios.get<AxiosResponse, API.TaskInstance>(url),
{
refetchInterval: isRefreshOn && (autoRefreshInterval || 1) * 1000,
enabled,
...options,
}
);
};
Expand Down
183 changes: 183 additions & 0 deletions airflow/www/static/js/components/DatasetEventCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*!
* 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 React from "react";
import { isEmpty } from "lodash";
import { TbApi } from "react-icons/tb";

import type { DatasetEvent } from "src/types/api-generated";
import {
Box,
Flex,
Tooltip,
Text,
Grid,
GridItem,
Link,
} from "@chakra-ui/react";
import { HiDatabase } from "react-icons/hi";
import { FiLink } from "react-icons/fi";
import { useSearchParams } from "react-router-dom";

import { getMetaValue } from "src/utils";
import Time from "src/components/Time";
import { useContainerRef } from "src/context/containerRef";
import { SimpleStatus } from "src/dag/StatusBox";
import { formatDuration, getDuration } from "src/datetime_utils";
import RenderedJsonField from "src/components/RenderedJsonField";

import SourceTaskInstance from "./SourceTaskInstance";

type CardProps = {
datasetEvent: DatasetEvent;
};

const gridUrl = getMetaValue("grid_url");
const datasetsUrl = getMetaValue("datasets_url");

const DatasetEventCard = ({ datasetEvent }: CardProps) => {
const [searchParams] = useSearchParams();

const selectedUri = decodeURIComponent(searchParams.get("uri") || "");
const containerRef = useContainerRef();

const { fromRestApi, ...extra } = datasetEvent?.extra as Record<
string,
string
>;

return (
<Box>
<Grid
templateColumns="repeat(4, 1fr)"
key={`${datasetEvent.datasetId}-${datasetEvent.timestamp}`}
_hover={{ bg: "gray.50" }}
transition="background-color 0.2s"
p={2}
borderTopWidth={1}
borderColor="gray.300"
borderStyle="solid"
>
<GridItem colSpan={2}>
<Time dateTime={datasetEvent.timestamp} />
<Flex alignItems="center">
<HiDatabase size="16px" />
{datasetEvent.datasetUri &&
datasetEvent.datasetUri !== selectedUri ? (
<Link
color="blue.600"
ml={2}
href={`${datasetsUrl}?uri=${encodeURIComponent(
datasetEvent.datasetUri
)}`}
>
{datasetEvent.datasetUri}
</Link>
) : (
<Text ml={2}>{datasetEvent.datasetUri}</Text>
)}
</Flex>
</GridItem>
<GridItem>
Source:
{fromRestApi && (
<Tooltip
portalProps={{ containerRef }}
hasArrow
placement="top"
label="Manually created from REST API"
>
<Box width="20px">
<TbApi size="20px" />
</Box>
</Tooltip>
)}
{!!datasetEvent.sourceTaskId && (
<SourceTaskInstance datasetEvent={datasetEvent} />
)}
</GridItem>
<GridItem>
{!!datasetEvent?.createdDagruns?.length && (
<>
Triggered Dag Runs:
<Flex alignItems="center">
{datasetEvent?.createdDagruns.map((run) => {
const runId = (run as any).dagRunId; // For some reason the type is wrong here
const url = `${gridUrl?.replace(
"__DAG_ID__",
run.dagId || ""
)}?dag_run_id=${encodeURIComponent(runId)}`;

return (
<Tooltip
key={runId}
label={
<Box>
<Text>DAG Id: {run.dagId}</Text>
<Text>Status: {run.state || "no status"}</Text>
<Text>
Duration:{" "}
{formatDuration(
getDuration(run.startDate, run.endDate)
)}
</Text>
<Text>
Start Date: <Time dateTime={run.startDate} />
</Text>
{run.endDate && (
<Text>
End Date: <Time dateTime={run.endDate} />
</Text>
)}
</Box>
}
portalProps={{ containerRef }}
hasArrow
placement="top"
>
<Flex width="30px">
<SimpleStatus state={run.state} mx={1} />
<Link color="blue.600" href={url}>
<FiLink size="12px" />
</Link>
</Flex>
</Tooltip>
);
})}
</Flex>
</>
)}
</GridItem>
</Grid>
{!isEmpty(extra) && (
<RenderedJsonField
content={extra}
bg="gray.100"
maxH="300px"
overflow="auto"
jsonProps={{
collapsed: true,
}}
/>
)}
</Box>
);
};

export default DatasetEventCard;
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,22 @@ import { formatDuration, getDuration } from "src/datetime_utils";
import type { TaskInstance, Task } from "src/types";
import Time from "src/components/Time";

type Instance = Pick<
TaskInstance,
| "taskId"
| "startDate"
| "endDate"
| "state"
| "runId"
| "mappedStates"
| "note"
| "tryNumber"
>;

interface Props {
group: Task;
instance: TaskInstance;
group?: Task;
instance: Instance;
dagId?: string;
}

const InstanceTooltip = ({
Expand All @@ -43,38 +56,43 @@ const InstanceTooltip = ({
note,
tryNumber,
},
dagId,
}: Props) => {
if (!group) return null;
const isGroup = !!group.children;
const { isMapped } = group;
const isGroup = !!group?.children;
const isMapped = !!group?.isMapped;
const summary: React.ReactNode[] = [];

const { totalTasks, childTaskMap } = getGroupAndMapSummary({
group,
runId,
mappedStates,
});
let totalTasks = 1;
if (group) {
const { totalTasks: total, childTaskMap } = getGroupAndMapSummary({
group,
runId,
mappedStates,
});
totalTasks = total;

childTaskMap.forEach((key, val) => {
const childState = snakeCase(val);
if (key > 0) {
summary.push(
<Text key={childState} ml="10px">
{childState}
{": "}
{key}
</Text>
);
}
});
childTaskMap.forEach((key, val) => {
const childState = snakeCase(val);
if (key > 0) {
summary.push(
<Text key={childState} ml="10px">
{childState}
{": "}
{key}
</Text>
);
}
});
}

return (
<Box py="2px">
{!!dagId && <Text>DAG Id: {dagId}</Text>}
<Text>Task Id: {taskId}</Text>
{!!group.setupTeardownType && (
{!!group?.setupTeardownType && (
<Text>Type: {group.setupTeardownType}</Text>
)}
{group.tooltip && <Text>{group.tooltip}</Text>}
{group?.tooltip && <Text>{group.tooltip}</Text>}
{isMapped && totalTasks > 0 && (
<Text>
{totalTasks} mapped task
Expand Down Expand Up @@ -103,7 +121,7 @@ const InstanceTooltip = ({
</>
)}
{tryNumber && tryNumber > 1 && <Text>Try Number: {tryNumber}</Text>}
{group.triggerRule && <Text>Trigger Rule: {group.triggerRule}</Text>}
{group?.triggerRule && <Text>Trigger Rule: {group.triggerRule}</Text>}
{note && <Text>Contains a note</Text>}
</Box>
);
Expand Down
20 changes: 13 additions & 7 deletions airflow/www/static/js/components/RenderedJsonField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import React from "react";

import ReactJson from "react-json-view";
import ReactJson, { ReactJsonViewProps } from "react-json-view";

import {
Flex,
Expand All @@ -32,15 +32,20 @@ import {
} from "@chakra-ui/react";

interface Props extends FlexProps {
content: string;
content: string | object;
jsonProps?: Omit<ReactJsonViewProps, "src">;
}

const JsonParse = (content: string) => {
const JsonParse = (content: string | object) => {
let contentJson = null;
let contentFormatted = "";
let isJson = false;
try {
contentJson = JSON.parse(content);
if (typeof content === "string") {
contentJson = JSON.parse(content);
} else {
contentJson = content;
}
contentFormatted = JSON.stringify(contentJson, null, 4);
isJson = true;
} catch (e) {
Expand All @@ -49,7 +54,7 @@ const JsonParse = (content: string) => {
return [isJson, contentJson, contentFormatted];
};

const RenderedJsonField = ({ content, ...rest }: Props) => {
const RenderedJsonField = ({ content, jsonProps, ...rest }: Props) => {
const [isJson, contentJson, contentFormatted] = JsonParse(content);
const { onCopy, hasCopied } = useClipboard(contentFormatted);
const theme = useTheme();
Expand All @@ -69,14 +74,15 @@ const RenderedJsonField = ({ content, ...rest }: Props) => {
fontSize: theme.fontSizes.md,
font: theme.fonts.mono,
}}
{...jsonProps}
/>
<Spacer />
<Button aria-label="Copy" onClick={onCopy}>
<Button aria-label="Copy" onClick={onCopy} position="sticky" top={0}>
{hasCopied ? "Copied!" : "Copy"}
</Button>
</Flex>
) : (
<Code fontSize="md">{content}</Code>
<Code fontSize="md">{content as string}</Code>
);
};

Expand Down
Loading

0 comments on commit 25e1cf1

Please sign in to comment.