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
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,14 @@
},
"filter": "Filter",
"filters": {
"addFilter": "Add Filter",
"date": "Date",
"durationFrom": "Duration From",
"durationTo": "Duration To",
"endTime": "End Time",
"logicalDateFrom": "Logical Date From",
"logicalDateTo": "Logical Date To",
"removeFilter": "Remove Filter",
"runAfterFrom": "Run After From",
"runAfterTo": "Run After To",
"searchAsset": "Search Asset",
Expand Down Expand Up @@ -366,8 +368,10 @@
"filterReset_other": "Reset filters",
"from": "From",
"maxActiveRuns": "Max Active Runs",
"noResultsFound": "No results found",
"noTagsFound": "No tags found",
"noTeamsFound": "No teams found",
"ownerPlaceholder": "Filter by owner",
"tagMode": {
"all": "All",
"any": "Any"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,25 @@
"allRunTypes": "All Run Types",
"allStates": "All States",
"anyRunState": "Any run",
"anyRunStatePlaceholder": "Select any run state…",
"favorite": {
"all": "All",
"favorite": "Favorite",
"unfavorite": "Unfavorite"
},
"favoriteState": "Favorite state",
"favoriteStatePlaceholder": "Select favorite state…",
"lastRunState": "Last run",
"lastRunStatePlaceholder": "Select last run state…",
"noTimetableTypesFound": "No timetable types found",
"paused": {
"active": "Active",
"all": "All",
"paused": "Paused"
},
"pausedPlaceholder": "Select paused state…",
"pausedState": "Paused state",
"requiresHitlAction": "Requires HITL action",
"runIdPatternFilter": "Search Dag Runs",
"timetableType": "Timetable type"
},
Expand Down
86 changes: 39 additions & 47 deletions airflow-core/src/airflow/ui/src/components/Assets/AssetEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
Box,
Heading,
Flex,
HStack,
Skeleton,
Separator,
type BoxProps,
createListCollection,
} from "@chakra-ui/react";
import { Box, createListCollection, Heading, HStack, Skeleton, type BoxProps } from "@chakra-ui/react";
import { useTranslation } from "react-i18next";
import { FiDatabase } from "react-icons/fi";

Expand Down Expand Up @@ -76,54 +67,55 @@ export const AssetEvents = ({
});

return (
<Box borderBottomWidth={0} borderRadius={8} borderWidth={1} p={4} py={2} {...rest}>
<Flex alignItems="center" flexWrap="wrap" justify="space-between">
<HStack>
<StateBadge colorPalette="brand" fontSize="md" variant="solid">
<FiDatabase />
{data?.total_entries ?? " "}
</StateBadge>
<Heading marginEnd="auto" size="md">
{translate(titleKey ?? "common:assetEvent", { count: data?.total_entries ?? 0 })}
</Heading>
</HStack>
{setOrderBy === undefined ? undefined : (
<Select.Root
borderWidth={0}
collection={assetSortOptions}
data-testid="asset-sort-duration"
defaultValue={["-timestamp"]}
onValueChange={(option) => setOrderBy(option.value[0] as string)}
size="sm"
width={130}
>
<Select.Trigger>
<Select.ValueText placeholder={translate("dashboard:sortBy.placeholder")} />
</Select.Trigger>

<Select.Content>
{assetSortOptions.items.map((option) => (
<Select.Item item={option} key={option.value[0]}>
{option.label}
</Select.Item>
))}
</Select.Content>
</Select.Root>
)}
</Flex>
{showFilters ? <AssetEventsFilter /> : null}
<Separator mt={2.5} />
<Box p={4} py={2} {...rest}>
<DataTable
cardDef={cardDef(assetId)}
columns={[]}
data={data?.asset_events ?? []}
displayMode="card"
filterActions={showFilters ? <AssetEventsFilter /> : undefined}
headingExtra={
<HStack>
<StateBadge colorPalette="brand" fontSize="md" variant="solid">
<FiDatabase />
{data?.total_entries ?? " "}
</StateBadge>
<Heading marginEnd="auto" size="md">
{translate(titleKey ?? "common:assetEvent", { count: data?.total_entries ?? 0 })}
</Heading>
</HStack>
}
hideRowCountHeading
initialState={tableUrlState}
isLoading={isLoading}
modelName="common:assetEvent"
noRowsMessage={translate("noAssetEvents")}
onStateChange={setTableUrlState}
presentationActions={
setOrderBy === undefined ? undefined : (
<Select.Root
borderWidth={0}
collection={assetSortOptions}
data-testid="asset-sort-duration"
defaultValue={["-timestamp"]}
onValueChange={(option) => setOrderBy(option.value[0] as string)}
size="sm"
width={130}
>
<Select.Trigger>
<Select.ValueText placeholder={translate("dashboard:sortBy.placeholder")} />
</Select.Trigger>

<Select.Content>
{assetSortOptions.items.map((option) => (
<Select.Item item={option} key={option.value[0]}>
{option.label}
</Select.Item>
))}
</Select.Content>
</Select.Root>
)
}
skeletonCount={5}
total={data?.total_entries}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ const cardDef: CardDef<{ name: string }> = {
card: ({ row }) => <Text>My name is {row.name}.</Text>,
};

/** Header row buttons in DOM order, named by accessible label, for asserting slot placement. */
const headerButtonOrder = () =>
within(screen.getByTestId("data-table-header"))
.getAllByRole("button")
.map((button) => button.getAttribute("aria-label") ?? button.textContent);

describe("DataTable", () => {
it("renders table with data", () => {
render(
Expand Down Expand Up @@ -462,20 +468,86 @@ describe("DataTable", () => {
await waitFor(() => expect(within(screen.getByTestId("table-list")).queryByText("Second")).toBeNull());
});

it("renders actions independently of rows and heading", () => {
// Each slot needs its own entry in the header row condition, or its content vanishes whenever
// nothing else occupies the row — including while loading, when the row count is suppressed.
const actionSlots = [
["filterActions", { filterActions: <button type="button">slot content</button> }],
["presentationActions", { presentationActions: <button type="button">slot content</button> }],
["primaryActions", { primaryActions: <button type="button">slot content</button> }],
] as const;

it.each(actionSlots)("renders %s with no rows and no heading", (_name, slot) => {
render(
<DataTable columns={columns} data={[]} hideRowCountHeading modelName="task" total={0} {...slot} />,
{ wrapper: ChakraWrapper },
);

expect(screen.getByText("slot content")).toBeInTheDocument();
});

it.each(actionSlots)("keeps %s visible while loading", (_name, slot) => {
render(<DataTable columns={columns} data={[]} isLoading modelName="task" {...slot} />, {
wrapper: ChakraWrapper,
});

expect(screen.queryByRole("heading")).toBeNull();
expect(screen.getByText("slot content")).toBeInTheDocument();
});

it("renders all three action slots together", () => {
render(
<DataTable
actions={<button type="button">custom action</button>}
columns={columns}
data={[]}
hideRowCountHeading
data={data}
filterActions={<button type="button">filter slot</button>}
modelName="task"
total={0}
presentationActions={<button type="button">presentation slot</button>}
primaryActions={<button type="button">primary slot</button>}
total={2}
/>,
{ wrapper: ChakraWrapper },
);

const header = within(screen.getByTestId("data-table-header"));

expect(header.getByText("filter slot")).toBeInTheDocument();
expect(header.getByText("presentation slot")).toBeInTheDocument();
expect(header.getByText("primary slot")).toBeInTheDocument();
});

it("renders presentation actions before the columns menu", () => {
render(
<DataTable
columns={wideColumns}
data={data}
modelName="task"
presentationActions={<button type="button">custom action</button>}
total={2}
/>,
{ wrapper: ChakraWrapper },
);

expect(screen.getByText("custom action")).toBeInTheDocument();
const order = headerButtonOrder();

expect(order.indexOf("custom action")).toBeLessThan(order.indexOf(columnsMenuLabel));
});

it("renders primary actions before presentation actions", () => {
render(
<DataTable
columns={columns}
data={data}
modelName="task"
presentationActions={<button type="button">presentation slot</button>}
primaryActions={<button type="button">primary slot</button>}
total={2}
/>,
{ wrapper: ChakraWrapper },
);

const order = headerButtonOrder();

expect(order.indexOf("primary slot")).toBeLessThan(order.indexOf("presentation slot"));
});

it("renders headingExtra next to the row count heading", () => {
Expand Down
Loading
Loading