Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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