Skip to content

Commit

Permalink
Use airflow config in new UI (apache#44252)
Browse files Browse the repository at this point in the history
* use airflow config in new ui

* Update airflow/ui/src/queries/useConfig.tsx

Co-authored-by: Pierre Jeambrun <[email protected]>

---------

Co-authored-by: Pierre Jeambrun <[email protected]>
  • Loading branch information
bbovenzi and pierrejeambrun authored Nov 21, 2024
1 parent 1307e37 commit 970cb27
Show file tree
Hide file tree
Showing 16 changed files with 329 additions and 91 deletions.
66 changes: 66 additions & 0 deletions airflow/ui/src/components/ConfirmationModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*!
* 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 { Button, type DialogBodyProps, Heading } from "@chakra-ui/react";

import { Dialog } from "src/components/ui";

type Props = {
readonly children?: DialogBodyProps["children"];
readonly description?: string;
readonly header: string;
readonly onConfirm: () => void;
readonly onOpenChange: (details: { open: boolean }) => void;
readonly open: boolean;
};

export const ConfirmationModal = ({
children,
header,
onConfirm,
onOpenChange,
open,
}: Props) => (
<Dialog.Root onOpenChange={onOpenChange} open={open}>
<Dialog.Content backdrop>
<Dialog.Header>
<Heading>{header}</Heading>
</Dialog.Header>

<Dialog.CloseTrigger />

<Dialog.Body>{children}</Dialog.Body>
<Dialog.Footer>
<Dialog.ActionTrigger asChild>
<Button onClick={() => onOpenChange({ open })} variant="outline">
Cancel
</Button>
</Dialog.ActionTrigger>
<Button
colorPalette="blue"
onClick={() => {
onConfirm();
onOpenChange({ open });
}}
>
Confirm
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
);
26 changes: 16 additions & 10 deletions airflow/ui/src/components/DataTable/useTableUrlState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,33 @@
import { useCallback, useMemo } from "react";
import { useSearchParams } from "react-router-dom";

import { useConfig } from "src/queries/useConfig";

import { searchParamsToState, stateToSearchParams } from "./searchParams";
import type { TableState } from "./types";

export const defaultTableState = {
pagination: {
pageIndex: 0,
pageSize: 50,
},
sorting: [],
} as const satisfies TableState;

export const useTableURLState = (defaultState?: Partial<TableState>) => {
const [searchParams, setSearchParams] = useSearchParams();

const configPageSize = useConfig("webserver", "page_size");
const pageSize =
typeof configPageSize === "string" ? parseInt(configPageSize, 10) : 50;

const defaultTableState = {
pagination: {
pageIndex: 0,
pageSize,
},
sorting: [],
} as const satisfies TableState;

const handleStateChange = useCallback(
(state: TableState) => {
setSearchParams(stateToSearchParams(state, defaultTableState), {
replace: true,
});
},
[setSearchParams],
[setSearchParams, defaultTableState],
);

const tableURLState = useMemo(
Expand All @@ -48,7 +54,7 @@ export const useTableURLState = (defaultState?: Partial<TableState>) => {
...defaultTableState,
...defaultState,
}),
[searchParams, defaultState],
[searchParams, defaultState, defaultTableState],
);

return {
Expand Down
46 changes: 38 additions & 8 deletions airflow/ui/src/components/TogglePause.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useDisclosure } from "@chakra-ui/react";
import { useQueryClient } from "@tanstack/react-query";
import { useCallback } from "react";

Expand All @@ -24,16 +25,26 @@ import {
useDagServiceGetDagsKey,
useDagServicePatchDag,
} from "openapi/queries";
import { useConfig } from "src/queries/useConfig";

import { ConfirmationModal } from "./ConfirmationModal";
import { Switch } from "./ui";

type Props = {
readonly dagDisplayName?: string;
readonly dagId: string;
readonly isPaused: boolean;
readonly skipConfirm?: boolean;
};

export const TogglePause = ({ dagId, isPaused }: Props) => {
export const TogglePause = ({
dagDisplayName,
dagId,
isPaused,
skipConfirm,
}: Props) => {
const queryClient = useQueryClient();
const { onClose, onOpen, open } = useDisclosure();

const onSuccess = async () => {
await queryClient.invalidateQueries({
Expand All @@ -49,7 +60,10 @@ export const TogglePause = ({ dagId, isPaused }: Props) => {
onSuccess,
});

const onChange = useCallback(() => {
const showConfirmation =
useConfig("webserver", "require_confirmation_dag_change") === "True";

const onToggle = useCallback(() => {
mutate({
dagId,
requestBody: {
Expand All @@ -58,12 +72,28 @@ export const TogglePause = ({ dagId, isPaused }: Props) => {
});
}, [dagId, isPaused, mutate]);

const onChange = () => {
if (showConfirmation && skipConfirm !== true) {
onOpen();
} else {
onToggle();
}
};

return (
<Switch
checked={!isPaused}
colorPalette="blue"
onCheckedChange={onChange}
size="sm"
/>
<>
<Switch
checked={!isPaused}
colorPalette="blue"
onCheckedChange={onChange}
size="sm"
/>
<ConfirmationModal
header={`${isPaused ? "Unpause" : "Pause"} ${dagDisplayName ?? dagId}?`}
onConfirm={onToggle}
onOpenChange={onClose}
open={open}
/>
</>
);
};
6 changes: 5 additions & 1 deletion airflow/ui/src/components/TriggerDag/TriggerDAGModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ const TriggerDAGModal: React.FC<TriggerDAGModalProps> = ({
<VStack align="start" gap={4}>
<Heading size="xl">
Trigger DAG - {dagDisplayName}{" "}
<TogglePause dagId={dagParams.dagId} isPaused={isPaused} />
<TogglePause
dagId={dagParams.dagId}
isPaused={isPaused}
skipConfirm
/>
</Heading>
{isPaused ? (
<Alert status="warning" title="Paused DAG">
Expand Down
2 changes: 1 addition & 1 deletion airflow/ui/src/constants/sortParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/
import { createListCollection } from "@chakra-ui/react/collection";

export const DagSortOptions = createListCollection({
export const dagSortOptions = createListCollection({
items: [
{ label: "Sort by Display Name (A-Z)", value: "dag_display_name" },
{ label: "Sort by Display Name (Z-A)", value: "-dag_display_name" },
Expand Down
6 changes: 5 additions & 1 deletion airflow/ui/src/context/timezone/TimezoneProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import { createContext, useMemo, type PropsWithChildren } from "react";
import { useLocalStorage } from "usehooks-ts";

import { useConfig } from "src/queries/useConfig";

export type TimezoneContextType = {
selectedTimezone: string;
setSelectedTimezone: (timezone: string) => void;
Expand All @@ -31,9 +33,11 @@ export const TimezoneContext = createContext<TimezoneContextType | undefined>(
const TIMEZONE_KEY = "timezone";

export const TimezoneProvider = ({ children }: PropsWithChildren) => {
const defaultUITimezone = useConfig("webserver", "default_ui_timezone");

const [selectedTimezone, setSelectedTimezone] = useLocalStorage(
TIMEZONE_KEY,
"UTC",
typeof defaultUITimezone === "string" ? defaultUITimezone : "UTC",
);

const value = useMemo<TimezoneContextType>(
Expand Down
30 changes: 22 additions & 8 deletions airflow/ui/src/layouts/BaseLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,27 @@ import { Flex } from "@chakra-ui/react";
import type { PropsWithChildren } from "react";
import { Outlet } from "react-router-dom";

import { useConfig } from "src/queries/useConfig";

import { Nav } from "./Nav";

export const BaseLayout = ({ children }: PropsWithChildren) => (
<>
<Nav />
<Flex flexFlow="column" height="100%" ml={20} p={3}>
{children ?? <Outlet />}
</Flex>
</>
);
export const BaseLayout = ({ children }: PropsWithChildren) => {
const instanceName = useConfig("webserver", "instance_name");
// const instanceNameHasMarkup =
// webserverConfig?.options.find(
// ({ key }) => key === "instance_name_has_markup",
// )?.value === "True";

if (typeof instanceName === "string") {
document.title = instanceName;
}

return (
<>
<Nav />
<Flex flexFlow="column" height="100%" ml={20} p={3}>
{children ?? <Outlet />}
</Flex>
</>
);
};
51 changes: 29 additions & 22 deletions airflow/ui/src/layouts/Nav/DocsButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Link } from "@chakra-ui/react";
import { FiBookOpen } from "react-icons/fi";

import { Menu } from "src/components/ui";
import { useConfig } from "src/queries/useConfig";

import { NavButton } from "./NavButton";

Expand All @@ -33,29 +34,35 @@ const links = [
title: "GitHub Repo",
},
{
href: `/docs`,
href: "/docs",
title: "REST API Reference",
},
];

export const DocsButton = () => (
<Menu.Root positioning={{ placement: "right" }}>
<Menu.Trigger asChild>
<NavButton icon={<FiBookOpen size="1.75rem" />} title="Docs" />
</Menu.Trigger>
<Menu.Content>
{links.map((link) => (
<Menu.Item asChild key={link.title} value={link.title}>
<Link
aria-label={link.title}
href={link.href}
rel="noopener noreferrer"
target="_blank"
>
{link.title}
</Link>
</Menu.Item>
))}
</Menu.Content>
</Menu.Root>
);
export const DocsButton = () => {
const showAPIDocs = useConfig("webserver", "enable_swagger_ui") === "True";

return (
<Menu.Root positioning={{ placement: "right" }}>
<Menu.Trigger asChild>
<NavButton icon={<FiBookOpen size="1.75rem" />} title="Docs" />
</Menu.Trigger>
<Menu.Content>
{links
.filter((link) => !(!showAPIDocs && link.href === "/docs"))
.map((link) => (
<Menu.Item asChild key={link.title} value={link.title}>
<Link
aria-label={link.title}
href={link.href}
rel="noopener noreferrer"
target="_blank"
>
{link.title}
</Link>
</Menu.Item>
))}
</Menu.Content>
</Menu.Root>
);
};
6 changes: 4 additions & 2 deletions airflow/ui/src/pages/DagsList/Dag/Code/Code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { ErrorAlert } from "src/components/ErrorAlert";
import Time from "src/components/Time";
import { ProgressBar } from "src/components/ui";
import { useColorMode } from "src/context/colorMode";
import { useConfig } from "src/queries/useConfig";

SyntaxHighlighter.registerLanguage("python", python);

Expand All @@ -59,8 +60,9 @@ export const Code = () => {
dagId: dagId ?? "",
});

// TODO: get default_wrap from config
const [wrap, setWrap] = useState(false);
const defaultWrap = useConfig("webserver", "default_wrap") === "True";

const [wrap, setWrap] = useState(defaultWrap);

const toggleWrap = () => setWrap(!wrap);
const { colorMode } = useColorMode();
Expand Down
6 changes: 5 additions & 1 deletion airflow/ui/src/pages/DagsList/Dag/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ export const Header = ({
<DagIcon height={8} width={8} />
<Heading size="lg">{dag?.dag_display_name ?? dagId}</Heading>
{dag !== undefined && (
<TogglePause dagId={dag.dag_id} isPaused={dag.is_paused} />
<TogglePause
dagDisplayName={dag.dag_display_name}
dagId={dag.dag_id}
isPaused={dag.is_paused}
/>
)}
</HStack>
<Flex>{dag ? <TriggerDAGTextButton dag={dag} /> : undefined}</Flex>
Expand Down
6 changes: 5 additions & 1 deletion airflow/ui/src/pages/DagsList/DagCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ export const DagCard = ({ dag }: Props) => {
<DagTags tags={dag.tags} />
</HStack>
<HStack>
<TogglePause dagId={dag.dag_id} isPaused={dag.is_paused} />
<TogglePause
dagDisplayName={dag.dag_display_name}
dagId={dag.dag_id}
isPaused={dag.is_paused}
/>
<TriggerDAGIconButton dag={dag} />
</HStack>
</Flex>
Expand Down
Loading

0 comments on commit 970cb27

Please sign in to comment.