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>
+  );
+};

Reply via email to