This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun 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 612a290a34e Enhance code view to support search and diff (#55467)
612a290a34e is described below
commit 612a290a34ec5ab697bca8f4f54fc633cafd0874
Author: Guan-Ming (Wesley) Chiu <[email protected]>
AuthorDate: Thu Dec 11 00:35:17 2025 +0800
Enhance code view to support search and diff (#55467)
* Enhance code view to support search and diff
* Refactor diff mode functionality in code view
---
airflow-core/src/airflow/ui/package.json | 1 +
airflow-core/src/airflow/ui/pnpm-lock.yaml | 41 ++++
.../airflow/ui/public/i18n/locales/en/common.json | 3 +
.../src/airflow/ui/src/pages/Dag/Code/Code.tsx | 249 ++++++++++++++-------
.../ui/src/pages/Dag/Code/CodeDiffViewer.tsx | 87 +++++++
.../ui/src/pages/Dag/Code/VersionCompareSelect.tsx | 112 +++++++++
6 files changed, 408 insertions(+), 85 deletions(-)
diff --git a/airflow-core/src/airflow/ui/package.json
b/airflow-core/src/airflow/ui/package.json
index def7a1a89fa..ff0923d3599 100644
--- a/airflow-core/src/airflow/ui/package.json
+++ b/airflow-core/src/airflow/ui/package.json
@@ -29,6 +29,7 @@
"@chakra-ui/react": "^3.20.0",
"@codemirror/lang-json": "^6.0.1",
"@emotion/react": "^11.14.0",
+ "@monaco-editor/react": "^4.7.0",
"@tanstack/react-query": "^5.75.1",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.8",
diff --git a/airflow-core/src/airflow/ui/pnpm-lock.yaml
b/airflow-core/src/airflow/ui/pnpm-lock.yaml
index 1cf8c881fc8..5402e1a086a 100644
--- a/airflow-core/src/airflow/ui/pnpm-lock.yaml
+++ b/airflow-core/src/airflow/ui/pnpm-lock.yaml
@@ -20,6 +20,9 @@ importers:
'@emotion/react':
specifier: ^11.14.0
version: 11.14.0(@types/[email protected])([email protected])
+ '@monaco-editor/react':
+ specifier: ^4.7.0
+ version:
4.7.0([email protected])([email protected]([email protected]))([email protected])
'@tanstack/react-query':
specifier: ^5.75.1
version: 5.75.4([email protected])
@@ -839,6 +842,16 @@ packages:
resolution: {integrity:
sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==}
engines: {node: '>=18'}
+ '@monaco-editor/[email protected]':
+ resolution: {integrity:
sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==}
+
+ '@monaco-editor/[email protected]':
+ resolution: {integrity:
sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==}
+ peerDependencies:
+ monaco-editor: '>= 0.25.0 < 1'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
'@mswjs/[email protected]':
resolution: {integrity:
sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==}
engines: {node: '>=18'}
@@ -1294,6 +1307,9 @@ packages:
'@types/[email protected]':
resolution: {integrity:
sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
+ '@types/[email protected]':
+ resolution: {integrity:
sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==}
+
'@types/[email protected]':
resolution: {integrity:
sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
@@ -3519,6 +3535,9 @@ packages:
[email protected]:
resolution: {integrity:
sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
+ [email protected]:
+ resolution: {integrity:
sha512-0WNThgC6CMWNXXBxTbaYYcunj08iB5rnx4/G56UOPeL9UVIUGGHA1GR0EWIh9Ebabj7NpCRawQ5b0hfN1jQmYQ==}
+
[email protected]:
resolution: {integrity:
sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -4210,6 +4229,9 @@ packages:
[email protected]:
resolution: {integrity:
sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+ [email protected]:
+ resolution: {integrity:
sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==}
+
[email protected]:
resolution: {integrity:
sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
@@ -5434,6 +5456,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@monaco-editor/[email protected]':
+ dependencies:
+ state-local: 1.0.7
+
+
'@monaco-editor/[email protected]([email protected])([email protected]([email protected]))([email protected])':
+ dependencies:
+ '@monaco-editor/loader': 1.5.0
+ monaco-editor: 0.53.0
+ react: 19.1.1
+ react-dom: 19.1.1([email protected])
+
'@mswjs/[email protected]':
dependencies:
'@open-draft/deferred-promise': 2.2.0
@@ -5832,6 +5865,8 @@ snapshots:
'@types/[email protected]': {}
+ '@types/[email protected]': {}
+
'@types/[email protected]': {}
'@types/[email protected]': {}
@@ -9093,6 +9128,10 @@ snapshots:
pkg-types: 1.3.1
ufo: 1.6.1
+ [email protected]:
+ dependencies:
+ '@types/trusted-types': 1.0.6
+
[email protected]: {}
[email protected](@types/[email protected])([email protected]):
@@ -9907,6 +9946,8 @@ snapshots:
[email protected]: {}
+ [email protected]: {}
+
[email protected]: {}
[email protected]: {}
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
index 9fd0cc4efe9..bf5e2a1f7d5 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
@@ -74,6 +74,9 @@
"dagWarnings": "Dag warnings/errors",
"defaultToGraphView": "Default to graph view",
"defaultToGridView": "Default to grid view",
+ "diff": "Diff",
+ "diffCompareWith": "Compare with",
+ "diffExit": "Exit Diff",
"direction": "Direction",
"docs": {
"documentation": "Documentation",
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Code/Code.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Code/Code.tsx
index 3331aeff014..dbe9e1f27ed 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Code/Code.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Code/Code.tsx
@@ -16,17 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Box, Button, Heading, HStack, Link } from "@chakra-ui/react";
+import { Box, Button, Heading, HStack, Link, VStack } from "@chakra-ui/react";
+import Editor, { type EditorProps } from "@monaco-editor/react";
import { useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
-import { createElement } from "react-syntax-highlighter";
import {
useDagServiceGetDagDetails,
useDagSourceServiceGetDagSource,
useDagVersionServiceGetDagVersion,
+ useDagVersionServiceGetDagVersions,
} from "openapi/queries";
import type { ApiError } from "openapi/requests/core/ApiError";
import type { DAGSourceResponse } from "openapi/requests/types.gen";
@@ -39,7 +40,9 @@ import { useColorMode } from "src/context/colorMode";
import useSelectedVersion from "src/hooks/useSelectedVersion";
import { useConfig } from "src/queries/useConfig";
import { renderDuration } from "src/utils";
-import { oneDark, oneLight, SyntaxHighlighter } from
"src/utils/syntaxHighlighter";
+
+import { CodeDiffViewer } from "./CodeDiffViewer";
+import { VersionCompareSelect } from "./VersionCompareSelect";
export const Code = () => {
const { t: translate } = useTranslation(["dag", "common"]);
@@ -64,6 +67,18 @@ export const Code = () => {
{ enabled: dag !== undefined && selectedVersion !== undefined },
);
+ const { data: dagVersions } = useDagVersionServiceGetDagVersions({
+ dagId: dagId ?? "",
+ });
+
+ const defaultWrap = Boolean(useConfig("default_wrap"));
+
+ const [wrap, setWrap] = useState(defaultWrap);
+ const [compareVersionNumber, setCompareVersionNumber] = useState<number |
undefined>(undefined);
+ const [isCompareDropdownOpen, setIsCompareDropdownOpen] = useState(false);
+
+ const isDiffMode = compareVersionNumber !== undefined;
+
const {
data: code,
error: codeError,
@@ -73,24 +88,58 @@ export const Code = () => {
versionNumber: selectedVersion,
});
- const defaultWrap = Boolean(useConfig("default_wrap"));
-
- const [wrap, setWrap] = useState(defaultWrap);
+ const {
+ data: compareCode,
+ error: compareCodeError,
+ isLoading: isCompareCodeLoading,
+ } = useDagSourceServiceGetDagSource<DAGSourceResponse, ApiError | null>(
+ {
+ dagId: dagId ?? "",
+ versionNumber: compareVersionNumber,
+ },
+ undefined,
+ { enabled: isDiffMode },
+ );
const toggleWrap = () => setWrap(!wrap);
+ const toggleCompareDropdown = () =>
setIsCompareDropdownOpen(!isCompareDropdownOpen);
+ const exitDiffMode = () => {
+ setCompareVersionNumber(undefined);
+ setIsCompareDropdownOpen(false);
+ };
+ const handleVersionChange = (versionNumber: number) => {
+ setCompareVersionNumber(versionNumber);
+ setIsCompareDropdownOpen(false);
+ };
+
const { colorMode } = useColorMode();
useHotkeys("w", toggleWrap);
- const style = colorMode === "dark" ? oneDark : oneLight;
+ const editorOptions: EditorProps["options"] = {
+ automaticLayout: true,
+ contextmenu: false,
+ find: {
+ addExtraSpaceOnTop: false,
+ autoFindInSelection: "never" as const,
+ seedSearchStringFromSelection: "always" as const,
+ },
+ fontSize: 14,
+ glyphMargin: false,
+ lineDecorationsWidth: 20,
+ lineNumbers: "on",
+ minimap: { enabled: false },
+ readOnly: true,
+ renderLineHighlight: "none",
+ wordWrap: wrap ? "on" : "off",
+ };
+
+ const theme = colorMode === "dark" ? "vs-dark" : "vs-light";
- // wrapLongLines wasn't working with the prsim styles so we have to manually
apply the style
- if (style['code[class*="language-"]'] !== undefined) {
- style['code[class*="language-"]'].whiteSpace = wrap ? "pre-wrap" : "pre";
- }
+ const hasMultipleVersions = (dagVersions?.dag_versions.length ?? 0) >= 2;
return (
- <Box>
+ <Box h="100%" overflow="hidden">
<HStack justifyContent="space-between" mt={2}>
<HStack gap={5}>
{dag?.last_parsed_time !== undefined && (
@@ -127,83 +176,113 @@ export const Code = () => {
) : undefined
}
</HStack>
- <HStack>
- <DagVersionSelect showLabel={false} />
- <ClipboardRoot value={code?.content ?? ""}>
- <ClipboardButton />
- </ClipboardRoot>
- <Tooltip
- closeDelay={100}
- content={translate("common:wrap.tooltip", { hotkey: "w" })}
- openDelay={100}
- >
- <Button
- aria-label={translate(`common:wrap.${wrap ? "un" : ""}wrap`)}
- onClick={toggleWrap}
- variant="outline"
+ <VStack gap={2} position="relative">
+ <HStack flexWrap="wrap" gap={2}>
+ <DagVersionSelect showLabel={false} />
+ <ClipboardRoot value={code?.content ?? ""}>
+ <ClipboardButton />
+ </ClipboardRoot>
+ <Tooltip
+ closeDelay={100}
+ content={translate("common:wrap.tooltip", { hotkey: "w" })}
+ openDelay={100}
>
- {translate(`common:wrap.${wrap ? "un" : ""}wrap`)}
- </Button>
- </Tooltip>
- </HStack>
+ <Button
+ aria-label={translate(`common:wrap.${wrap ? "un" : ""}wrap`)}
+ onClick={toggleWrap}
+ variant="outline"
+ >
+ {translate(`common:wrap.${wrap ? "un" : ""}wrap`)}
+ </Button>
+ </Tooltip>
+ {hasMultipleVersions ? (
+ <Button
+ aria-label={translate("common:diff")}
+ onClick={toggleCompareDropdown}
+ variant={isCompareDropdownOpen ? "solid" : "outline"}
+ >
+ {translate("common:diff")}
+ </Button>
+ ) : undefined}
+ {isDiffMode ? (
+ <Button aria-label={translate("common:diffExit")}
onClick={exitDiffMode} variant="solid">
+ {translate("common:diffExit")}
+ </Button>
+ ) : undefined}
+ </HStack>
+ {isCompareDropdownOpen ? (
+ <Box
+ bg="bg.panel"
+ borderRadius="md"
+ insetInlineEnd={0}
+ mt={4}
+ p={2}
+ position="absolute"
+ shadow="sm"
+ top="100%"
+ zIndex={10}
+ >
+ <VersionCompareSelect
+ label={translate("common:diffCompareWith")}
+ onVersionChange={handleVersionChange}
+ placeholder="Select version to compare"
+ selectedVersionNumber={compareVersionNumber}
+ />
+ </Box>
+ ) : undefined}
+ </VStack>
</HStack>
{/* We want to show an empty state on 404 instead of an error */}
- <ErrorAlert error={error ?? (codeError?.status === 404 ? undefined :
codeError)} />
- <ProgressBar size="xs" visibility={isLoading || isCodeLoading ?
"visible" : "hidden"} />
- <Box
- css={{
- "& *::selection": {
- bg: "blue.emphasized",
- },
- }}
- fontSize="14px"
- >
- <SyntaxHighlighter
- language="python"
- renderer={({ rows, stylesheet, useInlineStyles }) =>
- rows.map((row, index) => {
- const { children } = row;
- const lineNumberElement = children?.shift();
-
- // Skip line number span when applying line break styles
https://github.com/react-syntax-highlighter/react-syntax-highlighter/issues/376#issuecomment-1584440759
- if (lineNumberElement) {
- if (lineNumberElement.properties) {
- lineNumberElement.properties.style = {
- ...(lineNumberElement.properties.style as Record<string,
string>),
- WebkitUserSelect: "none",
- };
- }
-
- row.children = [
- lineNumberElement,
- {
- children,
- properties: {
- className: [],
- },
- tagName: "span",
- type: "element",
- },
- ];
- }
-
- return createElement({
- key: index,
- node: row,
- stylesheet,
- useInlineStyles,
- });
- })
- }
- showLineNumbers
- style={style}
- wrapLongLines={wrap}
+ <ErrorAlert
+ error={
+ error ??
+ (codeError?.status === 404 ? undefined : codeError) ??
+ (compareCodeError?.status === 404 ? undefined : compareCodeError)
+ }
+ />
+ <ProgressBar
+ size="xs"
+ visibility={isLoading || isCodeLoading || isCompareCodeLoading ?
"visible" : "hidden"}
+ />
+
+ {isDiffMode ? (
+ <Box dir="ltr" height="full">
+ <CodeDiffViewer
+ modifiedCode={
+ codeError?.status === 404 && !Boolean(code?.content)
+ ? translate("code.noCode")
+ : (code?.content ?? "")
+ }
+ originalCode={
+ compareCodeError?.status === 404 &&
!Boolean(compareCode?.content)
+ ? translate("code.noCode")
+ : (compareCode?.content ?? "")
+ }
+ />
+ </Box>
+ ) : (
+ <Box
+ css={{
+ "& *::selection": {
+ bg: "gray.emphasized",
+ },
+ }}
+ dir="ltr"
+ fontSize="14px"
+ height="full"
>
- {codeError?.status === 404 && !Boolean(code?.content)
- ? translate("code.noCode")
- : (code?.content ?? "")}
- </SyntaxHighlighter>
- </Box>
+ <Editor
+ language="python"
+ options={editorOptions}
+ theme={theme}
+ value={
+ codeError?.status === 404 && !Boolean(code?.content)
+ ? translate("code.noCode")
+ : (code?.content ?? "")
+ }
+ />
+ </Box>
+ )}
</Box>
);
};
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Code/CodeDiffViewer.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Code/CodeDiffViewer.tsx
new file mode 100644
index 00000000000..8ca2a6e41cb
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Code/CodeDiffViewer.tsx
@@ -0,0 +1,87 @@
+/*!
+ * 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 } from "@chakra-ui/react";
+import { DiffEditor, type DiffEditorProps } from "@monaco-editor/react";
+
+import { useColorMode } from "src/context/colorMode";
+
+type CodeDiffViewerProps = {
+ readonly height?: string;
+ readonly language?: string;
+ readonly modifiedCode: string;
+ readonly originalCode: string;
+ readonly renderSideBySide?: boolean;
+};
+
+export const CodeDiffViewer = ({
+ height = "full",
+ language = "python",
+ modifiedCode,
+ originalCode,
+ renderSideBySide = true,
+}: CodeDiffViewerProps) => {
+ const { colorMode } = useColorMode();
+
+ const diffOptions: DiffEditorProps["options"] = {
+ automaticLayout: true,
+ contextmenu: false,
+ find: {
+ addExtraSpaceOnTop: false,
+ autoFindInSelection: "never" as const,
+ seedSearchStringFromSelection: "always" as const,
+ },
+ fontSize: 14,
+ glyphMargin: false,
+ ignoreTrimWhitespace: false,
+ lineDecorationsWidth: 20,
+ lineNumbers: "on",
+ minimap: { enabled: false },
+ originalEditable: false,
+ readOnly: true,
+ renderLineHighlight: "none",
+ renderSideBySide,
+ scrollbar: {
+ vertical: "hidden",
+ verticalScrollbarSize: 10,
+ },
+ };
+
+ const theme = colorMode === "dark" ? "vs-dark" : "vs-light";
+
+ return (
+ <Box
+ css={{
+ "& *::selection": {
+ bg: "gray.emphasized",
+ },
+ }}
+ fontSize="14px"
+ height={height}
+ zIndex={1}
+ >
+ <DiffEditor
+ language={language}
+ modified={modifiedCode}
+ options={diffOptions}
+ original={originalCode}
+ theme={theme}
+ />
+ </Box>
+ );
+};
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/Code/VersionCompareSelect.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Code/VersionCompareSelect.tsx
new file mode 100644
index 00000000000..954d55729e7
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Code/VersionCompareSelect.tsx
@@ -0,0 +1,112 @@
+/*!
+ * 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 { createListCollection, Flex, Select, type SelectValueChangeDetails,
Text } from "@chakra-ui/react";
+import { useCallback, useMemo } from "react";
+import { useTranslation } from "react-i18next";
+import { useParams } from "react-router-dom";
+
+import { useDagVersionServiceGetDagVersions } from "openapi/queries";
+import type { DagVersionResponse } from "openapi/requests/types.gen";
+import Time from "src/components/Time";
+
+type VersionSelected = {
+ value: number;
+ version: DagVersionResponse;
+};
+
+type VersionCompareSelectProps = {
+ readonly label: string;
+ readonly onVersionChange: (versionNumber: number) => void;
+ readonly placeholder?: string;
+ readonly selectedVersionNumber?: number;
+};
+
+export const VersionCompareSelect = ({
+ label,
+ onVersionChange,
+ placeholder = "Select version",
+ selectedVersionNumber,
+}: VersionCompareSelectProps) => {
+ const { t: translate } = useTranslation("components");
+ const { dagId = "" } = useParams();
+ const { data, isLoading } = useDagVersionServiceGetDagVersions({ dagId,
orderBy: ["-version_number"] });
+
+ const selectedVersion = data?.dag_versions.find((dv) => dv.version_number
=== selectedVersionNumber);
+
+ const versionOptions = useMemo(
+ () =>
+ createListCollection({
+ items: (data?.dag_versions ?? []).map((dv) => ({ value:
dv.version_number, version: dv })),
+ }),
+ [data],
+ );
+
+ const handleStateChange = useCallback(
+ ({ items }: SelectValueChangeDetails<VersionSelected>) => {
+ if (items[0]) {
+ onVersionChange(items[0].value);
+ }
+ },
+ [onVersionChange],
+ );
+
+ return (
+ <Select.Root
+ collection={versionOptions}
+ disabled={isLoading || !data?.dag_versions}
+ onValueChange={handleStateChange}
+ size="sm"
+ value={selectedVersionNumber === undefined ? [] :
[selectedVersionNumber.toString()]}
+ w={60}
+ >
+ <Select.Label fontSize="xs">{label}</Select.Label>
+ <Select.Control>
+ <Select.Trigger>
+ <Select.ValueText placeholder={placeholder}>
+ {selectedVersion === undefined ? undefined : (
+ <Flex gap={2} justifyContent="space-between">
+ <Text>
+ {translate("versionSelect.versionCode", { versionCode:
selectedVersion.version_number })}
+ </Text>
+ <Time datetime={selectedVersion.created_at} />
+ </Flex>
+ )}
+ </Select.ValueText>
+ </Select.Trigger>
+ <Select.IndicatorGroup>
+ <Select.Indicator />
+ </Select.IndicatorGroup>
+ </Select.Control>
+ <Select.Positioner>
+ <Select.Content>
+ {versionOptions.items.map((option) => (
+ <Select.Item item={option} key={option.version.version_number}>
+ <Flex gap={2} justifyContent="space-between" w="full">
+ <Text>
+ {translate("versionSelect.versionCode", { versionCode:
option.version.version_number })}
+ </Text>
+ <Time datetime={option.version.created_at} />
+ </Flex>
+ </Select.Item>
+ ))}
+ </Select.Content>
+ </Select.Positioner>
+ </Select.Root>
+ );
+};