Skip to content
Closed
57 changes: 45 additions & 12 deletions airflow-core/src/airflow/ui/src/pages/DagsList/AssetSchedule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import dayjs from "dayjs";
import { useTranslation } from "react-i18next";
import { FiDatabase } from "react-icons/fi";
import { Link as RouterLink } from "react-router-dom";

import { useAssetServiceNextRunAssets } from "openapi/queries";
import { AssetExpression, type ExpressionType } from "src/components/AssetExpression";
import type { NextRunEvent } from "src/components/AssetExpression/types";
Expand All @@ -34,17 +33,19 @@ type Props = {
readonly timetableSummary: string | null;
};

export const AssetSchedule = ({ assetExpression, dagId, latestRunAfter, timetableSummary }: Props) => {
export const AssetSchedule = ({
assetExpression,
dagId,
latestRunAfter,
timetableSummary,
}: Props) => {
const { t: translate } = useTranslation("dags");
const { data: nextRun, isLoading } = useAssetServiceNextRunAssets({ dagId });

const nextRunEvents = (nextRun?.events ?? []) as Array<NextRunEvent>;

const pendingEvents = nextRunEvents.filter((ev) => {
if (ev.lastUpdate !== null && latestRunAfter !== undefined) {
return dayjs(ev.lastUpdate).isAfter(latestRunAfter);
}

return false;
});

Expand All @@ -63,28 +64,60 @@ export const AssetSchedule = ({ assetExpression, dagId, latestRunAfter, timetabl
return (
<HStack>
<FiDatabase style={{ display: "inline" }} />
<Link asChild color="fg.info" display="block" fontSize="sm" maxWidth="200px" truncate>
<RouterLink to={`/assets/${asset.id}`}>{asset.name ?? asset.uri}</RouterLink>
<Link
asChild
color="fg.info"
display="block"
fontSize="sm"
maxWidth="200px"
isTruncated
>
<RouterLink
to={`/assets/${asset.id}`}
title={asset.name ?? asset.uri}
>
{asset.name ?? asset.uri}
</RouterLink>
</Link>
</HStack>
);
}

return (
// eslint-disable-next-line jsx-a11y/no-autofocus
<Popover.Root autoFocus={false} lazyMount positioning={{ placement: "bottom-end" }} unmountOnExit>
<Popover.Root
autoFocus={false}
lazyMount
positioning={{ placement: "bottom-end" }}
unmountOnExit
>
<Popover.Trigger asChild>
<Button loading={isLoading} paddingInline={0} size="sm" variant="ghost">
<Button
loading={isLoading}
paddingInline={0}
size="sm"
variant="ghost"
>
<FiDatabase style={{ display: "inline" }} />
{translate("assetSchedule", { count: pendingEvents.length, total: nextRunEvents.length })}
{translate("assetSchedule", {
count: pendingEvents.length,
total: nextRunEvents.length,
})}
</Button>
</Popover.Trigger>
<Popover.Content css={{ "--popover-bg": "colors.bg.emphasized" }} width="fit-content">
<Popover.Content
css={{
"--popover-bg": "colors.bg.emphasized",
}}
width="fit-content"
>
<Popover.Arrow />
<Popover.Body>
<AssetExpression
events={pendingEvents}
expression={(nextRun?.asset_expression ?? assetExpression) as ExpressionType}
expression={
(nextRun?.asset_expression ?? assetExpression) as ExpressionType
}
/>
</Popover.Body>
</Popover.Content>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { render, screen } from '@testing-library/react';
import { describe, test, expect, vi } from 'vitest';
import AssetSchedule from '../AssetSchedule';
import { Wrapper } from 'src/utils/Wrapper';

const longName = 'VeryLongAssetNameWhichShouldBeTruncatedBecauseItIsTooLongToFitInList';

// Mock translation function
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (str: string) => str,
}),
}));

// Mock hook for next run assets
vi.mock('openapi/queries', () => ({
useAssetServiceNextRunAssets: () => ({
data: {
events: [
{
id: 'asset1',
name: longName,
uri: longName,
lastUpdate: null,
},
],
},
isLoading: false,
}),
}));

describe('AssetSchedule', () => {
test('truncates long asset names using isTruncated', () => {
render(
<AssetSchedule dagId="testDag" timetableSummary="test summary" />,
{ wrapper: Wrapper },
);
const linkElement = screen.getByText(longName);
expect(linkElement).toHaveAttribute('title', longName);
});
});
Loading