diff --git a/airflow-core/src/airflow/ui/src/components/ui/index.ts b/airflow-core/src/airflow/ui/src/components/ui/index.ts index e39d15dd09d49..968f0471e9b5a 100644 --- a/airflow-core/src/airflow/ui/src/components/ui/index.ts +++ b/airflow-core/src/airflow/ui/src/components/ui/index.ts @@ -36,3 +36,7 @@ export * from "./Popover"; export * from "./Checkbox"; export * from "./ResetButton"; export * from "./InputWithAddon"; +export * from "./NumberInput"; +export * from "./RadioCard"; +export * from "./Tag"; +export { default as SegmentedControl } from "./SegmentedControl"; diff --git a/airflow-core/src/airflow/ui/src/pages/Playground/Playground.tsx b/airflow-core/src/airflow/ui/src/pages/Playground/Playground.tsx new file mode 100644 index 0000000000000..5901a4ed3751b --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Playground/Playground.tsx @@ -0,0 +1,174 @@ +/* eslint-disable i18next/no-literal-string */ + +/* eslint-disable unicorn/consistent-function-scoping */ + +/*! + * 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 { Box, Container, Heading, HStack, VStack } from "@chakra-ui/react"; +import { useState } from "react"; + +import { + AirflowComponents, + Buttons, + Charts, + Collections, + Colors, + Forms, + Graph, + Layout, + ModalDialog, + Overlays, + Feedback, + TableOfContents, + Typography, +} from "./index"; + +/** + * AccessibilityHelper page - A comprehensive component showcase inspired by Chakra UI Playground + * for testing accessibility, contrast ratios, and Lighthouse metrics. + * + * Based on: https://www.chakra-ui.com/playground + * This page includes examples of all major UI components used in the Airflow UI + * to ensure they meet accessibility standards and provide good contrast ratios. + */ +export const Playground = () => { + // Modal state + const [isDialogOpen, setIsDialogOpen] = useState(false); + + // Form state + const [formState, setFormState] = useState({ + currentPage: 1, + multipleSegmented: ["option1", "option2"], + progress: 75, + radio: "option1", + radioCard: "option1", + segmented: ["option1"], + slider: [50], + switch: false, + }); + + // Section visibility state - all open by default for better UX + const [openSections, setOpenSections] = useState>({ + airflow: true, + buttons: true, + charts: true, + collections: true, + colors: true, + forms: true, + graphs: true, + layout: true, + overlays: true, + progress: true, + typography: true, + }); + + const toggleSection = (section: keyof typeof openSections) => { + setOpenSections((prev) => ({ ...prev, [section]: !prev[section] })); + }; + + const scrollToSection = (sectionId: string) => { + document.querySelector(`#${sectionId}`)?.scrollIntoView({ behavior: "smooth" }); + }; + + const updateFormState = (updates: Partial) => { + setFormState((prev) => ({ ...prev, ...updates })); + }; + + return ( + + + {/* Page Header */} + + + Airflow UI Component Playground + + + + {/* Two Column Layout: Content + Table of Contents */} + + {/* Main Content */} + + + {/* Colors Section */} + toggleSection("colors")} /> + + {/* Airflow Components Section */} + toggleSection("airflow")} /> + + {/* Layout Section */} + toggleSection("layout")} /> + + {/* Typography Section */} + toggleSection("typography")} + /> + + {/* Buttons Section */} + toggleSection("buttons")} /> + + {/* Forms Section */} + toggleSection("forms")} + updateFormState={updateFormState} + /> + + {/* Collections Section */} + toggleSection("collections")} + /> + + {/* Progress & Alerts Section */} + toggleSection("progress")} + onStatesToggle={() => {}} + progressValue={formState.progress} + setProgressValue={(value: number) => updateFormState({ progress: value })} + /> + + {/* Graph Components Section */} + toggleSection("graphs")} /> + + {/* Charts & Gantt Section */} + toggleSection("charts")} /> + + {/* Overlays Section */} + toggleSection("overlays")} /> + + + + {/* Table of Contents Sidebar */} + + + + {/* Modal Dialog */} + setIsDialogOpen(false)} /> + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/Playground/TableOfContents.tsx b/airflow-core/src/airflow/ui/src/pages/Playground/TableOfContents.tsx new file mode 100644 index 0000000000000..16b83ef0fd46e --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Playground/TableOfContents.tsx @@ -0,0 +1,121 @@ +/* eslint-disable i18next/no-literal-string */ + +/*! + * 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 { Box, ButtonGroup, Heading, HStack, IconButton, Link, List, Text, VStack } from "@chakra-ui/react"; +import React from "react"; +import { MdExpand, MdCompress } from "react-icons/md"; + +type TableOfContentsProps = { + readonly openSections: Record; + readonly scrollToSection: (sectionId: string) => void; + readonly setOpenSections: React.Dispatch>>; +}; + +const sections = [ + { id: "colors", title: "Colors" }, + { id: "airflow", title: "Airflow Components" }, + { id: "layout", title: "Layout" }, + { id: "typography", title: "Typography" }, + { id: "buttons", title: "Buttons" }, + { id: "forms", title: "Forms" }, + { id: "collections", title: "Collections" }, + { id: "progress", title: "Progress & Alerts" }, + { id: "graphs", title: "Graph" }, + { id: "charts", title: "Charts" }, + { id: "overlays", title: "Overlays" }, +]; + +export const TableOfContents = ({ openSections, scrollToSection, setOpenSections }: TableOfContentsProps) => ( + + + + Table of Contents + {/* Quick Actions */} + + + Quick Actions + + + { + Object.keys(openSections).forEach((section) => { + setOpenSections((prev) => ({ ...prev, [section]: true })); + }); + }} + size="sm" + title="Expand All" + > + + + { + Object.keys(openSections).forEach((section) => { + setOpenSections((prev) => ({ ...prev, [section]: false })); + }); + }} + size="sm" + title="Collapse All" + > + + + + + + {/* Navigation Links */} + + + Sections + + + + {sections.map(({ id, title }) => ( + + { + // Toggle the section state (open/close) + setOpenSections((prev) => ({ ...prev, [id]: !openSections[id] })); + scrollToSection(id); + }} + padding="2" + transition="background 0.2s" + width="full" + > + + {title} + + {(openSections[id] ?? true) ? "−" : "+"} + + + + + ))} + + + + + + +); diff --git a/airflow-core/src/airflow/ui/src/pages/Playground/airflow/AirflowComponents.tsx b/airflow-core/src/airflow/ui/src/pages/Playground/airflow/AirflowComponents.tsx new file mode 100644 index 0000000000000..da2e7b9f550ff --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Playground/airflow/AirflowComponents.tsx @@ -0,0 +1,302 @@ +/* eslint-disable i18next/no-literal-string */ + +/* eslint-disable react/jsx-max-depth */ + +/*! + * 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 { Box, Collapsible, Heading, HStack, Text, VStack } from "@chakra-ui/react"; + +import { HeaderCard } from "src/components/HeaderCard"; +import { StateBadge } from "src/components/StateBadge"; +import { StateIcon } from "src/components/StateIcon"; +import { StatsCard } from "src/components/StatsCard"; +import type { TaskInstanceState } from "openapi/requests/types.gen"; + +type AirflowComponentsProps = { + readonly isOpen: boolean; + readonly onToggle: () => void; +}; + +export const AirflowComponents = ({ isOpen, onToggle }: AirflowComponentsProps) => { + const taskStates = [ + "success", "running", "failed", "queued", "skipped", + "upstream_failed", "up_for_retry", "up_for_reschedule", + "scheduled", "deferred", "removed" + ]; + + return ( + + + + + + Airflow Components + + Airflow-specific UI components and patterns + + + + {isOpen ? "−" : "+"} + + + + + + + {/* State Components */} + + State Components + + + + State Badges + + + {taskStates.map((state) => ( + + {state.replace("_", " ")} + + ))} + + + + + + State Icons + + + {taskStates.map((state) => ( + + + {state.replace("_", " ")} + + ))} + + + + + + {/* Header Card */} + + Header Card + + + + Basic Header Card + + + Edit + Trigger + Pause + + } + icon="📊" + state="success" + stats={[ + { label: "Tasks", value: "15" }, + { label: "Duration", value: "2h 30m" }, + { label: "Last Run", value: "2024-01-15" }, + { label: "Next Run", value: "2024-01-16" }, + ]} + subTitle="(dag_id: sample_dag)" + title="Sample DAG" + /> + + + + + Header Card with Error State + + + Retry + Unpause + + } + icon="❌" + state="failed" + stats={[ + { label: "Tasks", value: "8" }, + { label: "Failed", value: "3" }, + { label: "Last Run", value: "2024-01-14" }, + { label: "Next Run", value: "Paused" }, + ]} + subTitle="(dag_id: failed_dag)" + title="Failed DAG" + /> + + + + + {/* Stats Card */} + + Stats Card + + + + Basic Stats Card + + + + + + + Stats Card with Custom Colors + + + + + + + {/* Individual Stats */} + + Individual Stats + + + + Stat Components + + + + Total DAGs + 47 + + + Active DAGs + 42 + + + Paused DAGs + 5 + + + Success Rate + 94.8% + + + + + + + {/* Task Instance States */} + + Task Instance States + + + + All possible task execution states with counts and percentages + + + {[ + { count: 150, label: "Success", state: "success" }, + { count: 25, label: "Running", state: "running" }, + { count: 8, label: "Failed", state: "failed" }, + { count: 3, label: "Upstream Failed", state: "upstream_failed" }, + { count: 45, label: "Skipped", state: "skipped" }, + { count: 5, label: "Up for Retry", state: "up_for_retry" }, + { count: 2, label: "Up for Reschedule", state: "up_for_reschedule" }, + { count: 12, label: "Queued", state: "queued" }, + { count: 30, label: "Scheduled", state: "scheduled" }, + { count: 7, label: "Deferred", state: "deferred" }, + { count: 1, label: "Removed", state: "removed" }, + ].map(({ count, label, state }) => { + const totalCount = 288; // Sum of all counts + const percentage = totalCount === 0 ? 0 : ((count / totalCount) * 100).toFixed(1); + const barWidth = totalCount === 0 ? 0 : (count / totalCount) * 100; + const remainingWidth = 100 - barWidth; + + return ( + + {/* Top row: State info and count */} + + + + + {label} + + + + {count} + + + ({percentage}%) + + + + + {/* Bottom row: Progress bar */} + + 0 ? "2px" : "0px"} + width={`${barWidth}%`} + /> + + + + ); + })} + + + + + + + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/Playground/buttons/Buttons.tsx b/airflow-core/src/airflow/ui/src/pages/Playground/buttons/Buttons.tsx new file mode 100644 index 0000000000000..e45c9e5bee2a9 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Playground/buttons/Buttons.tsx @@ -0,0 +1,148 @@ +/* eslint-disable i18next/no-literal-string */ + +/* eslint-disable react/jsx-max-depth */ + +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/*! + * 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 { Box, Code, Collapsible, Heading, HStack, Text, VStack } from "@chakra-ui/react"; + +import { Button } from "src/components/ui"; + +type ButtonsProps = { + readonly isOpen: boolean; + readonly onToggle: () => void; +}; + +export const Buttons = ({ isOpen, onToggle }: ButtonsProps) => { + const colorPalettes = ["brand", "gray", "red", "green", "blue", "yellow", "purple"]; + + return ( + + + + + + Buttons & Code + + Interactive elements and code display + + + + {isOpen ? "−" : "+"} + + + + + + + {/* Button Variants */} + + Button Variants + + {["solid", "outline", "ghost", "subtle"].map((variant) => ( + + + {variant} + + + {colorPalettes.map((palette) => ( + + ))} + + + ))} + + + + {/* Button Sizes */} + + Button Sizes + + + + + + + + + + {/* Code Display */} + + Code Display + + + + Inline Code + + + Use npm install to install dependencies, then run npm start to + start the development server. + + + + + + Code Block + + + {`function hello() { + console.log("Hello, World!"); + return "Hello from Airflow UI"; +}`} + + + + + + + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/Playground/charts/Charts.tsx b/airflow-core/src/airflow/ui/src/pages/Playground/charts/Charts.tsx new file mode 100644 index 0000000000000..8e89fb8d3f5f6 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Playground/charts/Charts.tsx @@ -0,0 +1,522 @@ +/* eslint-disable i18next/no-literal-string */ + +/* eslint-disable react/jsx-max-depth */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + + + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + +/* eslint-disable max-lines */ + +/* eslint-disable perfectionist/sort-objects */ + +/* eslint-disable perfectionist/sort-jsx-props */ + +/* eslint-disable @stylistic/padding-line-between-statements */ + +/*! + * 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 { Box, Collapsible, Heading, HStack, Text, VStack, useToken } from "@chakra-ui/react"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + BarElement, + Filler, + Title, + Tooltip, + Legend, + TimeScale, +} from "chart.js"; +import { Bar, Line } from "react-chartjs-2"; +import "chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm"; + +import { getComputedCSSVariableValue } from "src/theme"; + +// import { useColorMode } from "src/context/colorMode"; + +// Register Chart.js components +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + BarElement, + Filler, + Title, + Tooltip, + Legend, + TimeScale, +); + +// Helper function to add alpha transparency to any color format +const addAlpha = (color: string, alpha: number = 0.2) => { + // If it's already a hex color, add alpha + if (color.startsWith("#")) { + const hex = + alpha === 1 + ? "FF" + : Math.round(alpha * 255) + .toString(16) + .padStart(2, "0"); + return color + hex; + } + // For rgb/hsl colors, use CSS color-mix for transparency + return `color-mix(in srgb, ${color} ${Math.round(alpha * 100)}%, transparent)`; +}; + +type ChartsProps = { + readonly isOpen: boolean; + readonly onToggle: () => void; +}; + +export const Charts = ({ isOpen, onToggle }: ChartsProps) => { + // const { colorMode } = useColorMode(); + + // Get semantic token references + const [ + successToken, + failedToken, + runningToken, + queuedToken, + skippedToken, + upstreamFailedToken, + upForRetryToken, + upForRescheduleToken, + scheduledToken, + deferredToken, + removedToken, + ] = useToken("colors", [ + "success.solid", + "failed.solid", + "running.solid", + "queued.solid", + "skipped.solid", + "upstream_failed.solid", + "up_for_retry.solid", + "up_for_reschedule.solid", + "scheduled.solid", + "deferred.solid", + "removed.solid", + ]); + + // Convert CSS variables to computed color values + const success = getComputedCSSVariableValue(successToken ?? "oklch(0.5 0 0)"); + const failed = getComputedCSSVariableValue(failedToken ?? "oklch(0.5 0 0)"); + const running = getComputedCSSVariableValue(runningToken ?? "oklch(0.5 0 0)"); + const queued = getComputedCSSVariableValue(queuedToken ?? "oklch(0.5 0 0)"); + const skipped = getComputedCSSVariableValue(skippedToken ?? "oklch(0.5 0 0)"); + const upstreamFailed = getComputedCSSVariableValue(upstreamFailedToken ?? "oklch(0.5 0 0)"); + const upForRetry = getComputedCSSVariableValue(upForRetryToken ?? "oklch(0.5 0 0)"); + const upForReschedule = getComputedCSSVariableValue(upForRescheduleToken ?? "oklch(0.5 0 0)"); + const scheduled = getComputedCSSVariableValue(scheduledToken ?? "oklch(0.5 0 0)"); + const deferred = getComputedCSSVariableValue(deferredToken ?? "oklch(0.5 0 0)"); + const removed = getComputedCSSVariableValue(removedToken ?? "oklch(0.5 0 0)"); + + // Mock data for duration chart + const durationData = { + labels: [ + "success_task", + "failed_task", + "running_task", + "queued_task", + "skipped_task", + "upstream_failed_task", + "up_for_retry_task", + "up_for_reschedule_task", + "scheduled_task", + "deferred_task", + "removed_task", + ], + datasets: [ + { + label: "Duration (minutes)", + data: [12, 8, 15, 6, 20, 4, 9, 3, 18, 11, 1], + backgroundColor: [ + success, + failed, + running, + queued, + skipped, + upstreamFailed, + upForRetry, + upForReschedule, + scheduled, + deferred, + removed, + ], + borderColor: [ + success, + failed, + running, + queued, + skipped, + upstreamFailed, + upForRetry, + upForReschedule, + scheduled, + deferred, + removed, + ], + borderWidth: 1, + }, + ], + }; + + // Mock data for trend chart + const trendData = { + labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], + datasets: [ + { + label: "Success Rate", + data: [85, 88, 92, 87, 94, 91], + borderColor: success, + backgroundColor: addAlpha(success, 0.2), + fill: true, + tension: 0.4, + }, + { + label: "Failure Rate", + data: [8, 7, 5, 9, 4, 6], + borderColor: failed, + backgroundColor: addAlpha(failed, 0.2), + fill: true, + tension: 0.4, + }, + { + label: "Running Rate", + data: [3, 2, 1, 2, 1, 1], + borderColor: running, + backgroundColor: addAlpha(running, 0.2), + fill: true, + tension: 0.4, + }, + { + label: "Queued Rate", + data: [2, 1, 1, 1, 0, 1], + borderColor: queued, + backgroundColor: addAlpha(queued, 0.2), + fill: true, + tension: 0.4, + }, + { + label: "Skipped Rate", + data: [2, 2, 1, 1, 1, 1], + borderColor: skipped, + backgroundColor: addAlpha(skipped, 0.2), + fill: true, + tension: 0.4, + }, + ], + }; + + // Mock data for Gantt chart following the same pattern as the real Gantt component + const ganttDataItems = [ + // Data extraction tasks + { x: ["2024-01-01 08:00:00", "2024-01-01 08:12:00"], y: "extract_user_data", state: "success" }, + { x: ["2024-01-01 08:15:00", "2024-01-01 08:18:00"], y: "extract_product_catalog", state: "success" }, + + // Data validation tasks + { x: ["2024-01-01 08:12:00", "2024-01-01 08:20:00"], y: "validate_data_quality", state: "running" }, + { x: ["2024-01-01 08:18:00", "2024-01-01 08:25:00"], y: "validate_api_responses", state: "running" }, + + // Data transformation tasks + { x: ["2024-01-01 08:20:00", "2024-01-01 08:35:00"], y: "transform_customer_info", state: "success" }, + { x: ["2024-01-01 08:25:00", "2024-01-01 08:40:00"], y: "process_payments", state: "success" }, + + // Loading tasks + { x: ["2024-01-01 08:35:00", "2024-01-01 08:50:00"], y: "load_to_warehouse", state: "success" }, + { x: ["2024-01-01 08:40:00", "2024-01-01 08:55:00"], y: "update_inventory", state: "success" }, + + // Reporting tasks + { x: ["2024-01-01 08:50:00", "2024-01-01 09:05:00"], y: "generate_reports", state: "success" }, + { x: ["2024-01-01 08:55:00", "2024-01-01 09:00:00"], y: "send_notifications", state: "success" }, + + // System maintenance tasks + { x: ["2024-01-01 09:00:00", "2024-01-01 09:08:00"], y: "cleanup_temp_files", state: "skipped" }, + { x: ["2024-01-01 09:05:00", "2024-01-01 09:12:00"], y: "update_metrics", state: "skipped" }, + { x: ["2024-01-01 09:08:00", "2024-01-01 09:20:00"], y: "backup_database", state: "skipped" }, + + // Integration tasks + { x: ["2024-01-01 09:12:00", "2024-01-01 09:25:00"], y: "sync_with_crm", state: "success" }, + { x: ["2024-01-01 09:20:00", "2024-01-01 09:30:00"], y: "monitor_system_health", state: "success" }, + + // Cleanup tasks + { x: ["2024-01-01 09:25:00", "2024-01-01 09:28:00"], y: "archive_old_logs", state: "failed" }, + { x: ["2024-01-01 09:28:00", "2024-01-01 09:35:00"], y: "refresh_materialized_views", state: "failed" }, + { x: ["2024-01-01 09:30:00", "2024-01-01 09:33:00"], y: "process_webhooks", state: "failed" }, + { x: ["2024-01-01 09:33:00", "2024-01-01 09:38:00"], y: "update_cache", state: "failed" }, + + // Scheduling task + { x: ["2024-01-01 09:35:00", "2024-01-01 09:37:00"], y: "schedule_next_run", state: "success" }, + ]; + + // Get all unique states and their colors + const states = [...new Set(ganttDataItems.map((item) => item.state))]; + const stateColorTokens = useToken( + "colors", + states.map((state) => `${state}.solid`), + ); + const stateColorMap = Object.fromEntries( + states.map((state, index) => [ + state, + getComputedCSSVariableValue(stateColorTokens[index] ?? "oklch(0.5 0 0)"), + ]), + ); + + const ganttData = { + datasets: [ + { + backgroundColor: ganttDataItems.map((dataItem) => stateColorMap[dataItem.state]), + data: ganttDataItems, + maxBarThickness: 20, + minBarLength: 10, + }, + ], + labels: ganttDataItems.map((item) => item.y), + }; + + const chartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: "top" as const, + }, + title: { + display: false, + }, + }, + scales: { + y: { + beginAtZero: true, + }, + }, + }; + + const ganttOptions = { + indexAxis: "y" as const, + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false, + }, + tooltip: { + callbacks: { + label: (context: any) => { + const taskInstance = ganttDataItems.find((dataItem) => dataItem.y === context.label); + const startTime = new Date(taskInstance?.x[0] ?? ""); + const endTime = new Date(taskInstance?.x[1] ?? ""); + const duration = Math.round((endTime.getTime() - startTime.getTime()) / (1000 * 60)); + return [ + `State: ${taskInstance?.state}`, + `Start: ${startTime.toLocaleTimeString()}`, + `End: ${endTime.toLocaleTimeString()}`, + `Duration: ${duration} minutes` + ]; + }, + }, + }, + }, + scales: { + x: { + type: "time" as const, + position: "top" as const, + stacked: true, + min: Math.min(...ganttDataItems.map((item) => new Date(item.x[0] ?? "").getTime())), + max: Math.max(...ganttDataItems.map((item) => new Date(item.x[1] ?? "").getTime())), + ticks: { + align: "start" as const, + callback: (value: number | string) => new Date(value).toLocaleTimeString(), + maxRotation: 8, + maxTicksLimit: 8, + minRotation: 8, + }, + grid: { + display: true, + }, + }, + y: { + stacked: true, + ticks: { + display: false, + }, + grid: { + display: true, + }, + }, + }, + }; + + const lineOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: "top" as const, + }, + }, + scales: { + y: { + beginAtZero: true, + max: 100, + ticks: { + callback: (value: any) => `${value}%`, + }, + }, + }, + }; + + return ( + + + + + + Charts & Gantt + + Data visualization and timeline charts + + + + {isOpen ? "−" : "+"} + + + + + + + {/* First Row: Duration and Trend Charts */} + + {/* Duration Chart */} + + + Duration Chart + + Bar chart showing task durations + + + + + + + Displays task execution times with color-coded states. Accessible via screen reader with + data table fallback. + + + + {/* Trend Chart */} + + + Trend Chart + + Line chart showing success/failure trends + + + + + + + Shows task success and failure rates over time. Includes hover tooltips and keyboard + navigation. + + + + + {/* Second Row: Gantt Chart and Chart Accessibility */} + + {/* Gantt Chart */} + + + Gantt Chart + + Timeline visualization showing task execution with start/end times + + + + + + + True Gantt chart showing task timelines with start/end times. Displays parallel execution, + task dependencies, and realistic DAG workflow from 8:00 AM to 9:38 AM. + + + + {/* Chart Accessibility Features */} + + + Chart Accessibility + + A11y features for charts + + + + + Keyboard Navigation: + + + • Tab: Navigate between chart elements + • Arrow keys: Move between data points + • Enter: Activate/select data point + • Escape: Exit chart focus + + + + Screen Reader Support: + + + • Chart type and purpose announced + • Data values read aloud + • Axis labels and scales described + • Alternative data table available + + + + Visual Accessibility: + + + • High contrast color schemes + • Pattern/texture options for color blindness + • Scalable text and elements + • Focus indicators visible + + + + + + + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/Playground/collections/Collections.tsx b/airflow-core/src/airflow/ui/src/pages/Playground/collections/Collections.tsx new file mode 100644 index 0000000000000..f900cab3228d3 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Playground/collections/Collections.tsx @@ -0,0 +1,222 @@ +/* eslint-disable i18next/no-literal-string */ + +/* eslint-disable react/jsx-max-depth */ + +/*! + * 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 { Avatar, Badge, Box, Collapsible, Heading, HStack, Text, VStack } from "@chakra-ui/react"; + +import { Tag } from "src/components/ui"; +import { Lists } from "./Lists"; + +type CollectionsProps = { + readonly isOpen: boolean; + readonly onToggle: () => void; +}; + +export const Collections = ({ isOpen, onToggle }: CollectionsProps) => { + const colorPalettes = ["brand", "gray", "red", "green", "blue", "yellow", "purple"]; + + return ( + + + + + + Collections + + Badges, avatars, tags, and other collection components + + + + {isOpen ? "−" : "+"} + + + + + + + {/* Badges */} + + Badges + + + + Solid Badges + + + {colorPalettes.map((palette) => ( + + {palette} + + ))} + + + + + + Outline Badges + + + {colorPalettes.map((palette) => ( + + {palette} + + ))} + + + + + + Subtle Badges + + + {colorPalettes.map((palette) => ( + + {palette} + + ))} + + + + + + {/* Avatars */} + + Avatars + + + + Avatar Sizes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Avatar with Images + + + + + AK + + + + AK + + + + + + + + + + {/* Tags */} + + Tags + + + + Basic Tags + + + Python + Data Pipeline + ETL + Analytics + Production + Development + + + + + + Closable Tags + + + console.log("Removed")}> + Removable Tag + + console.log("Removed")}> + Another Tag + + + + + + + Tags with Icons (like DAG tags) + + + 📊}> + Dashboard + + ⚡}> + Fast + + 🔒}> + Secure + + + + + + + + {/* Lists */} + + + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/Playground/collections/Lists.tsx b/airflow-core/src/airflow/ui/src/pages/Playground/collections/Lists.tsx new file mode 100644 index 0000000000000..6d39afa0213a5 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Playground/collections/Lists.tsx @@ -0,0 +1,105 @@ +/* eslint-disable i18next/no-literal-string */ + +/*! + * 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 { Badge, Box, Heading, HStack, List, Text, VStack } from "@chakra-ui/react"; + +export const Lists = () => ( + + Lists + + + + Basic List + + + First item + Second item + Third item + Fourth item + + + + + + List with Icons + + + + + 📊 + Dashboard + + + + + ⚙️ + Settings + + + + + 📈 + Analytics + + + + + 🔧 + Tools + + + + + + + + List with Badges + + + + + Task 1 + Completed + + + + + Task 2 + In Progress + + + + + Task 3 + Failed + + + + + Task 4 + Pending + + + + + + +); + diff --git a/airflow-core/src/airflow/ui/src/pages/Playground/colors/Colors.tsx b/airflow-core/src/airflow/ui/src/pages/Playground/colors/Colors.tsx new file mode 100644 index 0000000000000..80f1a4aa05ab1 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Playground/colors/Colors.tsx @@ -0,0 +1,170 @@ +/* eslint-disable i18next/no-literal-string */ + +/* eslint-disable react/jsx-max-depth */ + + + + + +/*! + * 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 { Box, Collapsible, Heading, HStack, Text, VStack } from "@chakra-ui/react"; + +type ColorsProps = { + readonly isOpen: boolean; + readonly onToggle: () => void; +}; + +export const Colors = ({ isOpen, onToggle }: ColorsProps) => { + // Color palettes available in the theme + const colorPalettes = [ + { key: "brand", name: "Brand" }, + { key: "gray", name: "Gray" }, + { key: "red", name: "Red" }, + { key: "orange", name: "Orange" }, + { key: "amber", name: "Amber" }, + { key: "yellow", name: "Yellow" }, + { key: "lime", name: "Lime" }, + { key: "green", name: "Green" }, + { key: "emerald", name: "Emerald" }, + { key: "teal", name: "Teal" }, + { key: "cyan", name: "Cyan" }, + { key: "sky", name: "Sky" }, + { key: "blue", name: "Blue" }, + { key: "indigo", name: "Indigo" }, + { key: "violet", name: "Violet" }, + { key: "purple", name: "Purple" }, + { key: "fuchsia", name: "Fuchsia" }, + { key: "pink", name: "Pink" }, + { key: "rose", name: "Rose" }, + { key: "slate", name: "Slate" }, + { key: "zinc", name: "Zinc" }, + { key: "neutral", name: "Neutral" }, + { key: "stone", name: "Stone" }, + ]; + + const colorNumbers = ["50", "100", "200", "300", "400", "500", "600", "700", "800", "900", "950"]; + + return ( + + + + + + Color Palette + + Complete color system and contrast examples + + + + {isOpen ? "−" : "+"} + + + + + + + Color Palette Matrix + + {/* Header row with color numbers */} + + {/* Spacer for color name column */} + + {colorNumbers.map((number) => ( + + + {number} + + + ))} + + + {/* Color rows */} + {colorPalettes.map((palette) => ( + + {/* Color name label */} + + + {palette.name.toLowerCase()} + + + + {/* Color swatches */} + {colorNumbers.map((number) => ( + + ))} + + ))} + + + {/* Black & White Special Colors */} + + + Absolute Colors + + Pure black and white values + + + + + + + Black + + + + + + White + + + + + + + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/Playground/feedback/Feedback.tsx b/airflow-core/src/airflow/ui/src/pages/Playground/feedback/Feedback.tsx new file mode 100644 index 0000000000000..a7ddaa292c853 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Playground/feedback/Feedback.tsx @@ -0,0 +1,271 @@ +/* eslint-disable i18next/no-literal-string */ + +/* eslint-disable react/jsx-max-depth */ + +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/* eslint-disable max-lines */ + + + + + +/*! + * 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 { + Box, + Collapsible, + Heading, + HStack, + Progress, + ProgressCircle, + Spinner, + Text, + VStack, + Alert, +} from "@chakra-ui/react"; + +import { StateBadge } from "src/components/StateBadge"; +import { StateIcon } from "src/components/StateIcon"; + +type FeedbackProps = { + readonly isProgressOpen: boolean; + readonly isStatesOpen: boolean; + readonly onProgressToggle: () => void; + readonly onStatesToggle: () => void; + readonly progressValue: number; + readonly setProgressValue: (value: number) => void; +}; + +export const Feedback = ({ + isProgressOpen, + isStatesOpen, + onProgressToggle, + onStatesToggle, + progressValue, +}: FeedbackProps) => { + const taskStates = [ + { count: 150, label: "Success", state: "success" }, + { count: 25, label: "Running", state: "running" }, + { count: 8, label: "Failed", state: "failed" }, + { count: 3, label: "Upstream Failed", state: "upstream_failed" }, + { count: 45, label: "Skipped", state: "skipped" }, + { count: 5, label: "Up for Retry", state: "up_for_retry" }, + { count: 2, label: "Up for Reschedule", state: "up_for_reschedule" }, + { count: 12, label: "Queued", state: "queued" }, + { count: 30, label: "Scheduled", state: "scheduled" }, + { count: 7, label: "Deferred", state: "deferred" }, + { count: 1, label: "Removed", state: "removed" }, + ]; + + return ( + + {/* Progress & Alerts Section */} + + + + + + Progress & Alerts + + Progress indicators, loading states, and alert messages + + + + {isProgressOpen ? "−" : "+"} + + + + + + + {/* Linear Progress */} + + Linear Progress + + + + Default Progress ({progressValue}%) + + + + + + + + + + + Colored Progress + + + + + + + + + + + + + + + + + + + + + + + {/* Circular Progress */} + + Circular Progress + + + + + + + + + + Default + + + + + + + + + + + Success + + + + + + + + + + + Error + + + + + {/* Spinners */} + + Loading Spinners + + + + Extra Small + + + + Small + + + + Medium + + + + Large + + + + Extra Large + + + + + {/* Alerts */} + + Alerts & Notifications + + {/* Success Alert */} + + + + Success! + + Your DAG has been successfully deployed and is now running. + + + + + {/* Warning Alert */} + + + + Warning + + This DAG has been running for more than 24 hours. Consider checking for issues. + + + + + {/* Error Alert */} + + + + Error + + Failed to connect to the database. Please check your connection settings. + + + + + {/* Info Alert */} + + + + Information + + New features are available in the latest version. Consider updating your installation. + + + + + + + + + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/Playground/forms/AdvancedControls.tsx b/airflow-core/src/airflow/ui/src/pages/Playground/forms/AdvancedControls.tsx new file mode 100644 index 0000000000000..394ac41e65c32 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Playground/forms/AdvancedControls.tsx @@ -0,0 +1,61 @@ +/* eslint-disable i18next/no-literal-string */ + + + +/*! + * 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 { Heading, HStack, Text, VStack } from "@chakra-ui/react"; +import { useState } from "react"; + +import { Switch } from "src/components/ui"; + +export const AdvancedControls = () => { + const [switchValue, setSwitchValue] = useState(false); + + return ( + + Advanced Form Controls + + {/* Switch */} + + + Switch + + + setSwitchValue(event.checked)} + /> + Enable notifications + + + + {/* Additional form controls can be added here as they become available */} + + + More Controls Coming Soon + + + Additional form controls like Select, NumberInput, RadioCard, and FileUpload + will be added as their APIs are finalized. + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/Playground/forms/Forms.tsx b/airflow-core/src/airflow/ui/src/pages/Playground/forms/Forms.tsx new file mode 100644 index 0000000000000..a29c0e1c2cce1 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Playground/forms/Forms.tsx @@ -0,0 +1,105 @@ +/* eslint-disable i18next/no-literal-string */ + +/*! + * 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 { Box, Collapsible, Heading, HStack, Text, VStack } from "@chakra-ui/react"; + +import { AdvancedControls } from "src/pages/Playground/forms/AdvancedControls"; +import { NavigationControls } from "src/pages/Playground/forms/NavigationControls"; +import { NumberRangeControls } from "src/pages/Playground/forms/NumberRangeControls"; +import { Controls } from "src/pages/Playground/forms/SelectionControls"; +import { TextInputs } from "src/pages/Playground/forms/TextInputs"; + +type FormState = { + readonly currentPage: number; + readonly multipleSegmented: Array; + readonly progress: number; + readonly radio: string; + readonly radioCard: string; + readonly segmented: Array; + readonly slider: Array; + readonly switch: boolean; +}; + +type FormsProps = { + readonly formState: FormState; + readonly isOpen: boolean; + readonly onToggle: () => void; + readonly updateFormState: (updates: Partial) => void; +}; + +export const Forms = ({ formState, isOpen, onToggle, updateFormState }: FormsProps) => ( + + + + + + Forms + + Form controls, inputs, and interactive components + + + + {isOpen ? "−" : "+"} + + + + + + + {/* Basic Form Controls Row */} + + + updateFormState({ radio: value })} + /> + + + {/* Interactive Controls Row */} + + updateFormState({ slider: value })} + setSwitchValue={(value) => updateFormState({ switch: value })} + sliderValue={formState.slider} + switchValue={formState.switch} + /> + updateFormState({ currentPage: value })} + /> + + + {/* Advanced Form Controls */} + + + + + + +); diff --git a/airflow-core/src/airflow/ui/src/pages/Playground/forms/NavigationControls.tsx b/airflow-core/src/airflow/ui/src/pages/Playground/forms/NavigationControls.tsx new file mode 100644 index 0000000000000..9d0f0bb07f127 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Playground/forms/NavigationControls.tsx @@ -0,0 +1,87 @@ +/* eslint-disable i18next/no-literal-string */ + + + +/*! + * 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 { Card, Field, Heading, Text, VStack } from "@chakra-ui/react"; + +import { Pagination } from "src/components/ui"; + +type NavigationControlsProps = { + readonly currentPage: number; + readonly setCurrentPage: (page: number) => void; +}; + +export const NavigationControls = ({ currentPage, setCurrentPage }: NavigationControlsProps) => ( + + + Navigation Controls + + Pagination and navigation + + + + + + Pagination Component + setCurrentPage(details.page)} + page={currentPage} + pageSize={5} + siblingCount={1} + > + + + + + Page {currentPage} of 10 + + + + + Form Accessibility Features: + + + • Proper labeling for all inputs + • ARIA attributes for screen readers + • Keyboard navigation support + • Focus management and indicators + • Error state announcements + • Helper text associations + + + + + + Keyboard Shortcuts: + + + • Tab: Navigate between fields + • Space: Toggle checkboxes/switches + • Arrow keys: Navigate options + • Enter: Submit or activate + • Escape: Close dropdowns + + + + + +); diff --git a/airflow-core/src/airflow/ui/src/pages/Playground/forms/NumberRangeControls.tsx b/airflow-core/src/airflow/ui/src/pages/Playground/forms/NumberRangeControls.tsx new file mode 100644 index 0000000000000..3ebe39d41c34d --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Playground/forms/NumberRangeControls.tsx @@ -0,0 +1,111 @@ +/* eslint-disable i18next/no-literal-string */ + +/*! + * 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 { Card, Field, Heading, PinInput, Slider, Text, VStack } from "@chakra-ui/react"; +import { useState } from "react"; + +import { NumberInputField, NumberInputRoot, Switch } from "src/components/ui"; + +type NumberRangeControlsProps = { + readonly setSliderValue: (value: Array) => void; + readonly setSwitchValue: (value: boolean) => void; + readonly sliderValue: Array; + readonly switchValue: boolean; +}; + +export const NumberRangeControls = ({ + setSliderValue, + setSwitchValue, + sliderValue, + switchValue, +}: NumberRangeControlsProps) => { + const [numberValue, setNumberValue] = useState("0"); + const [pinValue, setPinValue] = useState(""); + + return ( + + + Number & Range Controls + + Numeric inputs and sliders + + + + + + Number Input + setNumberValue(details.value)} + value={numberValue} + > + + + + + + Range Slider + setSliderValue(details.value)} + step={1} + value={sliderValue} + > + + + + + + + Value: {sliderValue[0]} + + + + + PIN Input + setPinValue(details.value.join(""))} + value={[...pinValue].slice(0, 4)} + > + + {[0, 1, 2, 3].map((index) => ( + + ))} + + + Enter 4-digit PIN + + + + Switch Control + setSwitchValue(Boolean(details.checked))} + > + Toggle setting + + + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/Playground/forms/SelectionControls.tsx b/airflow-core/src/airflow/ui/src/pages/Playground/forms/SelectionControls.tsx new file mode 100644 index 0000000000000..61559c79ee8e9 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Playground/forms/SelectionControls.tsx @@ -0,0 +1,241 @@ +/* eslint-disable i18next/no-literal-string */ + +/* eslint-disable react/jsx-max-depth */ + +/*! + * 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 { + Card, + Field, + Fieldset, + Heading, + HStack, + NativeSelect, + RadioGroup, + Text, + VStack, +} from "@chakra-ui/react"; +import { useState } from "react"; + +import { Checkbox, RadioCardItem, RadioCardLabel, RadioCardRoot, SegmentedControl } from "src/components/ui"; + +type ControlsProps = { + readonly radioValue: string; + readonly setRadioValue: (value: string) => void; +}; + +export const Controls = ({ radioValue, setRadioValue }: ControlsProps) => { + const [selectValue, setSelectValue] = useState("option1"); + const [checkboxChecked, setCheckboxChecked] = useState(false); + const [radioCardValue, setRadioCardValue] = useState("option1"); + const [segmentedValue, setSegmentedValue] = useState>(["option1"]); + const [multipleSegmentedValue, setMultipleSegmentedValue] = useState>(["option1", "option2"]); + + return ( + + + Controls + + All form controls including dropdowns, radios, checkboxes, RadioCard, and SegmentedControl + + + + + + Select Dropdown + + setSelectValue(event.currentTarget.value)} + placeholder="Choose an option" + value={selectValue} + > + + + + + + + + + {/* Radio Group and Checkboxes in Two Columns */} + + {/* Radio Group Column */} + + + Radio Group + + setRadioValue(details.value ?? "")} + value={radioValue} + > + + + + + First option + + + + + Second option + + + + + Third option + + + + + + + + {/* Checkboxes Column */} + + + Checkboxes + + + setCheckboxChecked(Boolean(details.checked))} + size="sm" + > + Checkbox option + + + + Pre-checked option + + + + Disabled option + + + + + + + + {/* RadioCard Component */} + + RadioCard Component + + setRadioCardValue(details.value ?? "option1")} + value={radioCardValue} + > + + Choose deployment strategy + + + + + + + + + {/* SegmentedControl Components */} + + SegmentedControl Components + + + + + Single Selection + + { + setSegmentedValue(value); + console.log("Single selection changed:", value); + }} + options={[ + { label: "All", value: "option1" }, + { label: "Active", value: "option2" }, + { label: "Paused", value: "option3" }, + ]} + /> + + Selected: {segmentedValue.join(", ")} + + + + + + Multiple Selection + + { + setMultipleSegmentedValue(value); + console.log("Multiple selection changed:", value); + }} + options={[ + { label: "Past", value: "option1" }, + { label: "Future", value: "option2" }, + { label: "Upstream", value: "option3" }, + { label: "Downstream", value: "option4" }, + ]} + /> + + Selected: {multipleSegmentedValue.join(", ")} + + + + + + With Disabled Options + + { + // Handle segmented control change + }} + options={[ + { label: "Enabled", value: "enabled" }, + { disabled: true, label: "Disabled", value: "disabled" }, + { label: "Another", value: "another" }, + ]} + /> + + + + + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/Playground/forms/TextInputs.tsx b/airflow-core/src/airflow/ui/src/pages/Playground/forms/TextInputs.tsx new file mode 100644 index 0000000000000..c4aa8863c98dc --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Playground/forms/TextInputs.tsx @@ -0,0 +1,82 @@ +/* eslint-disable i18next/no-literal-string */ + +/*! + * 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 { Card, Field, Heading, Input, Text, Textarea, VStack } from "@chakra-ui/react"; +import { useState } from "react"; + +export const TextInputs = () => { + const [inputValue, setInputValue] = useState(""); + const [textareaValue, setTextareaValue] = useState(""); + + return ( + + + Text Inputs + + Input fields and text areas + + + + + + Standard Input + setInputValue(event.target.value)} + placeholder="Enter some text..." + value={inputValue} + /> + This is helper text for the input + + + + Password Input + + + + + Search Input + + + + + Disabled Input + + + + + Error State Input + + This field has an error + + + + Textarea +