Repository: apache/airflow
Pull request: Render DAGCode in the Grid View as a tab
closes: #29858

Using REST API to render DAG Code in the grid view as a tab when a user has no tasks selected.

![image](https://user-images.githubusercontent.com/1306697/236638862-3e732fb2-940f-4848-9284-671541d87909.png)


<!--
Thank you for contributing! Please make sure that your code changes
are covered with tests. And in case of new features or big changes
remember to adjust the documentation.

Feel free to ping committers for the review!

In case of an existing issue, reference it using one of the following:

closes: #ISSUE
related: #ISSUE

How to write a good git commit message:
http://chris.beams.io/posts/git-commit/
-->

---
**^ Add meaningful description above**

Read the **[Pull Request Guidelines](https://github.com/apache/airflow/blob/main/CONTRIBUTING.rst#pull-request-guidelines)** for more information.
In case of fundamental code changes, an Airflow Improvement Proposal ([AIP](https://cwiki.apache.org/confluence/display/AIRFLOW/Airflow+Improvement+Proposals)) is needed.
In case of a new dependency, check compliance with the [ASF 3rd Party License Policy](https://www.apache.org/legal/resolved.html#category-x).
In case of backwards incompatible changes please leave a note in a newsfragment file, named `{pr_number}.significant.rst` or `{issue_number}.significant.rst`, in [newsfragments](https://github.com/apache/airflow/tree/main/newsfragments).


Problem statement:
Migrate DAG Code page to Grid Details
- [ ] Use REST API to render DAG Code in the grid view as a tab when a user has no runs/tasks selected
- [ ]  Redirect all urls to new code 
- [ ] delete the old code view

Hints from issue discussion:
Hello @bbovenzi, Can I take a stab at this one?
Feel free.
@bbovenzi @potiuk Can you please help me point to any additional details around this request to get me started? Thanks for your time!
What do you miss? Generally we expect anyone who takes on the tasks to be pretty self-sufficient using our code and documentation, contributing guides (see CONTRIBUTE.rst) and only reach out when they have specific questions about problems they had by following them.

Do you have some problems/questions?
I understand that this request is part of bigger UI initiative which @bbovenzi is also working on. 
Wanted to make sure if he has any more details to add to this. and also how this will fit in along with the other PRs for this UI.

I will read through the existing PRs that are being worked on as part of this UI upgrade and will get back if I have any further questions.

Thanks!
We have a GET `dagSources/{file_token}` endpoint in the REST API. (file token comes from the get dag endpoint)

My thought was adding it as a tab in the grid view kind of like how details, graph and logs works right now. What do you think?
@bbovenzi Thanks for the rest api reference.  
I will work on this and have something for your review in the next few days.

Commit message: Removed console.log

Patch to review:
diff --git a/airflow/www/package.json b/airflow/www/package.json
index b15568593cc84..15d9279740895 100644
--- a/airflow/www/package.json
+++ b/airflow/www/package.json
@@ -41,6 +41,7 @@
     "@types/color": "^3.0.3",
     "@types/react": "^18.0.12",
     "@types/react-dom": "^18.0.5",
+    "@types/react-syntax-highlighter": "^15.5.6",
     "@types/react-table": "^7.7.12",
     "@typescript-eslint/eslint-plugin": "^5.13.0",
     "@typescript-eslint/parser": "^5.0.0",
@@ -121,6 +122,7 @@
     "react-markdown": "^8.0.4",
     "react-query": "^3.39.1",
     "react-router-dom": "^6.3.0",
+    "react-syntax-highlighter": "^15.5.0",
     "react-table": "^7.8.0",
     "react-textarea-autosize": "^8.3.4",
     "reactflow": "^11.4.0",
diff --git a/airflow/www/static/js/api/index.ts b/airflow/www/static/js/api/index.ts
index 018f4e97b6068..e0c9177e57312 100644
--- a/airflow/www/static/js/api/index.ts
+++ b/airflow/www/static/js/api/index.ts
@@ -40,6 +40,7 @@ import useSetDagRunNote from "./useSetDagRunNote";
 import useSetTaskInstanceNote from "./useSetTaskInstanceNote";
 import useUpstreamDatasetEvents from "./useUpstreamDatasetEvents";
 import useTaskInstance from "./useTaskInstance";
+import useDagCode from "./useDagCode";
 
 axios.interceptors.response.use((res: AxiosResponse) =>
   res.data ? camelcaseKeys(res.data, { deep: true }) : res
@@ -68,4 +69,5 @@ export {
   useSetTaskInstanceNote,
   useTaskInstance,
   useUpstreamDatasetEvents,
+  useDagCode,
 };
diff --git a/airflow/www/static/js/api/useDagCode.ts b/airflow/www/static/js/api/useDagCode.ts
new file mode 100644
index 0000000000000..2a34b725fb6ed
--- /dev/null
+++ b/airflow/www/static/js/api/useDagCode.ts
@@ -0,0 +1,98 @@
+/*!
+ * 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 axios, { AxiosResponse } from "axios";
+import { useQuery } from "react-query";
+import { useRef, useEffect, useState } from "react";
+
+import { getMetaValue } from "src/utils";
+import type { API } from "src/types";
+import useErrorToast from "../utils/useErrorToast";
+
+interface DagCodeData {
+  lastParsedTime?: string;
+  codeSource?: string;
+}
+
+export default function useDagCode() {
+  const errorToast = useErrorToast();
+  const [data, setData] = useState<DagCodeData>({
+    lastParsedTime: "N/A",
+    codeSource: "",
+  });
+
+  const dagQuery = useQuery(
+    ["dagQuery"],
+    () => {
+      const dagApiUrl = getMetaValue("dag_api");
+      return axios.get<AxiosResponse, API.DAG>(dagApiUrl);
+    },
+    {
+      onError: (error: Error) => {
+        errorToast({
+          error,
+        });
+      },
+    }
+  );
+
+  const dagSourceQuery = useQuery(
+    ["dagSourceQuery"],
+    () => {
+      const fileToken = dagQuery.data?.fileToken || "";
+      const dagSourceApiUrl = getMetaValue("dag_source_api").replace(
+        "_FILE_TOKEN_",
+        fileToken
+      );
+      return axios.get<AxiosResponse, string>(dagSourceApiUrl, {
+        headers: { Accept: "text/plain" },
+      });
+    },
+    {
+      enabled: false,
+      onError: (error: Error) => {
+        errorToast({
+          error,
+        });
+      },
+    }
+  );
+  const dagSourceQueryRef = useRef(dagSourceQuery);
+  useEffect(() => {
+    if (dagQuery.isSuccess) {
+      dagSourceQueryRef.current.refetch();
+    }
+  }, [dagQuery.isSuccess, dagSourceQueryRef]);
+
+  useEffect(() => {
+    if (dagQuery.isSuccess && dagSourceQuery.isSuccess) {
+      setData({
+        lastParsedTime: dagQuery.data?.lastParsedTime || "N/A",
+        codeSource: dagSourceQuery.data || "",
+      });
+    }
+  }, [
+    dagQuery.isSuccess,
+    dagQuery.data,
+    dagSourceQuery.isSuccess,
+    dagSourceQuery.data,
+  ]);
+
+  return data;
+}
diff --git a/airflow/www/static/js/dag/details/dagCode/CodeBlock.tsx b/airflow/www/static/js/dag/details/dagCode/CodeBlock.tsx
new file mode 100644
index 0000000000000..a49507bcc165c
--- /dev/null
+++ b/airflow/www/static/js/dag/details/dagCode/CodeBlock.tsx
@@ -0,0 +1,69 @@
+/*!
+ * 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 { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter";
+import { oneLight } from "react-syntax-highlighter/dist/esm/styles/prism";
+import python from "react-syntax-highlighter/dist/esm/languages/prism/python";
+import React, { useRef, useState } from "react";
+import { Box, Button } from "@chakra-ui/react";
+
+SyntaxHighlighter.registerLanguage("python", python);
+
+interface Props {
+  code: string;
+}
+export default function CodeBlock({ code }: Props) {
+  const dagCodeRef = useRef(null);
+  const [codeWrap, setCodeWrap] = useState(false);
+  const toggleCodeWrap = () => setCodeWrap(!codeWrap);
+
+  return (
+    <Box
+      ref={dagCodeRef}
+      overflow="scroll"
+      height="calc(100% - 10px)"
+      borderWidth={2}
+      borderColor="gray:100"
+      position="relative"
+      fontSize="13.5px"
+    >
+      <Button
+        colorScheme="cyan"
+        variant="outline"
+        background="white"
+        aria-label="Toggle Wrap"
+        position="absolute"
+        top="15px"
+        right="15px"
+        fontSize="13.5px"
+        onClick={toggleCodeWrap}
+      >
+        Toggle Wrap
+      </Button>
+      <SyntaxHighlighter
+        language="python"
+        style={oneLight}
+        showLineNumbers
+        wrapLongLines={codeWrap}
+      >
+        {code}
+      </SyntaxHighlighter>
+    </Box>
+  );
+}
diff --git a/airflow/www/static/js/dag/details/dagCode/index.tsx b/airflow/www/static/js/dag/details/dagCode/index.tsx
new file mode 100644
index 0000000000000..667cb98c19493
--- /dev/null
+++ b/airflow/www/static/js/dag/details/dagCode/index.tsx
@@ -0,0 +1,43 @@
+/*!
+ * 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 React, { useRef } from "react";
+import { Box, Heading } from "@chakra-ui/react";
+import { useOffsetTop } from "src/utils";
+import Time from "src/components/Time";
+
+import { useDagCode } from "src/api";
+import CodeBlock from "./CodeBlock";
+
+const DagCode = () => {
+  const dagCodeRef = useRef<HTMLDivElement>(null);
+  const offsetTop = useOffsetTop(dagCodeRef);
+  const { lastParsedTime, codeSource = "" } = useDagCode();
+
+  return (
+    <Box ref={dagCodeRef} height={`calc(100% - ${offsetTop}px)`}>
+      <Heading as="h4" size="md" paddingBottom="10px" fontSize="14px">
+        Parsed at: <Time dateTime={lastParsedTime} />
+      </Heading>
+      <CodeBlock code={codeSource} />
+    </Box>
+  );
+};
+
+export default DagCode;
diff --git a/airflow/www/static/js/dag/details/index.tsx b/airflow/www/static/js/dag/details/index.tsx
index bf3dc6ccfa97d..d696d50495ec7 100644
--- a/airflow/www/static/js/dag/details/index.tsx
+++ b/airflow/www/static/js/dag/details/index.tsx
@@ -33,7 +33,7 @@ import { useSearchParams } from "react-router-dom";
 import useSelection from "src/dag/useSelection";
 import { getTask, getMetaValue } from "src/utils";
 import { useGridData, useTaskInstance } from "src/api";
-import { MdDetails, MdAccountTree, MdReorder } from "react-icons/md";
+import { MdDetails, MdAccountTree, MdReorder, MdCode } from "react-icons/md";
 import { BiBracket } from "react-icons/bi";
 import URLSearchParamsWrapper from "src/utils/URLSearchParamWrapper";
 
@@ -42,6 +42,7 @@ import TaskInstanceContent from "./taskInstance";
 import DagRunContent from "./dagRun";
 import DagContent from "./Dag";
 import Graph from "./graph";
+import DagCode from "./dagCode";
 import MappedInstances from "./taskInstance/MappedInstances";
 import Logs from "./taskInstance/Logs";
 import BackToTaskSummary from "./taskInstance/BackToTaskSummary";
@@ -63,9 +64,11 @@ const tabToIndex = (tab?: string) => {
   switch (tab) {
     case "graph":
       return 1;
+    case "code":
+      return 2;
     case "logs":
     case "mapped_tasks":
-      return 2;
+      return 3;
     case "details":
     default:
       return 0;
@@ -81,6 +84,8 @@ const indexToTab = (
     case 1:
       return "graph";
     case 2:
+      return "code";
+    case 3:
       if (showMappedTasks) return "mapped_tasks";
       if (showLogs) return "logs";
       return undefined;
@@ -112,6 +117,7 @@ const Details = ({ openGroupIds, onToggleGroups, hoveredTaskState }: Props) => {
   const isGroup = !!children;
   const isGroupOrMappedTaskSummary = isGroup || isMappedTaskSummary;
   const showLogs = !!(isTaskInstance && !isGroupOrMappedTaskSummary);
+  const showDagCode = !(isTaskInstance && !isGroupOrMappedTaskSummary);
   const showMappedTasks = !!(isTaskInstance && isMappedTaskSummary && !isGroup);
 
   const [searchParams, setSearchParams] = useSearchParams();
@@ -130,7 +136,7 @@ const Details = ({ openGroupIds, onToggleGroups, hoveredTaskState }: Props) => {
   );
 
   useEffect(() => {
-    if ((!taskId || isGroup) && tabIndex > 1) {
+    if ((!taskId || isGroup) && tabIndex > 2) {
       onChangeTab(1);
     }
   }, [runId, taskId, tabIndex, isGroup, onChangeTab]);
@@ -206,6 +212,14 @@ const Details = ({ openGroupIds, onToggleGroups, hoveredTaskState }: Props) => {
               Graph
             </Text>
           </Tab>
+          {showDagCode && (
+            <Tab>
+              <MdCode size={16} />
+              <Text as="strong" ml={1}>
+                Code
+              </Text>
+            </Tab>
+          )}
           {showLogs && (
             <Tab>
               <MdReorder size={16} />
@@ -248,6 +262,11 @@ const Details = ({ openGroupIds, onToggleGroups, hoveredTaskState }: Props) => {
               hoveredTaskState={hoveredTaskState}
             />
           </TabPanel>
+          {showDagCode && (
+            <TabPanel height="100%">
+              <DagCode />
+            </TabPanel>
+          )}
           {showLogs && run && (
             <TabPanel
               pt={mapIndex !== undefined ? "0px" : undefined}
diff --git a/airflow/www/templates/airflow/dag.html b/airflow/www/templates/airflow/dag.html
index 0fdc5473435dd..93acbcbb04d05 100644
--- a/airflow/www/templates/airflow/dag.html
+++ b/airflow/www/templates/airflow/dag.html
@@ -75,6 +75,8 @@
   <meta name="set_task_instance_note" content="{{ url_for('/api/v1.airflow_api_connexion_endpoints_task_instance_endpoint_set_task_instance_note', dag_id=dag.dag_id, dag_run_id='_DAG_RUN_ID_', task_id='_TASK_ID_' ) }}">
   <meta name="set_mapped_task_instance_note" content="{{ url_for('/api/v1.airflow_api_connexion_endpoints_task_instance_endpoint_set_mapped_task_instance_note', dag_id=dag.dag_id, dag_run_id='_DAG_RUN_ID_', task_id='_TASK_ID_', map_index=0 ) }}">
   <meta name="set_dag_run_note" content="{{ url_for('/api/v1.airflow_api_connexion_endpoints_dag_run_endpoint_set_dag_run_note', dag_id=dag.dag_id, dag_run_id='_DAG_RUN_ID_') }}">
+  <meta name="dag_api" content="{{ url_for('/api/v1.airflow_api_connexion_endpoints_dag_endpoint_get_dag', dag_id=dag.dag_id) }}">
+  <meta name="dag_source_api" content="{{ url_for('/api/v1.airflow_api_connexion_endpoints_dag_source_endpoint_get_dag_source', file_token='_FILE_TOKEN_') }}">
 
   <!-- End Urls -->
   <meta name="is_paused" content="{{ dag_is_paused }}">
diff --git a/airflow/www/yarn.lock b/airflow/www/yarn.lock
index b3ef3a3d80eca..3f8de910ccb28 100644
--- a/airflow/www/yarn.lock
+++ b/airflow/www/yarn.lock
@@ -1361,6 +1361,13 @@
   dependencies:
     regenerator-runtime "^0.13.11"
 
+"@babel/runtime@^7.3.1":
+  version "7.21.5"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200"
+  integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==
+  dependencies:
+    regenerator-runtime "^0.13.11"
+
 "@babel/runtime@^7.7.6":
   version "7.17.9"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
@@ -3186,6 +3193,13 @@
   dependencies:
     "@types/react" "*"
 
+"@types/react-syntax-highlighter@^15.5.6":
+  version "15.5.6"
+  resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.6.tgz#77c95e6b74d2be23208fcdcf187b93b47025f1b1"
+  integrity sha512-i7wFuLbIAFlabTeD2I1cLjEOrG/xdMa/rpx2zwzAoGHuXJDhSqp9BSfDlMHSh9JSuNfxHk9eEmMX6D55GiyjGg==
+  dependencies:
+    "@types/react" "*"
+
 "@types/react-table@^7.7.12":
   version "7.7.12"
   resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.12.tgz#628011d3cb695b07c678704a61f2f1d5b8e567fd"
@@ -4522,6 +4536,11 @@ combined-stream@^1.0.8:
   dependencies:
     delayed-stream "~1.0.0"
 
+comma-separated-tokens@^1.0.0:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea"
+  integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==
+
 comma-separated-tokens@^2.0.0:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
@@ -6146,6 +6165,13 @@ fastq@^1.6.0:
   dependencies:
     reusify "^1.0.4"
 
+fault@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13"
+  integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==
+  dependencies:
+    format "^0.2.0"
+
 fb-watchman@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85"
@@ -6285,6 +6311,11 @@ form-data@^3.0.0:
     combined-stream "^1.0.8"
     mime-types "^2.1.12"
 
+format@^0.2.0:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
+  integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==
+
 framer-motion@^6.0.0:
   version "6.3.11"
   resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-6.3.11.tgz#c304ce9728601ad9377d47d5d9264e43d741d470"
@@ -6620,16 +6651,37 @@ has@^1.0.3:
   dependencies:
     function-bind "^1.1.1"
 
+hast-util-parse-selector@^2.0.0:
+  version "2.2.5"
+  resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a"
+  integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==
+
 hast-util-whitespace@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz#4fc1086467cc1ef5ba20673cb6b03cec3a970f1c"
   integrity sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg==
 
+hastscript@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640"
+  integrity sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==
+  dependencies:
+    "@types/hast" "^2.0.0"
+    comma-separated-tokens "^1.0.0"
+    hast-util-parse-selector "^2.0.0"
+    property-information "^5.0.0"
+    space-separated-tokens "^1.0.0"
+
 hey-listen@^1.0.8:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
   integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
 
+highlight.js@^10.4.1, highlight.js@~10.7.0:
+  version "10.7.3"
+  resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
+  integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==
+
 history@^5.2.0:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b"
@@ -7994,6 +8046,14 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
   dependencies:
     js-tokens "^3.0.0 || ^4.0.0"
 
+lowlight@^1.17.0:
+  version "1.20.0"
+  resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888"
+  integrity sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==
+  dependencies:
+    fault "^1.0.0"
+    highlight.js "~10.7.0"
+
 lru-cache@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@@ -9646,6 +9706,11 @@ prismjs@^1.27.0:
   resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.28.0.tgz#0d8f561fa0f7cf6ebca901747828b149147044b6"
   integrity sha512-8aaXdYvl1F7iC7Xm1spqSaY/OJBpYW3v+KJ+F17iYxvdc8sfjW194COK5wVhMZX45tGteiBQgdvD/nhxcRwylw==
 
+prismjs@~1.27.0:
+  version "1.27.0"
+  resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057"
+  integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==
+
 promise-inflight@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
@@ -9689,6 +9754,13 @@ propagate@^2.0.0:
   resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45"
   integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==
 
+property-information@^5.0.0:
+  version "5.6.0"
+  resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69"
+  integrity sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==
+  dependencies:
+    xtend "^4.0.0"
+
 property-information@^6.0.0:
   version "6.2.0"
   resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.2.0.tgz#b74f522c31c097b5149e3c3cb8d7f3defd986a1d"
@@ -9894,6 +9966,17 @@ react-style-singleton@^2.2.1:
     invariant "^2.2.4"
     tslib "^2.0.0"
 
+react-syntax-highlighter@^15.5.0:
+  version "15.5.0"
+  resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20"
+  integrity sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==
+  dependencies:
+    "@babel/runtime" "^7.3.1"
+    highlight.js "^10.4.1"
+    lowlight "^1.17.0"
+    prismjs "^1.27.0"
+    refractor "^3.6.0"
+
 react-table@^7.8.0:
   version "7.8.0"
   resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.8.0.tgz#07858c01c1718c09f7f1aed7034fcfd7bda907d2"
@@ -10025,6 +10108,15 @@ redoc@^2.0.0-rc.72:
     swagger2openapi "^7.0.6"
     url-template "^2.0.8"
 
+refractor@^3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a"
+  integrity sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==
+  dependencies:
+    hastscript "^6.0.0"
+    parse-entities "^2.0.0"
+    prismjs "~1.27.0"
+
 reftools@^1.1.8:
   version "1.1.8"
   resolved "https://registry.yarnpkg.com/reftools/-/reftools-1.1.8.tgz#cc08fd67eb913d779fd330657d010cc080c7d643"
@@ -10543,6 +10635,11 @@ source-map@^0.7.3:
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
   integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
 
+space-separated-tokens@^1.0.0:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899"
+  integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==
+
 space-separated-tokens@^2.0.0:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f"
@@ -11842,6 +11939,11 @@ xmlchars@^2.2.0:
   resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
   integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
 
+xtend@^4.0.0:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+
 y18n@^5.0.5:
   version "5.0.8"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"


Code review comments:
diff_hunk: @@ -0,0 +1,98 @@
+/*!
+ * 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 axios, { AxiosResponse } from "axios";
+import { useQuery } from "react-query";
+import { useRef, useEffect, useState } from "react";
+
+import { getMetaValue } from "src/utils";
+import type { API } from "src/types";
+import useErrorToast from "../utils/useErrorToast";
+
+interface DagCodeData {
+  lastParsedTime?: string;
+  codeSource?: string;
+}
+
+export default function useDagCode() {
+  const errorToast = useErrorToast();
+  const [data, setData] = useState<DagCodeData>({
+    lastParsedTime: "N/A",
+    codeSource: "",
+  });
+
+  const dagQuery = useQuery(
+    ["dagQuery"],
+    () => {
+      const dagApiUrl = getMetaValue("dag_api");
+      return axios.get<AxiosResponse, API.DAG>(dagApiUrl);
+    },
+    {
+      onError: (error: Error) => {
+        errorToast({
+          error,
+        });
+      },
+    }
+  );
+
+  const dagSourceQuery = useQuery(
+    ["dagSourceQuery"],
+    () => {
+      const fileToken = dagQuery.data?.fileToken || "";
+      const dagSourceApiUrl = getMetaValue("dag_source_api").replace(
+        "_FILE_TOKEN_",
+        fileToken
+      );
+      return axios.get<AxiosResponse, string>(dagSourceApiUrl, {
+        headers: { Accept: "text/plain" },
+      });
+    },
+    {
+      enabled: false,
path: airflow/www/static/js/api/useDagCode.ts
text: @user1:
```suggestion
      enabled: !!dagQuery.data.fileToken
```

And then we can return the entire dagSourceQuery and remove all of the `useEffect`s

@user1:
Documentation: https://tanstack.com/query/v3/docs/react/guides/dependent-queries

@author:
I can use the code suggestion to remove the first `useEffects` function. 
I will still need the second one as I am pulling the `last_parsed_date` column from `dagApi` and the `dag_code` from `dagSourceApi` and need to combine them before returning. Please let me know if this needs to change.

diff_hunk: @@ -0,0 +1,69 @@
+/*!
+ * 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 { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter";
+import { oneLight } from "react-syntax-highlighter/dist/esm/styles/prism";
+import python from "react-syntax-highlighter/dist/esm/languages/prism/python";
+import React, { useRef, useState } from "react";
+import { Box, Button } from "@chakra-ui/react";
+
+SyntaxHighlighter.registerLanguage("python", python);
+
+interface Props {
+  code: string;
+}
+export default function CodeBlock({ code }: Props) {
+  const dagCodeRef = useRef(null);
+  const [codeWrap, setCodeWrap] = useState(false);
+  const toggleCodeWrap = () => setCodeWrap(!codeWrap);
+
+  return (
+    <Box
+      ref={dagCodeRef}
path: airflow/www/static/js/dag/details/dagCode/CodeBlock.tsx
text: @user1:
Are we using this ref for anything?

@author:
This is a miss. I have removed it :)

diff_hunk: @@ -0,0 +1,43 @@
+/*!
+ * 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 React, { useRef } from "react";
+import { Box, Heading } from "@chakra-ui/react";
+import { useOffsetTop } from "src/utils";
+import Time from "src/components/Time";
+
+import { useDagCode } from "src/api";
+import CodeBlock from "./CodeBlock";
+
+const DagCode = () => {
+  const dagCodeRef = useRef<HTMLDivElement>(null);
+  const offsetTop = useOffsetTop(dagCodeRef);
+  const { lastParsedTime, codeSource = "" } = useDagCode();
path: airflow/www/static/js/dag/details/dagCode/index.tsx
text: @user1:
Let's also pull in `isLoading` and have a spinner.

@author:
done

diff_hunk: @@ -0,0 +1,98 @@
+/*!
+ * 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 axios, { AxiosResponse } from "axios";
+import { useQuery } from "react-query";
+import { useRef, useEffect, useState } from "react";
+
+import { getMetaValue } from "src/utils";
+import type { API } from "src/types";
+import useErrorToast from "../utils/useErrorToast";
+
+interface DagCodeData {
+  lastParsedTime?: string;
+  codeSource?: string;
+}
+
+export default function useDagCode() {
+  const errorToast = useErrorToast();
+  const [data, setData] = useState<DagCodeData>({
+    lastParsedTime: "N/A",
+    codeSource: "",
+  });
+
+  const dagQuery = useQuery(
+    ["dagQuery"],
+    () => {
+      const dagApiUrl = getMetaValue("dag_api");
+      return axios.get<AxiosResponse, API.DAG>(dagApiUrl);
+    },
+    {
+      onError: (error: Error) => {
path: airflow/www/static/js/api/useDagCode.ts
text: @user1:
We mainly use `errorToast` for edit actions. We don't really need it here. If we want to show an error to a user, we should do it in the dga code react component.

@author:
I am now returning the error back to dagCode and handling it there. Let me know if this pattern needs any change.