This is an automated email from the ASF dual-hosted git repository.
bbovenzi 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 f48e8efae8 Show abandoned tasks in Grid View (#38511)
f48e8efae8 is described below
commit f48e8efae8591741c5c25081a93c4216fff7d697
Author: Brent Bovenzi <[email protected]>
AuthorDate: Tue Mar 26 14:12:22 2024 -0700
Show abandoned tasks in Grid View (#38511)
* Handle abandoned tasks in grid view
* Prevent actions on abandoned tasks
* Fix links from taskinstance/list
---
airflow/www/static/js/dag/details/Header.tsx | 4 +-
.../www/static/js/dag/details/NotesAccordion.tsx | 4 +-
airflow/www/static/js/dag/details/index.tsx | 5 +-
.../static/js/dag/details/taskInstance/Details.tsx | 132 +++++-----
.../js/dag/details/taskInstance/Logs/index.tsx | 266 ++++++++++-----------
.../static/js/dag/details/taskInstance/index.tsx | 40 ++--
airflow/www/utils.py | 14 +-
airflow/www/views.py | 18 +-
8 files changed, 261 insertions(+), 222 deletions(-)
diff --git a/airflow/www/static/js/dag/details/Header.tsx
b/airflow/www/static/js/dag/details/Header.tsx
index 1903574630..06c1e2dd11 100644
--- a/airflow/www/static/js/dag/details/Header.tsx
+++ b/airflow/www/static/js/dag/details/Header.tsx
@@ -64,8 +64,6 @@ const Header = () => {
clearSelection();
} else if (runId && !dagRun) {
onSelect({ taskId });
- } else if (taskId && !group) {
- onSelect({ runId });
}
}, [dagRun, taskId, group, runId, onSelect, clearSelection]);
@@ -99,7 +97,7 @@ const Header = () => {
const isMappedTaskDetails = runId && taskId && mapIndex !== undefined;
return (
- <Breadcrumb ml={3} separator={<Text color="gray.300">/</Text>}>
+ <Breadcrumb ml={3} pt={2} separator={<Text color="gray.300">/</Text>}>
<BreadcrumbItem isCurrentPage={isDagDetails} mt={4}>
<BreadcrumbLink
onClick={clearSelection}
diff --git a/airflow/www/static/js/dag/details/NotesAccordion.tsx
b/airflow/www/static/js/dag/details/NotesAccordion.tsx
index 5f1c00d7a2..dfe47a2a52 100644
--- a/airflow/www/static/js/dag/details/NotesAccordion.tsx
+++ b/airflow/www/static/js/dag/details/NotesAccordion.tsx
@@ -46,6 +46,7 @@ interface Props {
taskId?: string;
mapIndex?: number;
initialValue?: string | null;
+ isAbandonedTask?: boolean;
}
const NotesAccordion = ({
@@ -54,8 +55,9 @@ const NotesAccordion = ({
taskId,
mapIndex,
initialValue,
+ isAbandonedTask,
}: Props) => {
- const canEdit = getMetaValue("can_edit") === "True";
+ const canEdit = getMetaValue("can_edit") === "True" && !isAbandonedTask;
const [note, setNote] = useState(initialValue ?? "");
const [editMode, setEditMode] = useState(false);
const [accordionIndexes, setAccordionIndexes] = useState<Array<number>>(
diff --git a/airflow/www/static/js/dag/details/index.tsx
b/airflow/www/static/js/dag/details/index.tsx
index 0305fe0a2d..3e8b244c24 100644
--- a/airflow/www/static/js/dag/details/index.tsx
+++ b/airflow/www/static/js/dag/details/index.tsx
@@ -185,6 +185,8 @@ const Details = ({
const showTaskDetails = !!taskId && !runId;
+ const isAbandonedTask = !!taskId && !group;
+
const [searchParams, setSearchParams] = useSearchParams();
const tab = searchParams.get(TAB_PARAM) || undefined;
const tabIndex = tabToIndex(tab);
@@ -253,7 +255,7 @@ const Details = ({
<MarkRunAs runId={runId} state={run?.state} />
</>
)}
- {runId && taskId && (
+ {runId && taskId && !isAbandonedTask && (
<>
<ClearInstance
taskId={taskId}
@@ -379,6 +381,7 @@ const Details = ({
onChangeTab(0);
onSelect({ taskId });
}}
+ isDisabled={isAbandonedTask}
>
<MdHourglassBottom size={16} />
<Text as="strong" ml={1}>
diff --git a/airflow/www/static/js/dag/details/taskInstance/Details.tsx
b/airflow/www/static/js/dag/details/taskInstance/Details.tsx
index cc4dea6e96..72d8273ce6 100644
--- a/airflow/www/static/js/dag/details/taskInstance/Details.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/Details.tsx
@@ -34,45 +34,62 @@ import type {
} from "src/types";
interface Props {
- gridInstance: GridTaskInstance;
+ gridInstance?: GridTaskInstance;
taskInstance?: API.TaskInstance;
- group: Task;
+ group?: Task | null;
}
const Details = ({ gridInstance, taskInstance, group }: Props) => {
- const isGroup = !!group.children;
+ const isGroup = !!group?.children;
const summary: React.ReactNode[] = [];
- const { taskId, runId, startDate, endDate, state } = gridInstance;
+ const state =
+ gridInstance?.state ||
+ (taskInstance?.state === "none" ? null : taskInstance?.state) ||
+ null;
+ const isMapped = group?.isMapped;
+ const runId = gridInstance?.runId || taskInstance?.dagRunId;
+ const startDate = gridInstance?.startDate || taskInstance?.startDate;
+ const endDate = gridInstance?.endDate || taskInstance?.endDate;
+ const taskId = gridInstance?.taskId || taskInstance?.taskId;
+ const mapIndex = gridInstance?.mapIndex || taskInstance?.mapIndex;
- const mappedStates = !taskInstance ? gridInstance.mappedStates : undefined;
+ const operator = group?.operator || taskInstance?.operator;
- const { isMapped, tooltip, operator, triggerRule } = group;
+ const mappedStates = !taskInstance ? gridInstance?.mappedStates : undefined;
- const { totalTasks, childTaskMap } = getGroupAndMapSummary({
- group,
- runId,
- mappedStates,
- });
+ let totalTasks;
+ let childTaskMap;
- childTaskMap.forEach((key, val) => {
- const childState = snakeCase(val);
- if (key > 0) {
- summary.push(
- <Tr key={childState}>
- <Td />
- <Td>
- <Flex alignItems="center">
- <SimpleStatus state={childState as TaskState} mx={2} />
- {childState}
- {": "}
- {key}
- </Flex>
- </Td>
- </Tr>
- );
- }
- });
+ if (group) {
+ const groupAndMapSummary = getGroupAndMapSummary({
+ group,
+ runId,
+ mappedStates,
+ });
+
+ totalTasks = groupAndMapSummary.totalTasks;
+ childTaskMap = groupAndMapSummary.childTaskMap;
+
+ childTaskMap.forEach((key, val) => {
+ const childState = snakeCase(val);
+ if (key > 0) {
+ summary.push(
+ <Tr key={childState}>
+ <Td />
+ <Td>
+ <Flex alignItems="center">
+ <SimpleStatus state={childState as TaskState} mx={2} />
+ {childState}
+ {": "}
+ {key}
+ </Flex>
+ </Td>
+ </Tr>
+ );
+ }
+ });
+ }
const taskIdTitle = isGroup ? "Task Group ID" : "Task ID";
const isStateFinal =
@@ -87,9 +104,9 @@ const Details = ({ gridInstance, taskInstance, group }:
Props) => {
</Text>
<Table variant="striped">
<Tbody>
- {tooltip && (
+ {group?.tooltip && (
<Tr>
- <Td colSpan={2}>{tooltip}</Td>
+ <Td colSpan={2}>{group.tooltip}</Td>
</Tr>
)}
<Tr>
@@ -104,7 +121,7 @@ const Details = ({ gridInstance, taskInstance, group }:
Props) => {
</Flex>
</Td>
</Tr>
- {!!group.setupTeardownType && (
+ {!!group?.setupTeardownType && (
<Tr>
<Td>Type</Td>
<Td>
@@ -114,7 +131,7 @@ const Details = ({ gridInstance, taskInstance, group }:
Props) => {
</Td>
</Tr>
)}
- {mappedStates && totalTasks > 0 && (
+ {mappedStates && !!totalTasks && totalTasks > 0 && (
<Tr>
<Td colSpan={2}>
{totalTasks} {isGroup ? "Task Group" : "Task"}
@@ -124,32 +141,37 @@ const Details = ({ gridInstance, taskInstance, group }:
Props) => {
</Tr>
)}
{summary.length > 0 && summary}
- <Tr>
- <Td>{taskIdTitle}</Td>
- <Td>
- <ClipboardText value={taskId} />
- </Td>
- </Tr>
- <Tr>
- <Td>Run ID</Td>
- <Td>
- <Text whiteSpace="nowrap">
- <ClipboardText value={runId} />
- </Text>
- </Td>
- </Tr>
- {taskInstance?.mapIndex !== undefined && (
+ {!!taskId && (
<Tr>
- <Td>Map Index</Td>
- <Td>{taskInstance.mapIndex}</Td>
+ <Td>{taskIdTitle}</Td>
+ <Td>
+ <ClipboardText value={taskId} />
+ </Td>
+ </Tr>
+ )}
+ {!!runId && (
+ <Tr>
+ <Td>Run ID</Td>
+ <Td>
+ <Text whiteSpace="nowrap">
+ <ClipboardText value={runId} />
+ </Text>
+ </Td>
</Tr>
)}
- {taskInstance?.renderedMapIndex !== undefined && (
+ {mapIndex !== undefined && (
<Tr>
- <Td>Rendered Map Index</Td>
- <Td>{taskInstance.renderedMapIndex}</Td>
+ <Td>Map Index</Td>
+ <Td>{mapIndex}</Td>
</Tr>
)}
+ {taskInstance?.renderedMapIndex !== undefined &&
+ taskInstance?.renderedMapIndex !== null && (
+ <Tr>
+ <Td>Rendered Map Index</Td>
+ <Td>{taskInstance.renderedMapIndex}</Td>
+ </Tr>
+ )}
{!!taskInstance?.tryNumber && (
<Tr>
<Td>Try Number</Td>
@@ -162,10 +184,10 @@ const Details = ({ gridInstance, taskInstance, group }:
Props) => {
<Td>{operator}</Td>
</Tr>
)}
- {triggerRule && (
+ {group?.triggerRule && (
<Tr>
<Td>Trigger Rule</Td>
- <Td>{triggerRule}</Td>
+ <Td>{group.triggerRule}</Td>
</Tr>
)}
{startDate && (
diff --git a/airflow/www/static/js/dag/details/taskInstance/Logs/index.tsx
b/airflow/www/static/js/dag/details/taskInstance/Logs/index.tsx
index 9e664fda20..4a4013a2dd 100644
--- a/airflow/www/static/js/dag/details/taskInstance/Logs/index.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/Logs/index.tsx
@@ -201,146 +201,142 @@ const Logs = ({
</Flex>
</Box>
)}
- {tryNumber !== undefined && (
- <>
+ <Box>
+ {!showDropdown && (
<Box>
- {!showDropdown && (
- <Box>
- <Text as="span"> (by attempts)</Text>
- <Flex my={1} justifyContent="space-between">
- <Flex flexWrap="wrap">
- {internalIndexes.map((index) => (
- <Button
- key={index}
- variant={taskTryNumber === index ? "solid" : "ghost"}
- colorScheme="blue"
- onClick={() => setSelectedTryNumber(index)}
- data-testid={`log-attempt-select-button-${index}`}
- >
- {index}
- </Button>
- ))}
- </Flex>
- </Flex>
- </Box>
- )}
- <Flex my={1} justifyContent="space-between" flexWrap="wrap">
- <Flex alignItems="center" flexGrow={1} mr={10}>
- {showDropdown && (
- <Box width="100%" mr={2}>
- <Select
- size="sm"
- placeholder="Select log attempt"
- onChange={(e) => {
- setSelectedTryNumber(Number(e.target.value));
- }}
- >
- {internalIndexes.map((index) => (
- <option key={index} value={index}>
- {index}
- </option>
- ))}
- </Select>
- </Box>
- )}
- <Box width="100%" mr={2}>
- <MultiSelect
- size="sm"
- isMulti
- options={logLevelOptions}
- placeholder="All Levels"
- value={logLevelFilters}
- onChange={(options) => setLogLevelFilters([...options])}
- chakraStyles={{
- multiValue: (provided, ...rest) => ({
- ...provided,
- backgroundColor: rest[0].data.color,
- }),
- option: (provided, ...rest) => ({
- ...provided,
- borderLeft: "solid 4px black",
- borderColor: rest[0].data.color,
- mt: 2,
- }),
- }}
- />
- </Box>
- <Box width="100%">
- <MultiSelect
- size="sm"
- isMulti
- options={fileSources.map((fileSource) => ({
- label: fileSource,
- value: fileSource,
- }))}
- placeholder="All File Sources"
- value={fileSourceFilters}
- onChange={(options) => setFileSourceFilters([...options])}
- />
- </Box>
- </Flex>
- <Flex alignItems="center" flexWrap="wrap">
- <Checkbox
- isChecked={wrap}
- onChange={() => setWrap((previousState) => !previousState)}
- px={4}
- data-testid="wrap-checkbox"
- >
- <Text as="strong">Wrap</Text>
- </Checkbox>
- <LogLink
- dagId={dagId}
- taskId={taskId}
- executionDate={executionDate}
- isInternal
- tryNumber={tryNumber}
- mapIndex={mapIndex}
- />
- <LinkButton href={`${logUrl}&${params.toString()}`}>
- See More
- </LinkButton>
- <IconButton
- variant="ghost"
- aria-label="Toggle full screen"
- title="Toggle full screen"
- onClick={toggleFullScreen}
- icon={
- isFullScreen ? (
- <BiCollapse height="24px" />
- ) : (
- <BiExpand height="24px" />
- )
- }
- />
+ <Text as="span"> (by attempts)</Text>
+ <Flex my={1} justifyContent="space-between">
+ <Flex flexWrap="wrap">
+ {internalIndexes.map((index) => (
+ <Button
+ key={index}
+ variant={taskTryNumber === index ? "solid" : "ghost"}
+ colorScheme="blue"
+ onClick={() => setSelectedTryNumber(index)}
+ data-testid={`log-attempt-select-button-${index}`}
+ >
+ {index}
+ </Button>
+ ))}
</Flex>
</Flex>
</Box>
- {!!warning && (
- <Flex
- bg="yellow.200"
- borderRadius={2}
- borderColor="gray.400"
- alignItems="center"
- p={2}
- >
- <Icon as={MdWarning} color="yellow.500" mr={2} />
- <Text fontSize="sm">{warning}</Text>
- </Flex>
- )}
- {isLoading ? (
- <Spinner />
- ) : (
- !!parsedLogs && (
- <LogBlock
- parsedLogs={parsedLogs}
- wrap={wrap}
- tryNumber={taskTryNumber}
- unfoldedGroups={unfoldedLogGroups}
- setUnfoldedLogGroup={setUnfoldedLogGroup}
+ )}
+ <Flex my={1} justifyContent="space-between" flexWrap="wrap">
+ <Flex alignItems="center" flexGrow={1} mr={10}>
+ {showDropdown && (
+ <Box width="100%" mr={2}>
+ <Select
+ size="sm"
+ placeholder="Select log attempt"
+ onChange={(e) => {
+ setSelectedTryNumber(Number(e.target.value));
+ }}
+ >
+ {internalIndexes.map((index) => (
+ <option key={index} value={index}>
+ {index}
+ </option>
+ ))}
+ </Select>
+ </Box>
+ )}
+ <Box width="100%" mr={2}>
+ <MultiSelect
+ size="sm"
+ isMulti
+ options={logLevelOptions}
+ placeholder="All Levels"
+ value={logLevelFilters}
+ onChange={(options) => setLogLevelFilters([...options])}
+ chakraStyles={{
+ multiValue: (provided, ...rest) => ({
+ ...provided,
+ backgroundColor: rest[0].data.color,
+ }),
+ option: (provided, ...rest) => ({
+ ...provided,
+ borderLeft: "solid 4px black",
+ borderColor: rest[0].data.color,
+ mt: 2,
+ }),
+ }}
/>
- )
- )}
- </>
+ </Box>
+ <Box width="100%">
+ <MultiSelect
+ size="sm"
+ isMulti
+ options={fileSources.map((fileSource) => ({
+ label: fileSource,
+ value: fileSource,
+ }))}
+ placeholder="All File Sources"
+ value={fileSourceFilters}
+ onChange={(options) => setFileSourceFilters([...options])}
+ />
+ </Box>
+ </Flex>
+ <Flex alignItems="center" flexWrap="wrap">
+ <Checkbox
+ isChecked={wrap}
+ onChange={() => setWrap((previousState) => !previousState)}
+ px={4}
+ data-testid="wrap-checkbox"
+ >
+ <Text as="strong">Wrap</Text>
+ </Checkbox>
+ <LogLink
+ dagId={dagId}
+ taskId={taskId}
+ executionDate={executionDate}
+ isInternal
+ tryNumber={tryNumber}
+ mapIndex={mapIndex}
+ />
+ <LinkButton href={`${logUrl}&${params.toString()}`}>
+ See More
+ </LinkButton>
+ <IconButton
+ variant="ghost"
+ aria-label="Toggle full screen"
+ title="Toggle full screen"
+ onClick={toggleFullScreen}
+ icon={
+ isFullScreen ? (
+ <BiCollapse height="24px" />
+ ) : (
+ <BiExpand height="24px" />
+ )
+ }
+ />
+ </Flex>
+ </Flex>
+ </Box>
+ {!!warning && (
+ <Flex
+ bg="yellow.200"
+ borderRadius={2}
+ borderColor="gray.400"
+ alignItems="center"
+ p={2}
+ >
+ <Icon as={MdWarning} color="yellow.500" mr={2} />
+ <Text fontSize="sm">{warning}</Text>
+ </Flex>
+ )}
+ {isLoading ? (
+ <Spinner />
+ ) : (
+ !!parsedLogs && (
+ <LogBlock
+ parsedLogs={parsedLogs}
+ wrap={wrap}
+ tryNumber={taskTryNumber}
+ unfoldedGroups={unfoldedLogGroups}
+ setUnfoldedLogGroup={setUnfoldedLogGroup}
+ />
+ )
)}
</>
);
diff --git a/airflow/www/static/js/dag/details/taskInstance/index.tsx
b/airflow/www/static/js/dag/details/taskInstance/index.tsx
index 1b8f0a54ba..83681cb070 100644
--- a/airflow/www/static/js/dag/details/taskInstance/index.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/index.tsx
@@ -65,11 +65,8 @@ const TaskInstance = ({ taskId, runId, mapIndex }: Props) =>
{
mapIndex,
enabled: (!isGroup && !isMapped) || isMapIndexDefined,
});
- const gridInstance = group?.instances.find((ti) => ti.runId === runId);
-
- if (!group || !run || !gridInstance) return null;
- const { executionDate } = run;
+ const gridInstance = group?.instances.find((ti) => ti.runId === runId);
return (
<Box
@@ -79,12 +76,12 @@ const TaskInstance = ({ taskId, runId, mapIndex }: Props)
=> {
ref={taskInstanceRef}
overflowY="auto"
>
- {!isGroup && (
+ {!isGroup && run?.executionDate && (
<TaskNav
taskId={taskId}
isMapped={isMapped}
mapIndex={mapIndex}
- executionDate={executionDate}
+ executionDate={run?.executionDate}
operator={operator}
/>
)}
@@ -93,22 +90,25 @@ const TaskInstance = ({ taskId, runId, mapIndex }: Props)
=> {
dagId={dagId}
runId={runId}
taskId={taskId}
- mapIndex={gridInstance.mapIndex}
- initialValue={gridInstance.note}
- key={dagId + runId + taskId + gridInstance.mapIndex}
- />
- )}
- {!!group.extraLinks?.length && !isGroupOrMappedTaskSummary && (
- <ExtraLinks
- taskId={taskId}
- dagId={dagId}
- mapIndex={isMapped && isMapIndexDefined ? mapIndex : undefined}
- executionDate={executionDate}
- extraLinks={group?.extraLinks}
- tryNumber={taskInstance?.tryNumber || gridInstance.tryNumber}
+ mapIndex={mapIndex}
+ initialValue={gridInstance?.note || taskInstance?.note}
+ key={dagId + runId + taskId + mapIndex}
+ isAbandonedTask={!!taskId && !group}
/>
)}
- {group.hasOutletDatasets && (
+ {!!group?.extraLinks?.length &&
+ !isGroupOrMappedTaskSummary &&
+ run?.executionDate && (
+ <ExtraLinks
+ taskId={taskId}
+ dagId={dagId}
+ mapIndex={isMapped && isMapIndexDefined ? mapIndex : undefined}
+ executionDate={run.executionDate}
+ extraLinks={group.extraLinks}
+ tryNumber={taskInstance?.tryNumber || gridInstance?.tryNumber || 1}
+ />
+ )}
+ {group?.hasOutletDatasets && (
<DatasetUpdateEvents taskId={taskId} runId={runId} />
)}
<TriggererInfo taskInstance={taskInstance} />
diff --git a/airflow/www/utils.py b/airflow/www/utils.py
index 8cbb19d176..d73bff9a64 100644
--- a/airflow/www/utils.py
+++ b/airflow/www/utils.py
@@ -423,13 +423,17 @@ def task_instance_link(attr):
dag_id = attr.get("dag_id")
task_id = attr.get("task_id")
run_id = attr.get("run_id")
- execution_date = attr.get("dag_run.execution_date") or
attr.get("execution_date") or timezone.utcnow()
+ map_index = attr.get("map_index", None)
+ if map_index == -1:
+ map_index = None
+
url = url_for(
- "Airflow.task",
+ "Airflow.grid",
dag_id=dag_id,
task_id=task_id,
- execution_date=execution_date.isoformat(),
- map_index=attr.get("map_index", -1),
+ dag_run_id=run_id,
+ map_index=map_index,
+ tab="graph",
)
url_root = url_for(
"Airflow.grid",
@@ -437,8 +441,8 @@ def task_instance_link(attr):
task_id=task_id,
root=task_id,
dag_run_id=run_id,
+ map_index=map_index,
tab="graph",
- map_index=attr.get("map_index", -1),
)
return Markup(
"""
diff --git a/airflow/www/views.py b/airflow/www/views.py
index 9d66fe5bda..17eabf3902 100644
--- a/airflow/www/views.py
+++ b/airflow/www/views.py
@@ -5187,10 +5187,24 @@ class TaskInstanceModelView(AirflowModelView):
def log_url_formatter(self):
"""Format log URL."""
- log_url = self.get("log_url")
+ dag_id = self.get("dag_id")
+ task_id = self.get("task_id")
+ run_id = self.get("run_id")
+ map_index = self.get("map_index", None)
+ if map_index == -1:
+ map_index = None
+
+ url = url_for(
+ "Airflow.grid",
+ dag_id=dag_id,
+ task_id=task_id,
+ dag_run_id=run_id,
+ map_index=map_index,
+ tab="logs",
+ )
return Markup(
'<a href="{log_url}"><span class="material-icons"
aria-hidden="true">reorder</span></a>'
- ).format(log_url=log_url)
+ ).format(log_url=url)
def duration_f(self):
"""Format duration."""