You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by bb...@apache.org on 2022/03/08 20:57:15 UTC

[airflow] 06/11: switch from drawer to details section

This is an automated email from the ASF dual-hosted git repository.

bbovenzi pushed a commit to branch mapped-task-drawer
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 1ea0b423df078e26cb5e508d194ea50952c52f77
Author: Brent Bovenzi <br...@gmail.com>
AuthorDate: Tue Mar 1 14:00:11 2022 -0500

    switch from drawer to details section
---
 airflow/www/forms.py                               |  4 +-
 airflow/www/static/js/tree/InstanceTooltip.jsx     | 83 ++------------------
 airflow/www/static/js/tree/SidePanel.jsx           | 90 ----------------------
 airflow/www/static/js/tree/StatusBox.jsx           |  6 +-
 airflow/www/static/js/tree/Tree.jsx                | 67 ++++++++--------
 airflow/www/static/js/tree/dagRuns/Bar.jsx         |  6 +-
 airflow/www/static/js/tree/dagRuns/Tooltip.jsx     | 51 +-----------
 airflow/www/static/js/tree/dagRuns/index.jsx       |  6 +-
 airflow/www/static/js/tree/details/Header.jsx      | 85 ++++++++++++++++++++
 airflow/www/static/js/tree/details/content/Dag.jsx | 31 ++++++++
 .../www/static/js/tree/details/content/DagRun.jsx  | 31 ++++++++
 .../js/tree/details/content/TaskInstance.jsx       | 31 ++++++++
 airflow/www/static/js/tree/details/index.jsx       | 48 ++++++++++++
 airflow/www/static/js/tree/renderTaskRows.jsx      | 22 +++---
 14 files changed, 292 insertions(+), 269 deletions(-)

diff --git a/airflow/www/forms.py b/airflow/www/forms.py
index 3a07cbe..3c3d0ac 100644
--- a/airflow/www/forms.py
+++ b/airflow/www/forms.py
@@ -104,13 +104,13 @@ class DateTimeWithNumRunsForm(FlaskForm):
     )
     num_runs = SelectField(
         "Number of runs",
-        default=25,
+        default=10,
         choices=(
             (5, "5"),
+            (10, "10"),
             (25, "25"),
             (50, "50"),
             (100, "100"),
-            (365, "365"),
         ),
     )
 
diff --git a/airflow/www/static/js/tree/InstanceTooltip.jsx b/airflow/www/static/js/tree/InstanceTooltip.jsx
index b0d0584..e5c1117 100644
--- a/airflow/www/static/js/tree/InstanceTooltip.jsx
+++ b/airflow/www/static/js/tree/InstanceTooltip.jsx
@@ -17,33 +17,16 @@
  * under the License.
  */
 
-/* global moment */
-
 import React from 'react';
 import { Box, Text } from '@chakra-ui/react';
 
-import { formatDateTime, getDuration, formatDuration } from '../datetime_utils';
 import { finalStatesMap } from '../utils';
-
-const STATES = [
-  ['success', 0],
-  ['failed', 0],
-  ['upstream_failed', 0],
-  ['up_for_retry', 0],
-  ['up_for_reschedule', 0],
-  ['running', 0],
-  ['deferred', 0],
-  ['sensing', 0],
-  ['queued', 0],
-  ['scheduled', 0],
-  ['skipped', 0],
-  ['no_status', 0],
-];
+import { formatDateTime } from '../datetime_utils';
 
 const InstanceTooltip = ({
   group,
   instance: {
-    duration, operator, startDate, endDate, state, taskId, runId, mappedStates,
+    startDate, state, runId, mappedStates,
   },
 }) => {
   const isGroup = !!group.children;
@@ -93,25 +76,20 @@ const InstanceTooltip = ({
     });
   }
 
-  const taskIdTitle = isGroup ? 'Task Group Id: ' : 'Task Id: ';
-
   return (
-    <Box fontSize="12px" py="4px">
+    <Box fontSize="12px" py="2px">
       {group.tooltip && (
         <Text>{group.tooltip}</Text>
       )}
       <Text>
-        <Text as="strong">Status:</Text>
+        <Text as="strong">
+          {isGroup ? 'Overall ' : ''}
+          Status:
+        </Text>
         {' '}
         {state || 'no status'}
       </Text>
-      {isGroup && (
-        <>
-          <br />
-          <Text as="strong">Group Summary</Text>
-          {groupSummary}
-        </>
-      )}
+      {isGroup && groupSummary}
       {group.isMapped && (
         <>
           <br />
@@ -124,56 +102,11 @@ const InstanceTooltip = ({
           {mapSummary}
         </>
       )}
-      <br />
-      {/* <Text>
-        {taskIdTitle}
-        {taskId}
-      </Text> */}
-      {/* <Text whiteSpace="nowrap">
-        Run Id:
-        {' '}
-        {runId}
-      </Text> */}
-      {/* {operator && (
-      <Text>
-        Operator:
-        {' '}
-        {operator}
-      </Text>
-      )} */}
-      <Text>
-        Duration:
-        {' '}
-        {formatDuration(duration || getDuration(startDate, endDate))}
-      </Text>
-      <br />
-      {/* <Text as="strong">UTC</Text>
-      <Text>
-        Started:
-        {' '}
-        {startDate && formatDateTime(moment.utc(startDate))}
-      </Text>
-      <Text>
-        Ended:
-        {' '}
-        {endDate && formatDateTime(moment.utc(endDate))}
-      </Text>
-      <br />
-      <Text as="strong">
-        Local:
-        {' '}
-        {moment().format('Z')}
-      </Text>
       <Text>
         Started:
         {' '}
         {startDate && formatDateTime(startDate)}
       </Text>
-      <Text>
-        Ended:
-        {' '}
-        {endDate && formatDateTime(endDate)}
-      </Text> */}
     </Box>
   );
 };
diff --git a/airflow/www/static/js/tree/SidePanel.jsx b/airflow/www/static/js/tree/SidePanel.jsx
deleted file mode 100644
index 88439a0..0000000
--- a/airflow/www/static/js/tree/SidePanel.jsx
+++ /dev/null
@@ -1,90 +0,0 @@
-/*!
- * 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.
- */
-
-/* global moment */
-
-import React from 'react';
-import {
-  chakra,
-  Flex,
-  Text,
-  useDisclosure,
-  IconButton,
-} from '@chakra-ui/react';
-import { MdKeyboardArrowLeft, MdKeyboardArrowRight } from 'react-icons/md';
-
-import { formatDateTime } from '../datetime_utils';
-
-const SidePanel = ({ instance: { runId, taskId, executionDate } }) => {
-  const { isOpen, onOpen, onClose } = useDisclosure();
-  if (!isOpen) {
-    return (
-      <IconButton
-        m={2}
-        icon={<MdKeyboardArrowLeft size={18} />}
-        aria-label="Open Details Panel"
-        onClick={onOpen}
-      />
-    );
-  }
-  let title = '';
-  if (runId && taskId) {
-    title = (
-      <>
-        <chakra.span>Task Instance: </chakra.span>
-        <chakra.b>{taskId}</chakra.b>
-        <chakra.span> at </chakra.span>
-        <chakra.b>{formatDateTime(moment.utc(executionDate))}</chakra.b>
-      </>
-    );
-  } else if (runId) {
-    title = (
-      <>
-        <chakra.span>Dag Run: </chakra.span>
-        <chakra.b>{runId}</chakra.b>
-        <chakra.span> at </chakra.span>
-        <chakra.b>{formatDateTime(moment.utc(executionDate))}</chakra.b>
-      </>
-    );
-  } else {
-    title = (
-      <chakra.span>Dag Details: </chakra.span>
-    );
-  }
-
-  return (
-    <Flex bg="gray.200" maxWidth={isOpen ? '50%' : 0} minWidth={isOpen ? 300 : 0} flexDirection="column" p={3}>
-      <IconButton
-        m={2}
-        icon={<MdKeyboardArrowRight size={18} />}
-        aria-label="Close Details Panel"
-        onClick={onClose}
-      />
-      <Text as="h4">
-        {title}
-      </Text>
-      <Text>
-        {/* eslint-disable-next-line max-len */}
-        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Nunc vel risus commodo viverra maecenas accumsan. Ut porttitor leo a diam sollicitudin tempor id eu. Molestie at elementum eu facilisis sed odio morbi quis commodo. Facilisis leo vel fringilla est ullamcorper eget nulla facilisi etiam. Est sit amet facilisis magna etiam tempor orci eu. Id semper risus in hendrerit gravida rutrum. Ac odio tempor orci dapibus  [...]
-      </Text>
-    </Flex>
-  );
-};
-
-export default SidePanel;
diff --git a/airflow/www/static/js/tree/StatusBox.jsx b/airflow/www/static/js/tree/StatusBox.jsx
index 0953a79..9609d9a 100644
--- a/airflow/www/static/js/tree/StatusBox.jsx
+++ b/airflow/www/static/js/tree/StatusBox.jsx
@@ -30,7 +30,7 @@ import {
 import InstanceTooltip from './InstanceTooltip';
 
 const StatusBox = ({
-  group, instance, containerRef, onSelectInstance, selectedInstance, ...rest
+  group, instance, containerRef, onSelect, selected, ...rest
 }) => {
   const { runId } = instance;
   const { colors } = useTheme();
@@ -38,7 +38,7 @@ const StatusBox = ({
 
   // Fetch the corresponding column element and set its background color when hovering
   const onMouseEnter = () => {
-    if (selectedInstance.runId !== runId) {
+    if (selected.runId !== runId) {
       [...containerRef.current.getElementsByClassName(`js-${runId}`)]
         .forEach((e) => { e.style.backgroundColor = hoverBlue; });
     }
@@ -51,7 +51,7 @@ const StatusBox = ({
   const onClick = (e) => {
     e.stopPropagation();
     onMouseLeave();
-    onSelectInstance(instance);
+    onSelect(instance);
   };
 
   return (
diff --git a/airflow/www/static/js/tree/Tree.jsx b/airflow/www/static/js/tree/Tree.jsx
index 9ae5833..1f60d03 100644
--- a/airflow/www/static/js/tree/Tree.jsx
+++ b/airflow/www/static/js/tree/Tree.jsx
@@ -34,14 +34,15 @@ import {
 import useTreeData from './useTreeData';
 import renderTaskRows from './renderTaskRows';
 import DagRuns from './dagRuns';
-import SidePanel from './SidePanel';
+import Details from './details';
 
 const Tree = () => {
   const containerRef = useRef();
   const scrollRef = useRef();
   const { data: { groups = {}, dagRuns = [] }, isRefreshOn, onToggleRefresh } = useTreeData();
+  const [selected, setSelected] = useState({});
+
   const dagRunIds = dagRuns.map((dr) => dr.runId);
-  const [selectedInstance, setSelectedInstance] = useState({});
 
   useEffect(() => {
     // Set initial scroll to far right if it is scrollable
@@ -51,46 +52,42 @@ const Tree = () => {
     }
   }, []);
 
-  const { runId, taskId } = selectedInstance;
-  const onSelectInstance = (newInstance) => (
+  const { runId, taskId } = selected;
+  const onSelect = (newInstance) => (
     (newInstance.runId === runId && newInstance.taskId === taskId)
-      ? setSelectedInstance({})
-      : setSelectedInstance(newInstance)
+      ? setSelected({})
+      : setSelected(newInstance)
   );
 
   return (
-    <Box position="relative" ref={containerRef}>
-      <FormControl display="flex" alignItems="center" justifyContent="flex-end" width="100%" mb={2}>
-        {isRefreshOn && <Spinner color="blue.500" speed="1s" mr="4px" />}
-        <FormLabel htmlFor="auto-refresh" mb={0} fontSize="12px" fontWeight="normal">
-          Auto-refresh
-        </FormLabel>
-        <Switch id="auto-refresh" onChange={onToggleRefresh} isChecked={isRefreshOn} size="lg" />
-      </FormControl>
+    <Flex pl="24px" position="relative" flexDirection="row" justifyContent="space-between" ref={containerRef}>
       <Text transform="rotate(-90deg)" position="absolute" left="-6px" top="130px">Runs</Text>
       <Text transform="rotate(-90deg)" position="absolute" left="-6px" top="190px">Tasks</Text>
-      <Box pl="24px" height="100%" onClick={() => setSelectedInstance({})}>
-        <Flex position="relative" flexDirection="row" justifyContent="space-between" overflow="hidden">
-          <Box mr="12px" pb="12px" overflowX="auto" ref={scrollRef} maxWidth="60vw">
-            <Table height={0}>
-              <Thead>
-                <DagRuns
-                  containerRef={containerRef}
-                  selectedInstance={selectedInstance}
-                  onSelectInstance={onSelectInstance}
-                />
-              </Thead>
-              <Tbody>
-                {renderTaskRows({
-                  task: groups, containerRef, onSelectInstance, selectedInstance, dagRunIds,
-                })}
-              </Tbody>
-            </Table>
-          </Box>
-          <SidePanel instance={selectedInstance} />
-        </Flex>
+      <Box mr="12px" pb="12px" overflowX="auto" ref={scrollRef} maxWidth="300px" minWidth="300px" position="relative">
+        <FormControl display="flex" alignItems="center" justifyContent="flex-end" width="100%" mb={2}>
+          {isRefreshOn && <Spinner color="blue.500" speed="1s" mr="4px" />}
+          <FormLabel htmlFor="auto-refresh" mb={0} fontSize="12px" fontWeight="normal">
+            Auto-refresh
+          </FormLabel>
+          <Switch id="auto-refresh" onChange={onToggleRefresh} isChecked={isRefreshOn} size="lg" />
+        </FormControl>
+        <Table height={0}>
+          <Thead>
+            <DagRuns
+              containerRef={containerRef}
+              selected={selected}
+              onSelect={onSelect}
+            />
+          </Thead>
+          <Tbody>
+            {renderTaskRows({
+              task: groups, containerRef, onSelect, selected, dagRunIds,
+            })}
+          </Tbody>
+        </Table>
       </Box>
-    </Box>
+      <Details selected={selected} onSelect={onSelect} />
+    </Flex>
   );
 };
 
diff --git a/airflow/www/static/js/tree/dagRuns/Bar.jsx b/airflow/www/static/js/tree/dagRuns/Bar.jsx
index c64abdd..1c8dd6b 100644
--- a/airflow/www/static/js/tree/dagRuns/Bar.jsx
+++ b/airflow/www/static/js/tree/dagRuns/Bar.jsx
@@ -35,7 +35,7 @@ import DagRunTooltip from './Tooltip';
 const BAR_HEIGHT = 100;
 
 const DagRunBar = ({
-  run, max, index, totalRuns, containerRef, selectedInstance, onSelectInstance,
+  run, max, index, totalRuns, containerRef, selected, onSelect,
 }) => {
   const { colors } = useTheme();
   const hoverBlue = `${colors.blue[100]}50`;
@@ -44,7 +44,7 @@ const DagRunBar = ({
     const table = containerRef.current.getElementsByTagName('tbody')[0];
     highlightHeight = table.offsetHeight + BAR_HEIGHT;
   }
-  const isSelected = run.runId === selectedInstance.runId;
+  const isSelected = run.runId === selected.runId;
   return (
     <Box position="relative">
       <Flex
@@ -59,7 +59,7 @@ const DagRunBar = ({
         zIndex={1}
         onClick={(e) => {
           e.stopPropagation();
-          onSelectInstance(run);
+          onSelect(run);
         }}
         position="relative"
         data-peer
diff --git a/airflow/www/static/js/tree/dagRuns/Tooltip.jsx b/airflow/www/static/js/tree/dagRuns/Tooltip.jsx
index 4e42cde..70b7f6a 100644
--- a/airflow/www/static/js/tree/dagRuns/Tooltip.jsx
+++ b/airflow/www/static/js/tree/dagRuns/Tooltip.jsx
@@ -17,8 +17,6 @@
  * under the License.
  */
 
-/* global moment */
-
 import React from 'react';
 import { Box, Text } from '@chakra-ui/react';
 import { MdPlayArrow } from 'react-icons/md';
@@ -27,20 +25,19 @@ import { formatDateTime, formatDuration } from '../../datetime_utils';
 
 const DagRunTooltip = ({
   dagRun: {
-    state, runId, duration, dataIntervalStart, dataIntervalEnd, startDate, endDate, runType,
+    state, duration, dataIntervalEnd, runType,
   },
 }) => (
-  <Box fontSize="12px" py="4px">
+  <Box fontSize="12px" py="2px">
     <Text>
       <Text as="strong">Status:</Text>
       {' '}
       {state || 'no status'}
     </Text>
-    <br />
     <Text whiteSpace="nowrap">
-      Run Id:
+      Run:
       {' '}
-      {runId}
+      {formatDateTime(dataIntervalEnd)}
     </Text>
     <Text>
       Run Type:
@@ -53,46 +50,6 @@ const DagRunTooltip = ({
       {' '}
       {formatDuration(duration)}
     </Text>
-    <br />
-    <Text as="strong">Data Interval:</Text>
-    <Text>
-      Start:
-      {' '}
-      {formatDateTime(dataIntervalStart)}
-    </Text>
-    <Text>
-      End:
-      {' '}
-      {formatDateTime(dataIntervalEnd)}
-    </Text>
-    <br />
-    <Text as="strong">UTC</Text>
-    <Text>
-      Started:
-      {' '}
-      {formatDateTime(moment.utc(startDate))}
-    </Text>
-    <Text>
-      Ended:
-      {' '}
-      {endDate && formatDateTime(moment.utc(endDate))}
-    </Text>
-    <br />
-    <Text as="strong">
-      Local:
-      {' '}
-      {moment().format('Z')}
-    </Text>
-    <Text>
-      Started:
-      {' '}
-      {formatDateTime(startDate)}
-    </Text>
-    <Text>
-      Ended:
-      {' '}
-      {endDate && formatDateTime(endDate)}
-    </Text>
   </Box>
 );
 
diff --git a/airflow/www/static/js/tree/dagRuns/index.jsx b/airflow/www/static/js/tree/dagRuns/index.jsx
index 7459634..7ff4e39 100644
--- a/airflow/www/static/js/tree/dagRuns/index.jsx
+++ b/airflow/www/static/js/tree/dagRuns/index.jsx
@@ -36,7 +36,7 @@ const DurationTick = ({ children, ...rest }) => (
   </Text>
 );
 
-const DagRuns = ({ containerRef, selectedInstance, onSelectInstance }) => {
+const DagRuns = ({ containerRef, selected, onSelect }) => {
   const { data: { dagRuns = [] } } = useTreeData();
   const durations = [];
   const runs = dagRuns.map((dagRun) => {
@@ -97,8 +97,8 @@ const DagRuns = ({ containerRef, selectedInstance, onSelectInstance }) => {
               index={i}
               totalRuns={runs.length}
               containerRef={containerRef}
-              selectedInstance={selectedInstance}
-              onSelectInstance={onSelectInstance}
+              selected={selected}
+              onSelect={onSelect}
             />
           ))}
         </Flex>
diff --git a/airflow/www/static/js/tree/details/Header.jsx b/airflow/www/static/js/tree/details/Header.jsx
new file mode 100644
index 0000000..b9d3086
--- /dev/null
+++ b/airflow/www/static/js/tree/details/Header.jsx
@@ -0,0 +1,85 @@
+/*!
+ * 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 from 'react';
+import {
+  Breadcrumb,
+  BreadcrumbItem,
+  BreadcrumbLink,
+  Box,
+  Heading,
+} from '@chakra-ui/react';
+import { MdPlayArrow } from 'react-icons/md';
+
+import useTreeData from '../useTreeData';
+import { formatDateTime } from '../../datetime_utils';
+import getMetaValue from '../../meta_value';
+
+const dagId = getMetaValue('dag_id');
+
+const LabelValue = ({ label, value }) => (
+  <Box position="relative">
+    <Heading as="h5" size="sm" color="gray.300" position="absolute" top="-12px">{label}</Heading>
+    <Heading as="h3" size="md">{value}</Heading>
+  </Box>
+);
+
+const Header = ({
+  selected: { taskId, runId },
+  onSelect,
+}) => {
+  const { data: { dagRuns = [] } } = useTreeData();
+  const dagRun = dagRuns.find((r) => r.runId === runId);
+  // console.log(dagRun);
+  let runLabel = dagRun ? formatDateTime(dagRun.dataIntervalEnd) : '';
+  if (dagRun && dagRun.runType === 'manual') {
+    runLabel = (
+      <>
+        <MdPlayArrow style={{ display: 'inline' }} />
+        {runLabel}
+      </>
+    );
+  }
+
+  return (
+    <Breadcrumb>
+      <BreadcrumbItem isCurrentPage={!runId && !taskId}>
+        <BreadcrumbLink onClick={() => onSelect({})}>
+          <LabelValue label="DAG" value={dagId} />
+        </BreadcrumbLink>
+      </BreadcrumbItem>
+      {runId && (
+        <BreadcrumbItem isCurrentPage={runId && !taskId}>
+          <BreadcrumbLink onClick={() => onSelect({ runId })}>
+            <LabelValue label="Run" value={runLabel} />
+          </BreadcrumbLink>
+        </BreadcrumbItem>
+      )}
+      {taskId && (
+        <BreadcrumbItem isCurrentPage>
+          <BreadcrumbLink>
+            <LabelValue label="Task" value={taskId} />
+          </BreadcrumbLink>
+        </BreadcrumbItem>
+      )}
+    </Breadcrumb>
+  );
+};
+
+export default Header;
diff --git a/airflow/www/static/js/tree/details/content/Dag.jsx b/airflow/www/static/js/tree/details/content/Dag.jsx
new file mode 100644
index 0000000..be460ba
--- /dev/null
+++ b/airflow/www/static/js/tree/details/content/Dag.jsx
@@ -0,0 +1,31 @@
+/*!
+ * 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 from 'react';
+import {
+  Text,
+} from '@chakra-ui/react';
+
+const Dag = () => (
+  <>
+    <Text>dag details</Text>
+  </>
+);
+
+export default Dag;
diff --git a/airflow/www/static/js/tree/details/content/DagRun.jsx b/airflow/www/static/js/tree/details/content/DagRun.jsx
new file mode 100644
index 0000000..58e4b92
--- /dev/null
+++ b/airflow/www/static/js/tree/details/content/DagRun.jsx
@@ -0,0 +1,31 @@
+/*!
+ * 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 from 'react';
+import {
+  Text,
+} from '@chakra-ui/react';
+
+const DagRun = () => (
+  <>
+    <Text>dag run details</Text>
+  </>
+);
+
+export default DagRun;
diff --git a/airflow/www/static/js/tree/details/content/TaskInstance.jsx b/airflow/www/static/js/tree/details/content/TaskInstance.jsx
new file mode 100644
index 0000000..4ab46e5
--- /dev/null
+++ b/airflow/www/static/js/tree/details/content/TaskInstance.jsx
@@ -0,0 +1,31 @@
+/*!
+ * 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 from 'react';
+import {
+  Text,
+} from '@chakra-ui/react';
+
+const TaskInstance = () => (
+  <>
+    <Text>task instance details</Text>
+  </>
+);
+
+export default TaskInstance;
diff --git a/airflow/www/static/js/tree/details/index.jsx b/airflow/www/static/js/tree/details/index.jsx
new file mode 100644
index 0000000..b8f960e
--- /dev/null
+++ b/airflow/www/static/js/tree/details/index.jsx
@@ -0,0 +1,48 @@
+/*!
+ * 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 from 'react';
+import {
+  Flex,
+  Box,
+  Divider,
+} from '@chakra-ui/react';
+
+import Header from './Header';
+import TaskInstanceContent from './content/TaskInstance';
+import DagRunContent from './content/DagRun';
+import DagContent from './content/Dag';
+
+const Details = ({
+  selected,
+  onSelect,
+}) => (
+  <Flex borderLeftWidth="1px" flexDirection="column" p={3} flexGrow={1}>
+    <Header selected={selected} onSelect={onSelect} />
+    <Divider my={2} />
+    <Box>
+      {/* TODO: get full instance data from the API */}
+      {!selected.runId && !selected.taskId && <DagContent />}
+      {selected.runId && !selected.taskId && <DagRunContent />}
+      {selected.taskId && <TaskInstanceContent instance={selected} />}
+    </Box>
+  </Flex>
+);
+
+export default Details;
diff --git a/airflow/www/static/js/tree/renderTaskRows.jsx b/airflow/www/static/js/tree/renderTaskRows.jsx
index f3e4550..36e0b94 100644
--- a/airflow/www/static/js/tree/renderTaskRows.jsx
+++ b/airflow/www/static/js/tree/renderTaskRows.jsx
@@ -40,7 +40,7 @@ import { getMetaValue } from '../utils';
 const dagId = getMetaValue('dag_id');
 
 const renderTaskRows = ({
-  task, containerRef, level = 0, isParentOpen, dagRunIds, onSelectInstance, selectedInstance,
+  task, containerRef, level = 0, isParentOpen, onSelect, selected, dagRunIds,
 }) => task.children.map((t) => (
   <Row
     key={t.id}
@@ -49,9 +49,9 @@ const renderTaskRows = ({
     containerRef={containerRef}
     prevTaskId={task.id}
     isParentOpen={isParentOpen}
+    onSelect={onSelect}
+    selected={selected}
     dagRunIds={dagRunIds}
-    onSelectInstance={onSelectInstance}
-    selectedInstance={selectedInstance}
   />
 ));
 
@@ -85,7 +85,7 @@ const TaskName = ({
 );
 
 const TaskInstances = ({
-  task, containerRef, dagRunIds, onSelectInstance, selectedInstance,
+  task, containerRef, dagRunIds, onSelect, selected,
 }) => (
   <Flex justifyContent="flex-end">
     {dagRunIds.map((runId) => {
@@ -99,8 +99,8 @@ const TaskInstances = ({
             instance={instance}
             containerRef={containerRef}
             group={task}
-            onSelectInstance={onSelectInstance}
-            selectedInstance={selectedInstance}
+            onSelect={onSelect}
+            selected={selected}
           />
         )
         : <Box key={key} width="16px" data-testid="blank-task" />;
@@ -115,14 +115,14 @@ const Row = (props) => {
     level,
     prevTaskId,
     isParentOpen = true,
+    onSelect,
+    selected,
     dagRunIds,
-    onSelectInstance,
-    selectedInstance,
   } = props;
   const { colors } = useTheme();
   const hoverBlue = `${colors.blue[100]}50`;
   const isGroup = !!task.children;
-  const isSelected = selectedInstance.taskId === task.id;
+  const isSelected = selected.taskId === task.id;
 
   const taskName = prevTaskId ? task.id.replace(`${prevTaskId}.`, '') : task.id;
 
@@ -185,8 +185,8 @@ const Row = (props) => {
               dagRunIds={dagRunIds}
               task={task}
               containerRef={containerRef}
-              onSelectInstance={onSelectInstance}
-              selectedInstance={selectedInstance}
+              onSelect={onSelect}
+              selected={selected}
             />
           </Collapse>
         </Td>