This is an automated email from the ASF dual-hosted git repository.
ryanahamilton pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/master by this push:
new 645e772 UI layout containers + navigation (#15007)
645e772 is described below
commit 645e77209c79725c2c004f5fccf47be1d9a5a030
Author: Ryan Hamilton <[email protected]>
AuthorDate: Thu Mar 25 14:52:48 2021 -0400
UI layout containers + navigation (#15007)
* Adds layout containers to render navigation
- Adds additional placeholder routes/views
* Remove "containers" directory, move files in context w/in views
* Add app container to Login view
* Convert static list to use data array
* Move/separate Icons and SVGs into separate components
* Remove unintention file addition
* Update test contex
* Make the env var more universal
---
airflow/ui/.env.example | 2 +-
airflow/ui/.neutrinorc.js | 2 +-
airflow/ui/README.md | 4 +-
airflow/ui/package.json | 1 +
airflow/ui/src/App.tsx | 20 +++
airflow/ui/src/auth/AuthProvider.tsx | 2 +-
.../ui/src/components/AppContainer/AppHeader.tsx | 129 +++++++++++++++
airflow/ui/src/components/AppContainer/AppNav.tsx | 111 +++++++++++++
.../ui/src/components/AppContainer/AppNavBtn.tsx | 87 ++++++++++
airflow/ui/src/components/AppContainer/index.tsx | 88 ++++++++++
.../utils.tsx => src/components/SectionNavBtn.tsx} | 37 +++--
airflow/ui/src/components/SectionWrapper.tsx | 100 +++++++++++
.../ui/src/components/icons/ApacheAirflowLogo.tsx | 33 ++++
airflow/ui/src/components/icons/PinwheelLogo.tsx | 40 +++++
airflow/ui/src/components/icons/PipelineIcon.tsx | 32 ++++
.../Access/AccessContainer.tsx} | 64 +++----
.../EventLogs.tsx => Access/Permissions.tsx} | 14 +-
airflow/ui/src/views/Access/Roles.tsx | 8 +-
airflow/ui/src/views/Access/Users.tsx | 8 +-
airflow/ui/src/views/Access/index.tsx | 8 +-
.../ui/src/views/Activity/ActivityContainer.tsx | 73 ++++++++
airflow/ui/src/views/Activity/EventLogs.tsx | 8 +-
.../src/views/Activity/{EventLogs.tsx => Jobs.tsx} | 14 +-
.../src/views/Activity/{EventLogs.tsx => Runs.tsx} | 14 +-
.../Activity/{EventLogs.tsx => SLAMisses.tsx} | 14 +-
.../Activity/{EventLogs.tsx => TaskInstances.tsx} | 14 +-
.../{EventLogs.tsx => TaskReschedules.tsx} | 14 +-
.../views/Activity/{EventLogs.tsx => XComs.tsx} | 14 +-
.../views/Config/ConfigContainer.tsx} | 46 ++++--
.../EventLogs.tsx => Config/Connections.tsx} | 14 +-
.../{Activity/EventLogs.tsx => Config/Pools.tsx} | 14 +-
.../EventLogs.tsx => Config/Variables.tsx} | 14 +-
airflow/ui/src/views/Config/index.tsx | 10 +-
airflow/ui/src/views/Docs.tsx | 183 ++++++++++++++++++++-
airflow/ui/src/views/Login.tsx | 112 +++++++------
airflow/ui/src/views/Pipelines.tsx | 6 +-
airflow/ui/test/Login.test.tsx | 18 +-
airflow/ui/test/utils.tsx | 2 +-
airflow/ui/yarn.lock | 5 +
39 files changed, 1174 insertions(+), 205 deletions(-)
diff --git a/airflow/ui/.env.example b/airflow/ui/.env.example
index 2e5267d..0912d86 100644
--- a/airflow/ui/.env.example
+++ b/airflow/ui/.env.example
@@ -1 +1 @@
-API_URL = 'http://127.0.0.1:28080/api/v1/'
+WEBSERVER_URL = 'http://127.0.0.1:28080'
diff --git a/airflow/ui/.neutrinorc.js b/airflow/ui/.neutrinorc.js
index f6a699e..58aef66 100644
--- a/airflow/ui/.neutrinorc.js
+++ b/airflow/ui/.neutrinorc.js
@@ -57,7 +57,7 @@ module.exports = {
}),
react({
env: [
- 'API_URL'
+ 'WEBSERVER_URL'
],
html: {
title: 'Apache Airflow',
diff --git a/airflow/ui/README.md b/airflow/ui/README.md
index 586614e..b3be371 100644
--- a/airflow/ui/README.md
+++ b/airflow/ui/README.md
@@ -37,10 +37,10 @@ Be sure to allow CORS headers and set up an auth backend on
your Airflow instanc
export AIRFLOW__API__AUTH_BACKEND=airflow.api.auth.backend.basic_auth
export AIRFLOW__API__ACCESS_CONTROL_ALLOW_HEADERS=*
export AIRFLOW__API__ACCESS_CONTROL_ALLOW_METHODS=*
-export AIRFLOW__API__ACCESS_CONTROL_ALLOW_ORIGIN=http://127.0.0.1:28080/
+export AIRFLOW__API__ACCESS_CONTROL_ALLOW_ORIGIN=http://127.0.0.1:28080
```
-Create your local environment and adjust the `API_URL` if needed.
+Create your local environment and adjust the `WEBSERVER_URL` if needed.
```bash
cp .env.example .env
diff --git a/airflow/ui/package.json b/airflow/ui/package.json
index d4f0cad..5462b16 100644
--- a/airflow/ui/package.json
+++ b/airflow/ui/package.json
@@ -14,6 +14,7 @@
"@emotion/styled": "^11.1.5",
"@neutrinojs/copy": "^9.5.0",
"axios": "^0.21.1",
+ "dayjs": "^1.10.4",
"dotenv": "^8.2.0",
"framer-motion": "^3.10.0",
"react": "^16",
diff --git a/airflow/ui/src/App.tsx b/airflow/ui/src/App.tsx
index b6d2f35..ae5d5f1 100644
--- a/airflow/ui/src/App.tsx
+++ b/airflow/ui/src/App.tsx
@@ -27,12 +27,22 @@ import Pipelines from 'views/Pipelines';
import Pipeline from 'views/Pipeline';
import EventLogs from 'views/Activity/EventLogs';
+import Runs from 'views/Activity/Runs';
+import Jobs from 'views/Activity/Jobs';
+import TaskInstances from 'views/Activity/TaskInstances';
+import TaskReschedules from 'views/Activity/TaskReschedules';
+import SLAMisses from 'views/Activity/SlaMisses';
+import XComs from 'views/Activity/XComs';
import Config from 'views/Config';
+import Variables from 'views/Config/Variables';
+import Connections from 'views/Config/Connections';
+import Pools from 'views/Config/Pools';
import Access from 'views/Access';
import Users from 'views/Access/Users';
import Roles from 'views/Access/Roles';
+import Permissions from 'views/Access/Permissions';
import Docs from 'views/Docs';
import NotFound from 'views/NotFound';
@@ -44,8 +54,17 @@ const App = () => (
<PrivateRoute exact path="/pipelines/:dagId" component={Pipeline} />
<PrivateRoute exact path="/activity/event-logs" component={EventLogs} />
+ <PrivateRoute exact path="/activity/runs" component={Runs} />
+ <PrivateRoute exact path="/activity/jobs" component={Jobs} />
+ <PrivateRoute exact path="/activity/task-instances"
component={TaskInstances} />
+ <PrivateRoute exact path="/activity/task-reschedules"
component={TaskReschedules} />
+ <PrivateRoute exact path="/activity/sla-misses" component={SLAMisses} />
+ <PrivateRoute exact path="/activity/xcoms" component={XComs} />
<PrivateRoute exact path="/config" component={Config} />
+ <PrivateRoute exact path="/config/variables" component={Variables} />
+ <PrivateRoute exact path="/config/connections" component={Connections} />
+ <PrivateRoute exact path="/config/pools" component={Pools} />
<PrivateRoute exact path="/access" component={Access} />
<PrivateRoute exact path="/access/users" component={Users} />
@@ -53,6 +72,7 @@ const App = () => (
<PrivateRoute exact path="/access/users/:username" component={Users} />
<PrivateRoute exact path="/access/users/:username/edit" component={Users}
/>
<PrivateRoute exact path="/access/roles" component={Roles} />
+ <PrivateRoute exact path="/access/permissions" component={Permissions} />
<Route exact path="/docs" component={Docs} />
diff --git a/airflow/ui/src/auth/AuthProvider.tsx
b/airflow/ui/src/auth/AuthProvider.tsx
index 8e52fae..9e52ecf 100644
--- a/airflow/ui/src/auth/AuthProvider.tsx
+++ b/airflow/ui/src/auth/AuthProvider.tsx
@@ -80,7 +80,7 @@ const AuthProvider = ({ children }: Props): ReactElement => {
setError(null);
try {
const authorization = `Basic ${btoa(`${username}:${password}`)}`;
- await axios.get(`${process.env.API_URL}config`, {
+ await axios.get(`${process.env.WEBSERVER_URL}/api/v1/config`, {
headers: {
Authorization: authorization,
},
diff --git a/airflow/ui/src/components/AppContainer/AppHeader.tsx
b/airflow/ui/src/components/AppContainer/AppHeader.tsx
new file mode 100644
index 0000000..7e88c8c
--- /dev/null
+++ b/airflow/ui/src/components/AppContainer/AppHeader.tsx
@@ -0,0 +1,129 @@
+/*!
+ * 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 { Link } from 'react-router-dom';
+import dayjs from 'dayjs';
+import {
+ Avatar,
+ Box,
+ Button,
+ Flex,
+ Icon,
+ Menu,
+ MenuButton,
+ MenuDivider,
+ MenuList,
+ MenuItem,
+ useColorMode,
+ useColorModeValue,
+ Tooltip,
+} from '@chakra-ui/react';
+import {
+ MdWbSunny,
+ MdBrightness2,
+ MdAccountCircle,
+ MdExitToApp,
+} from 'react-icons/md';
+
+import { useAuthContext } from 'auth/context';
+
+import ApacheAirflowLogo from 'components/icons/ApacheAirflowLogo';
+
+interface Props {
+ bodyBg: string;
+ overlayBg: string;
+ breadcrumb?: React.ReactNode;
+}
+
+const AppHeader: React.FC<Props> = ({ bodyBg, overlayBg, breadcrumb }) => {
+ const { toggleColorMode } = useColorMode();
+ const now = dayjs();
+ const headerHeight = '56px';
+ const { hasValidAuthToken, logout } = useAuthContext();
+
+ const handleOpenTZ = () => window.alert('This will open time zone select
modal!');
+
+ const handleOpenProfile = () => window.alert('This will take you to your
user profile view.');
+
+ return (
+ <Flex
+ as="header"
+ role="banner"
+ position="fixed"
+ width={`calc(100vw - ${headerHeight})`}
+ height={headerHeight}
+ zIndex={2}
+ align="center"
+ justifyContent="space-between"
+ py="2"
+ px="4"
+ backgroundColor={overlayBg}
+ borderBottomWidth="1px"
+ borderBottomColor={bodyBg}
+ >
+ {breadcrumb}
+ {!breadcrumb && (
+ <Link to="/" aria-label="Back to home">
+ <ApacheAirflowLogo />
+ </Link>
+ )}
+ {hasValidAuthToken && (
+ <Flex align="center">
+ <Tooltip label="Change time zone" hasArrow>
+ {/* TODO: open modal for time zone update */}
+ <Button variant="ghost" mr="4" onClick={handleOpenTZ}>
+ <Box
+ as="time"
+ dateTime={now.toString()}
+ fontSize="md"
+ >
+ {now.format('h:mmA Z')}
+ </Box>
+ </Button>
+ </Tooltip>
+ <Menu>
+ <MenuButton>
+ <Avatar name="Ryan Hamilton" size="sm" color="blue.900"
bg="blue.200" />
+ </MenuButton>
+ <MenuList placement="top-end">
+ <MenuItem onClick={handleOpenProfile}>
+ <Icon as={MdAccountCircle} mr="2" />
+ Your Profile
+ </MenuItem>
+ <MenuItem onClick={toggleColorMode}>
+ <Icon as={useColorModeValue(MdBrightness2, MdWbSunny)} mr="2"
/>
+ Set
+ {useColorModeValue(' Dark ', ' Light ')}
+ Mode
+ </MenuItem>
+ <MenuDivider />
+ <MenuItem onClick={logout}>
+ <Icon as={MdExitToApp} mr="2" />
+ Logout
+ </MenuItem>
+ </MenuList>
+ </Menu>
+ </Flex>
+ )}
+ </Flex>
+ );
+};
+
+export default AppHeader;
diff --git a/airflow/ui/src/components/AppContainer/AppNav.tsx
b/airflow/ui/src/components/AppContainer/AppNav.tsx
new file mode 100644
index 0000000..cc1f0de
--- /dev/null
+++ b/airflow/ui/src/components/AppContainer/AppNav.tsx
@@ -0,0 +1,111 @@
+/*!
+ * 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 { Link } from 'react-router-dom';
+import { Box } from '@chakra-ui/react';
+import {
+ FiActivity,
+ FiBookOpen,
+ FiSettings,
+ FiUsers,
+} from 'react-icons/fi';
+
+import { useAuthContext } from 'auth/context';
+
+import PinwheelLogo from 'components/icons/PinwheelLogo';
+import PipelineIcon from 'components/icons/PipelineIcon';
+
+import AppNavBtn from './AppNavBtn';
+
+interface Props {
+ bodyBg: string;
+ overlayBg: string;
+}
+
+const AppNav: React.FC<Props> = ({ bodyBg, overlayBg }) => {
+ const { hasValidAuthToken } = useAuthContext();
+
+ const navItems = [
+ {
+ label: 'Pipelines',
+ icon: PipelineIcon,
+ path: '/pipelines',
+ activePath: '/pipelines',
+ },
+ {
+ label: 'Activity',
+ icon: FiActivity,
+ path: '/activity/event-logs',
+ activePath: '/activity',
+ },
+ {
+ label: 'Config',
+ icon: FiSettings,
+ path: '/config',
+ activePath: '/config',
+ },
+ {
+ label: 'access',
+ icon: FiUsers,
+ path: '/access',
+ activePath: '/access',
+ },
+ {
+ label: 'Docs',
+ icon: FiBookOpen,
+ path: '/docs',
+ activePath: '/docs',
+ },
+ ];
+
+ return (
+ <Box
+ as="nav"
+ role="navigation"
+ width="56px"
+ backgroundColor={overlayBg}
+ borderRightWidth="1px"
+ borderRightColor={bodyBg}
+ display="flex"
+ flexDirection="column"
+ >
+ <Box
+ as={Link}
+ to="/"
+ aria-label="Back to home"
+ width="56px"
+ height="56px"
+ display="flex"
+ alignItems="center"
+ justifyContent="center"
+ _hover={{
+ transformOrigin: '28px 28px',
+ }}
+ >
+ <PinwheelLogo />
+ </Box>
+ {hasValidAuthToken && navItems.map((item) => (
+ <AppNavBtn key={item.label} navItem={item} />
+ ))}
+ </Box>
+ );
+};
+
+export default AppNav;
diff --git a/airflow/ui/src/components/AppContainer/AppNavBtn.tsx
b/airflow/ui/src/components/AppContainer/AppNavBtn.tsx
new file mode 100644
index 0000000..b6e560f
--- /dev/null
+++ b/airflow/ui/src/components/AppContainer/AppNavBtn.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 React from 'react';
+import { Link, useLocation } from 'react-router-dom';
+import {
+ Box,
+ Icon,
+ Tooltip,
+} from '@chakra-ui/react';
+
+import type { IconType } from 'react-icons/lib';
+
+interface Props {
+ navItem: {
+ label: string;
+ icon: IconType | typeof Icon;
+ path?: string;
+ activePath?: string;
+ href?: string;
+ };
+}
+
+const AppNavBtn: React.FC<Props> = ({ navItem }) => {
+ const location = useLocation();
+ const {
+ label, icon, path, href, activePath,
+ } = navItem;
+ const isHome = activePath === '/';
+ const isActive = activePath && ((isHome && location.pathname === '/') ||
(!isHome && location.pathname.includes(activePath)));
+
+ return (
+ <Tooltip
+ key={label}
+ label={label}
+ aria-label={label}
+ placement="right"
+ hasArrow
+ >
+ <Box
+ as={Link}
+ to={path || ''}
+ href={href}
+ target={href && '_blank'}
+ aria-label={label}
+ display="flex"
+ width="56px"
+ height="56px"
+ alignItems="center"
+ justifyContent="center"
+ borderRightWidth="3px"
+ borderLeftWidth="3px"
+ borderColor="transparent"
+ borderLeftColor={isActive ? 'blue.500' : 'transparent'}
+ color={isActive ? 'blue.500' : 'gray.500'}
+ _hover={{
+ color: 'blue.500',
+ }}
+ >
+ <Icon
+ as={icon}
+ width="1.4em"
+ height="1.4em"
+ color="currentcolor"
+ />
+ </Box>
+ </Tooltip>
+ );
+};
+
+export default AppNavBtn;
diff --git a/airflow/ui/src/components/AppContainer/index.tsx
b/airflow/ui/src/components/AppContainer/index.tsx
new file mode 100644
index 0000000..3841752
--- /dev/null
+++ b/airflow/ui/src/components/AppContainer/index.tsx
@@ -0,0 +1,88 @@
+/*!
+ * 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 {
+ Box,
+ Flex,
+ useColorModeValue,
+} from '@chakra-ui/react';
+
+// import { useVersion } from 'api';
+// import { defaultVersion } from 'api/defaults';
+import AppHeader from './AppHeader';
+import AppNav from './AppNav';
+
+interface Props {
+ breadcrumb?: React.ReactNode;
+}
+
+const AppContainer: React.FC<Props> = ({ children, breadcrumb }) => {
+ // const { data: { version, gitVersion } = defaultVersion } = useVersion();
+ const version = '2.0.0';
+ const gitVersion = '';
+ const bodyBg = useColorModeValue('white', 'gray.800');
+ const overlayBg = useColorModeValue('gray.100', 'gray.700');
+
+ return (
+ <Flex width="100vw" height="100vh" alignItems="stretch">
+ <AppNav bodyBg={bodyBg} overlayBg={overlayBg} />
+ <Box flex="1" alignItems="stretch">
+ <AppHeader bodyBg={bodyBg} overlayBg={overlayBg}
breadcrumb={breadcrumb} />
+ <Flex direction="column" height="100vh" pt="56px" overflowY="scroll">
+ <Box
+ as="main"
+ role="main"
+ flex="1"
+ px="4"
+ >
+ {children}
+ </Box>
+ <Box
+ as="footer"
+ role="contentinfo"
+ p="4"
+ color={useColorModeValue('gray.600', 'gray.300')}
+ bg={overlayBg}
+ >
+ Apache Airflow
+ {' '}
+ <a
+ href={`https://pypi.python.org/pypi/apache-airflow/${version}`}
+ target="_blank"
+ rel="noreferrer"
+ >
+ {`v${version}`}
+ </a>
+ {gitVersion && (
+ <>
+ <br />
+ Git Version:
+ {' '}
+ {gitVersion}
+ </>
+ )}
+ </Box>
+ </Flex>
+ </Box>
+ </Flex>
+ );
+};
+
+export default AppContainer;
diff --git a/airflow/ui/test/utils.tsx
b/airflow/ui/src/components/SectionNavBtn.tsx
similarity index 62%
copy from airflow/ui/test/utils.tsx
copy to airflow/ui/src/components/SectionNavBtn.tsx
index 935637c..b40695b 100644
--- a/airflow/ui/test/utils.tsx
+++ b/airflow/ui/src/components/SectionNavBtn.tsx
@@ -18,22 +18,31 @@
*/
import React from 'react';
-import { QueryClient, QueryClientProvider } from 'react-query';
+import { Link } from 'react-router-dom';
+import { Button } from '@chakra-ui/react';
-export const url: string = process.env.API_URL || '';
+interface Props {
+ item: {
+ label: string;
+ path: string;
+ };
+ currentLabel: string;
+}
-export const defaultHeaders = {
- 'access-control-allow-origin': url,
- 'access-control-allow-credentials': 'true',
- 'access-control-allow-headers': '*',
- 'access-control-allow-methods': '*',
-};
-
-export const QueryWrapper: React.FC<{}> = ({ children }) => {
- const queryClient = new QueryClient();
+const SectionNavBtn: React.FC<Props> = ({ item, currentLabel }) => {
+ const { label, path } = item;
return (
- <QueryClientProvider client={queryClient}>
- {children}
- </QueryClientProvider>
+ <Button
+ as={Link}
+ to={path}
+ variant={currentLabel === label ? 'solid' : 'ghost'}
+ colorScheme="blue"
+ size="sm"
+ mr="2"
+ >
+ {label}
+ </Button>
);
};
+
+export default SectionNavBtn;
diff --git a/airflow/ui/src/components/SectionWrapper.tsx
b/airflow/ui/src/components/SectionWrapper.tsx
new file mode 100644
index 0000000..9da33b8
--- /dev/null
+++ b/airflow/ui/src/components/SectionWrapper.tsx
@@ -0,0 +1,100 @@
+/*!
+ * 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 {
+ Box,
+ Heading,
+ useColorModeValue,
+} from '@chakra-ui/react';
+
+import SectionNavBtn from 'components/SectionNavBtn';
+
+import AppContainer from 'components/AppContainer';
+
+interface Props {
+ currentSection: string;
+ currentView: string;
+ navItems: {
+ label: string;
+ path: string;
+ }[]
+ toolBar?: React.ReactNode;
+}
+
+const SectionWrapper: React.FC<Props> = ({
+ children, currentSection, currentView, navItems, toolBar,
+}) => (
+ <AppContainer
+ breadcrumb={(
+ <Heading as="h1" size="md">
+ <Box
+ as="span"
+ color={useColorModeValue('gray.400', 'gray.500')}
+ >
+ {currentSection}
+ /
+ </Box>
+ {currentView}
+ </Heading>
+ )}
+ >
+ <Box
+ pt={2}
+ mx={-4}
+ px={4}
+ pb="2"
+ bg={useColorModeValue('gray.100', 'gray.700')}
+ >
+ <Box
+ display="flex"
+ alignItems="center"
+ justifyContent="space-between"
+ >
+ <Box as="nav">
+ {navItems.map((item) => (
+ <SectionNavBtn key={item.label} item={item}
currentLabel={currentView} />
+ ))}
+ </Box>
+ </Box>
+ </Box>
+ {toolBar && (
+ <Box
+ position="sticky"
+ top="0"
+ zIndex="1"
+ display="flex"
+ justifyContent="flex-start"
+ width="calc(100% + 2rem)%"
+ mr={-4}
+ ml={-4}
+ py={2}
+ px={4}
+ borderBottomWidth="2px"
+ borderBottomColor={useColorModeValue('gray.100', 'gray.700')}
+ backgroundColor={useColorModeValue('white', 'gray.800')}
+ >
+ {toolBar}
+ </Box>
+ )}
+ <Box py="4">{children}</Box>
+ </AppContainer>
+);
+
+export default SectionWrapper;
diff --git a/airflow/ui/src/components/icons/ApacheAirflowLogo.tsx
b/airflow/ui/src/components/icons/ApacheAirflowLogo.tsx
new file mode 100644
index 0000000..b1d71bd
--- /dev/null
+++ b/airflow/ui/src/components/icons/ApacheAirflowLogo.tsx
@@ -0,0 +1,33 @@
+/*!
+ * 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, { ReactElement } from 'react';
+
+const ApacheAirflowLogo = ({
+ width = '63px',
+ height = '24px',
+ ...otherProps
+}: React.SVGProps<SVGSVGElement>): ReactElement => (
+ <svg width={width} height={height} fill="currentColor"
xmlns="http://www.w3.org/2000/svg" {...otherProps}>
+ <path d="M2.753 5.466a.145.145 0 01-.106-.045.145.145 0
01-.046-.107c0-.04.003-.068.008-.083L4.488.33c.04-.11.12-.166.242-.166h.515c.122
0 .202.055.243.166l1.871 4.902.015.083c0 .04-.015.076-.045.107a.145.145 0
01-.106.045h-.387a.196.196 0 01-.128-.038.238.238 0 01-.06-.09L6.23
4.26H3.745L3.33 5.337a.214.214 0 01-.069.091.183.183 0 01-.12.038h-.387zM6.01
3.61L4.988.92 3.965 3.61h2.046zm2.257 3.295a.175.175 0 01-.13-.053.164.164 0
01-.045-.12V1.7c0-.05.016-.091.046-.122a.175.175 0 01. [...]
+ <path d="M2.647
5.42l-.041.042.04-.041zm-.038-.189l-.054-.02-.001.002.055.018zM4.488.33l.054.021-.054-.02zm1
0l-.055.02V.35l.055-.02zm1.871 4.902l.058-.01a.054.054 0
00-.003-.01l-.055.02zm.015.083h.059l-.001-.01-.058.01zm-.045.107l.04.04-.04-.04zm-.621.007l-.042.041a.042.042
0 00.004.004l.038-.045zm-.06-.09l.054-.02v-.002l-.055.021zM6.23
4.26l.055-.02a.058.058 0 00-.055-.038v.058zm-2.485 0v-.058a.058.058 0
00-.054.037l.054.021zM3.33 5.337l-.055-.02v.002l.055.018zm-.069.091l.038.045-.
[...]
+ </svg>
+);
+
+export default ApacheAirflowLogo;
diff --git a/airflow/ui/src/components/icons/PinwheelLogo.tsx
b/airflow/ui/src/components/icons/PinwheelLogo.tsx
new file mode 100644
index 0000000..422cb11
--- /dev/null
+++ b/airflow/ui/src/components/icons/PinwheelLogo.tsx
@@ -0,0 +1,40 @@
+/*!
+ * 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, { ReactElement } from 'react';
+
+const PinwheelLogo = ({
+ width = '40px',
+ height = '40px',
+ ...otherProps
+}: React.SVGProps<SVGSVGElement>): ReactElement => (
+ <svg width={width} height={height} viewBox="0 0 40 40" fill="none"
xmlns="http://www.w3.org/2000/svg" {...otherProps}>
+ <path d="M0.861099 35.3873L17.7729 18.0515C17.8788 17.9429 17.8991 17.775
17.8108 17.6516C16.782 16.2156 14.8848 15.9667 14.1814 15.002C12.0981 12.1441
11.5695 10.5266 10.6743 10.6269C10.6117 10.6339 10.556 10.6676 10.512
10.7126L4.4026 16.9752C0.887961 20.5779 0.383943 28.5103 0.291509
35.1524C0.287333 35.4525 0.651482 35.6021 0.861099 35.3873Z" fill="#017cee" />
+ <path d="M35.4734 34.9588L18.1375 18.047C18.0289 17.941 17.861 17.9207
17.7377 18.0091C16.3017 19.0378 16.0528 20.9351 15.088 21.6384C12.2302 23.7217
10.6126 24.2504 10.7129 25.1456C10.72 25.2082 10.7536 25.2639 10.7987
25.3077L17.0613 31.4172C20.664 34.9319 28.5964 35.4359 35.2385 35.5282C35.5386
35.5326 35.6882 35.1684 35.4734 34.9588Z" fill="#00ad46" />
+ <path fillRule="evenodd" clipRule="evenodd" d="M17.0612 31.4173C15.0932
29.4975 14.1801 25.6994 17.953 17.8671C11.8213 20.6074 9.67257 24.2094 10.7296
25.2407L17.0612 31.4173Z" fill="#04d659" />
+ <path d="M35.0445 0.346896L18.1327 17.6827C18.0268 17.7913 18.0065 17.9592
18.0948 18.0825C19.1236 19.5186 21.0209 19.7674 21.724 20.7322C23.8075 23.59
24.3362 25.2075 25.2313 25.1074C25.2938 25.1004 25.3496 25.0666 25.3936
25.0216L31.5029 18.759C35.0177 15.1562 35.5217 7.22392 35.6141 0.58175C35.6182
0.281597 35.2541 0.132024 35.0445 0.346896Z" fill="#00c7d4" />
+ <path fillRule="evenodd" clipRule="evenodd" d="M31.5031 18.759C29.5832
20.7269 25.7851 21.6401 17.9528 17.8671C20.693 23.9988 24.2951 26.1477 25.3263
25.0905L31.5031 18.759Z" fill="#11e1ee" />
+ <path d="M0.432658 0.775339L17.7685 17.6871C17.8771 17.793 18.045 17.8134
18.1683 17.725C19.6043 16.6963 19.8532 14.799 20.8179 14.0957C23.6759 12.0123
25.2934 11.4837 25.193 10.5885C25.186 10.526 25.1523 10.4702 25.1074
10.4263L18.8447 4.31685C15.242 0.802203 7.30967 0.298184 0.667512
0.205751C0.367359 0.201573 0.217786 0.565722 0.432658 0.775339Z" fill="#e43921"
/>
+ <path fillRule="evenodd" clipRule="evenodd" d="M18.8446 4.31675C20.8125
6.23662 21.7257 10.0346 17.9528 17.8669C24.0844 15.1267 26.2333 11.5246 25.1761
10.4934L18.8446 4.31675Z" fill="#ff7557" />
+ <path fillRule="evenodd" clipRule="evenodd" d="M4.4028 16.9752C6.32267
15.0072 10.1207 14.0942 17.953 17.867C15.2128 11.7354 11.6107 9.58661 10.5795
10.6437L4.4028 16.9752Z" fill="#0cb6ff" />
+ <path d="M17.9649 18.6209C18.3825 18.6157 18.7169 18.273 18.7117
17.8553C18.7065 17.4377 18.3638 17.1034 17.9462 17.1085C17.5285 17.1137 17.1942
17.4564 17.1994 17.8741C17.2045 18.2917 17.5473 18.626 17.9649 18.6209Z"
fill="#4a4848" />
+ </svg>
+);
+
+export default PinwheelLogo;
diff --git a/airflow/ui/src/components/icons/PipelineIcon.tsx
b/airflow/ui/src/components/icons/PipelineIcon.tsx
new file mode 100644
index 0000000..6565a17
--- /dev/null
+++ b/airflow/ui/src/components/icons/PipelineIcon.tsx
@@ -0,0 +1,32 @@
+/*!
+ * 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 { createIcon } from '@chakra-ui/react';
+
+const PipelineIcon = createIcon({
+ displayName: 'PipelineIcon',
+ viewBox: '0 0 24 24',
+ path: [
+ <path key="a" d="M3.4 3C3.24295 3 3 3.15775 3 3.5V6.5C3 6.84225 3.24295 7
3.4 7H14.6C14.7571 7 15 6.84225 15 6.5V3.5C15 3.15776 14.7571 3 14.6 3H3.4ZM1
3.5C1 2.18539 2.01065 1 3.4 1H14.6C15.9893 1 17 2.18539 17 3.5V6.5C17 7.81461
15.9893 9 14.6 9H3.4C2.01065 9 1 7.81461 1 6.5V3.5ZM9.4 17C9.24295 17 9 17.1578
9 17.5V20.5C9 20.8422 9.24295 21 9.4 21H20.6C20.7571 21 21 20.8422 21
20.5V17.5C21 17.1578 20.7571 17 20.6 17H9.4ZM7 17.5C7 16.1854 8.01065 15 9.4
15H20.6C21.9893 15 23 16.1854 2 [...]
+ <path key="b" d="M7.99999 11.019L7.99999 8H8.99999L8.99999 11.019C8.99999
11.2952 9.22385 11.519 9.49999 11.519L14 11.519C14.8284 11.519 15.5 12.1906
15.5 13.019L15.5 15.5H14.5L14.5 13.019C14.5 12.7429 14.2761 12.519 14
12.519L9.49999 12.519C8.67157 12.519 7.99999 11.8475 7.99999 11.019Z"
fill="currentColor" />,
+ ],
+});
+
+export default PipelineIcon;
diff --git a/airflow/ui/src/components/AppHeader.tsx
b/airflow/ui/src/views/Access/AccessContainer.tsx
similarity index 55%
rename from airflow/ui/src/components/AppHeader.tsx
rename to airflow/ui/src/views/Access/AccessContainer.tsx
index b3193bb..a2a1687 100644
--- a/airflow/ui/src/components/AppHeader.tsx
+++ b/airflow/ui/src/views/Access/AccessContainer.tsx
@@ -18,40 +18,44 @@
*/
import React from 'react';
-import {
- Button,
- Flex,
- Box,
- Icon,
-} from '@chakra-ui/react';
-import { MdExitToApp } from 'react-icons/md';
-import { useAuthContext } from 'auth/context';
-const AppHeader: React.FC = ({ children }) => {
- const { logout } = useAuthContext();
+import SectionWrapper from 'components/SectionWrapper';
+
+interface Props {
+ current: string;
+ toolBar?: React.ReactNode;
+}
+
+const AccessContainer: React.FC<Props> = ({ children, current, toolBar }) => {
+ const navItems = [
+ {
+ label: 'Overview',
+ path: '/access',
+ },
+ {
+ label: 'Users',
+ path: '/access/users',
+ },
+ {
+ label: 'Roles',
+ path: '/access/roles',
+ },
+ {
+ label: 'Permissions',
+ path: '/access/permissions',
+ },
+ ];
return (
- <Flex width="100vw" height="100vh">
- <Flex
- as="header"
- position="fixed"
- width="100vw"
- zIndex={2}
- align="center"
- justifyContent="space-between"
- py="2"
- px="4"
- borderBottomWidth="1px"
- >
- <Box />
- <Button onClick={logout}>
- <Icon as={MdExitToApp} mr="2" />
- Logout
- </Button>
- </Flex>
+ <SectionWrapper
+ currentSection="Access"
+ currentView={current}
+ navItems={navItems}
+ toolBar={toolBar}
+ >
{children}
- </Flex>
+ </SectionWrapper>
);
};
-export default AppHeader;
+export default AccessContainer;
diff --git a/airflow/ui/src/views/Activity/EventLogs.tsx
b/airflow/ui/src/views/Access/Permissions.tsx
similarity index 76%
copy from airflow/ui/src/views/Activity/EventLogs.tsx
copy to airflow/ui/src/views/Access/Permissions.tsx
index 82b50e0..b3356b7 100644
--- a/airflow/ui/src/views/Activity/EventLogs.tsx
+++ b/airflow/ui/src/views/Access/Permissions.tsx
@@ -18,12 +18,14 @@
*/
import React from 'react';
-import { Center, Heading } from '@chakra-ui/react';
+import { Heading } from '@chakra-ui/react';
-const EventLogs: React.FC = () => (
- <Center height="100vh">
- <Heading>Event Logs</Heading>
- </Center>
+import AccessContainer from './AccessContainer';
+
+const Permissions: React.FC = () => (
+ <AccessContainer current="Permissions">
+ <Heading>Permissions</Heading>
+ </AccessContainer>
);
-export default EventLogs;
+export default Permissions;
diff --git a/airflow/ui/src/views/Access/Roles.tsx
b/airflow/ui/src/views/Access/Roles.tsx
index 023f3fc..7e1aad9 100644
--- a/airflow/ui/src/views/Access/Roles.tsx
+++ b/airflow/ui/src/views/Access/Roles.tsx
@@ -18,12 +18,14 @@
*/
import React from 'react';
-import { Center, Heading } from '@chakra-ui/react';
+import { Heading } from '@chakra-ui/react';
+
+import AccessContainer from './AccessContainer';
const Roles: React.FC = () => (
- <Center height="100vh">
+ <AccessContainer current="Roles">
<Heading>Roles</Heading>
- </Center>
+ </AccessContainer>
);
export default Roles;
diff --git a/airflow/ui/src/views/Access/Users.tsx
b/airflow/ui/src/views/Access/Users.tsx
index 60aed77..05ec0d8 100644
--- a/airflow/ui/src/views/Access/Users.tsx
+++ b/airflow/ui/src/views/Access/Users.tsx
@@ -18,12 +18,14 @@
*/
import React from 'react';
-import { Center, Heading } from '@chakra-ui/react';
+import { Heading } from '@chakra-ui/react';
+
+import AccessContainer from './AccessContainer';
const Users: React.FC = () => (
- <Center height="100vh">
+ <AccessContainer current="Users">
<Heading>Users</Heading>
- </Center>
+ </AccessContainer>
);
export default Users;
diff --git a/airflow/ui/src/views/Access/index.tsx
b/airflow/ui/src/views/Access/index.tsx
index b5593ba..f902a7f 100644
--- a/airflow/ui/src/views/Access/index.tsx
+++ b/airflow/ui/src/views/Access/index.tsx
@@ -18,12 +18,14 @@
*/
import React from 'react';
-import { Center, Heading } from '@chakra-ui/react';
+import { Heading } from '@chakra-ui/react';
+
+import AccessContainer from './AccessContainer';
const Access: React.FC = () => (
- <Center height="100vh">
+ <AccessContainer current="Overview">
<Heading>Access</Heading>
- </Center>
+ </AccessContainer>
);
export default Access;
diff --git a/airflow/ui/src/views/Activity/ActivityContainer.tsx
b/airflow/ui/src/views/Activity/ActivityContainer.tsx
new file mode 100644
index 0000000..8e137d7
--- /dev/null
+++ b/airflow/ui/src/views/Activity/ActivityContainer.tsx
@@ -0,0 +1,73 @@
+/*!
+ * 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 SectionWrapper from 'components/SectionWrapper';
+
+interface Props {
+ current: string;
+ toolBar?: React.ReactNode;
+}
+
+const ActivityContainer: React.FC<Props> = ({ children, current, toolBar }) =>
{
+ const navItems = [
+ {
+ label: 'Event Logs',
+ path: '/activity/event-logs',
+ },
+ {
+ label: 'Runs',
+ path: '/activity/runs',
+ },
+ {
+ label: 'Jobs',
+ path: '/activity/jobs',
+ },
+ {
+ label: 'Task Instances',
+ path: '/activity/task-instances',
+ },
+ {
+ label: 'Task Reschedules',
+ path: '/activity/task-reschedules',
+ },
+ {
+ label: 'SLA Misses',
+ path: '/activity/sla-misses',
+ },
+ {
+ label: 'XComs',
+ path: '/activity/xcoms',
+ },
+ ];
+
+ return (
+ <SectionWrapper
+ currentSection="Activity"
+ currentView={current}
+ navItems={navItems}
+ toolBar={toolBar}
+ >
+ {children}
+ </SectionWrapper>
+ );
+};
+
+export default ActivityContainer;
diff --git a/airflow/ui/src/views/Activity/EventLogs.tsx
b/airflow/ui/src/views/Activity/EventLogs.tsx
index 82b50e0..b663d50 100644
--- a/airflow/ui/src/views/Activity/EventLogs.tsx
+++ b/airflow/ui/src/views/Activity/EventLogs.tsx
@@ -18,12 +18,14 @@
*/
import React from 'react';
-import { Center, Heading } from '@chakra-ui/react';
+import { Heading } from '@chakra-ui/react';
+
+import ActivityContainer from './ActivityContainer';
const EventLogs: React.FC = () => (
- <Center height="100vh">
+ <ActivityContainer current="Event Logs">
<Heading>Event Logs</Heading>
- </Center>
+ </ActivityContainer>
);
export default EventLogs;
diff --git a/airflow/ui/src/views/Activity/EventLogs.tsx
b/airflow/ui/src/views/Activity/Jobs.tsx
similarity index 77%
copy from airflow/ui/src/views/Activity/EventLogs.tsx
copy to airflow/ui/src/views/Activity/Jobs.tsx
index 82b50e0..c5b3dd6 100644
--- a/airflow/ui/src/views/Activity/EventLogs.tsx
+++ b/airflow/ui/src/views/Activity/Jobs.tsx
@@ -18,12 +18,14 @@
*/
import React from 'react';
-import { Center, Heading } from '@chakra-ui/react';
+import { Heading } from '@chakra-ui/react';
-const EventLogs: React.FC = () => (
- <Center height="100vh">
- <Heading>Event Logs</Heading>
- </Center>
+import ActivityContainer from './ActivityContainer';
+
+const Jobs: React.FC = () => (
+ <ActivityContainer current="Jobs">
+ <Heading>Jobs</Heading>
+ </ActivityContainer>
);
-export default EventLogs;
+export default Jobs;
diff --git a/airflow/ui/src/views/Activity/EventLogs.tsx
b/airflow/ui/src/views/Activity/Runs.tsx
similarity index 77%
copy from airflow/ui/src/views/Activity/EventLogs.tsx
copy to airflow/ui/src/views/Activity/Runs.tsx
index 82b50e0..dc808e1 100644
--- a/airflow/ui/src/views/Activity/EventLogs.tsx
+++ b/airflow/ui/src/views/Activity/Runs.tsx
@@ -18,12 +18,14 @@
*/
import React from 'react';
-import { Center, Heading } from '@chakra-ui/react';
+import { Heading } from '@chakra-ui/react';
-const EventLogs: React.FC = () => (
- <Center height="100vh">
- <Heading>Event Logs</Heading>
- </Center>
+import ActivityContainer from './ActivityContainer';
+
+const Runs: React.FC = () => (
+ <ActivityContainer current="Runs">
+ <Heading>Runs</Heading>
+ </ActivityContainer>
);
-export default EventLogs;
+export default Runs;
diff --git a/airflow/ui/src/views/Activity/EventLogs.tsx
b/airflow/ui/src/views/Activity/SLAMisses.tsx
similarity index 76%
copy from airflow/ui/src/views/Activity/EventLogs.tsx
copy to airflow/ui/src/views/Activity/SLAMisses.tsx
index 82b50e0..4934297 100644
--- a/airflow/ui/src/views/Activity/EventLogs.tsx
+++ b/airflow/ui/src/views/Activity/SLAMisses.tsx
@@ -18,12 +18,14 @@
*/
import React from 'react';
-import { Center, Heading } from '@chakra-ui/react';
+import { Heading } from '@chakra-ui/react';
-const EventLogs: React.FC = () => (
- <Center height="100vh">
- <Heading>Event Logs</Heading>
- </Center>
+import ActivityContainer from './ActivityContainer';
+
+const SLAMisses: React.FC = () => (
+ <ActivityContainer current="SLA Misses">
+ <Heading>SLA Misses</Heading>
+ </ActivityContainer>
);
-export default EventLogs;
+export default SLAMisses;
diff --git a/airflow/ui/src/views/Activity/EventLogs.tsx
b/airflow/ui/src/views/Activity/TaskInstances.tsx
similarity index 75%
copy from airflow/ui/src/views/Activity/EventLogs.tsx
copy to airflow/ui/src/views/Activity/TaskInstances.tsx
index 82b50e0..592aeca 100644
--- a/airflow/ui/src/views/Activity/EventLogs.tsx
+++ b/airflow/ui/src/views/Activity/TaskInstances.tsx
@@ -18,12 +18,14 @@
*/
import React from 'react';
-import { Center, Heading } from '@chakra-ui/react';
+import { Heading } from '@chakra-ui/react';
-const EventLogs: React.FC = () => (
- <Center height="100vh">
- <Heading>Event Logs</Heading>
- </Center>
+import ActivityContainer from './ActivityContainer';
+
+const TaskInstances: React.FC = () => (
+ <ActivityContainer current="Task Instances">
+ <Heading>Task Instances</Heading>
+ </ActivityContainer>
);
-export default EventLogs;
+export default TaskInstances;
diff --git a/airflow/ui/src/views/Activity/EventLogs.tsx
b/airflow/ui/src/views/Activity/TaskReschedules.tsx
similarity index 74%
copy from airflow/ui/src/views/Activity/EventLogs.tsx
copy to airflow/ui/src/views/Activity/TaskReschedules.tsx
index 82b50e0..2ba0848 100644
--- a/airflow/ui/src/views/Activity/EventLogs.tsx
+++ b/airflow/ui/src/views/Activity/TaskReschedules.tsx
@@ -18,12 +18,14 @@
*/
import React from 'react';
-import { Center, Heading } from '@chakra-ui/react';
+import { Heading } from '@chakra-ui/react';
-const EventLogs: React.FC = () => (
- <Center height="100vh">
- <Heading>Event Logs</Heading>
- </Center>
+import ActivityContainer from './ActivityContainer';
+
+const TaskReschedules: React.FC = () => (
+ <ActivityContainer current="Task Reschedules">
+ <Heading>Task Reschedules</Heading>
+ </ActivityContainer>
);
-export default EventLogs;
+export default TaskReschedules;
diff --git a/airflow/ui/src/views/Activity/EventLogs.tsx
b/airflow/ui/src/views/Activity/XComs.tsx
similarity index 77%
copy from airflow/ui/src/views/Activity/EventLogs.tsx
copy to airflow/ui/src/views/Activity/XComs.tsx
index 82b50e0..3f09eeb 100644
--- a/airflow/ui/src/views/Activity/EventLogs.tsx
+++ b/airflow/ui/src/views/Activity/XComs.tsx
@@ -18,12 +18,14 @@
*/
import React from 'react';
-import { Center, Heading } from '@chakra-ui/react';
+import { Heading } from '@chakra-ui/react';
-const EventLogs: React.FC = () => (
- <Center height="100vh">
- <Heading>Event Logs</Heading>
- </Center>
+import ActivityContainer from './ActivityContainer';
+
+const XComs: React.FC = () => (
+ <ActivityContainer current="XComs">
+ <Heading>XComs</Heading>
+ </ActivityContainer>
);
-export default EventLogs;
+export default XComs;
diff --git a/airflow/ui/test/utils.tsx
b/airflow/ui/src/views/Config/ConfigContainer.tsx
similarity index 55%
copy from airflow/ui/test/utils.tsx
copy to airflow/ui/src/views/Config/ConfigContainer.tsx
index 935637c..8226ce1 100644
--- a/airflow/ui/test/utils.tsx
+++ b/airflow/ui/src/views/Config/ConfigContainer.tsx
@@ -18,22 +18,44 @@
*/
import React from 'react';
-import { QueryClient, QueryClientProvider } from 'react-query';
-export const url: string = process.env.API_URL || '';
+import SectionWrapper from 'components/SectionWrapper';
-export const defaultHeaders = {
- 'access-control-allow-origin': url,
- 'access-control-allow-credentials': 'true',
- 'access-control-allow-headers': '*',
- 'access-control-allow-methods': '*',
-};
+interface Props {
+ current: string;
+ toolBar?: React.ReactNode;
+}
+
+const ConfigContainer: React.FC<Props> = ({ children, current, toolBar }) => {
+ const navItems = [
+ {
+ label: 'airflow.cfg',
+ path: '/config',
+ },
+ {
+ label: 'Variables',
+ path: '/config/variables',
+ },
+ {
+ label: 'Connections',
+ path: '/config/connections',
+ },
+ {
+ label: 'Pools',
+ path: '/config/pools',
+ },
+ ];
-export const QueryWrapper: React.FC<{}> = ({ children }) => {
- const queryClient = new QueryClient();
return (
- <QueryClientProvider client={queryClient}>
+ <SectionWrapper
+ currentSection="Config"
+ currentView={current}
+ navItems={navItems}
+ toolBar={toolBar}
+ >
{children}
- </QueryClientProvider>
+ </SectionWrapper>
);
};
+
+export default ConfigContainer;
diff --git a/airflow/ui/src/views/Activity/EventLogs.tsx
b/airflow/ui/src/views/Config/Connections.tsx
similarity index 76%
copy from airflow/ui/src/views/Activity/EventLogs.tsx
copy to airflow/ui/src/views/Config/Connections.tsx
index 82b50e0..4cd350f 100644
--- a/airflow/ui/src/views/Activity/EventLogs.tsx
+++ b/airflow/ui/src/views/Config/Connections.tsx
@@ -18,12 +18,14 @@
*/
import React from 'react';
-import { Center, Heading } from '@chakra-ui/react';
+import { Heading } from '@chakra-ui/react';
-const EventLogs: React.FC = () => (
- <Center height="100vh">
- <Heading>Event Logs</Heading>
- </Center>
+import ConfigContainer from './ConfigContainer';
+
+const Connections: React.FC = () => (
+ <ConfigContainer current="Connections">
+ <Heading>Connections</Heading>
+ </ConfigContainer>
);
-export default EventLogs;
+export default Connections;
diff --git a/airflow/ui/src/views/Activity/EventLogs.tsx
b/airflow/ui/src/views/Config/Pools.tsx
similarity index 78%
copy from airflow/ui/src/views/Activity/EventLogs.tsx
copy to airflow/ui/src/views/Config/Pools.tsx
index 82b50e0..c386399 100644
--- a/airflow/ui/src/views/Activity/EventLogs.tsx
+++ b/airflow/ui/src/views/Config/Pools.tsx
@@ -18,12 +18,14 @@
*/
import React from 'react';
-import { Center, Heading } from '@chakra-ui/react';
+import { Heading } from '@chakra-ui/react';
-const EventLogs: React.FC = () => (
- <Center height="100vh">
- <Heading>Event Logs</Heading>
- </Center>
+import ConfigContainer from './ConfigContainer';
+
+const Pools: React.FC = () => (
+ <ConfigContainer current="Pools">
+ <Heading>Pools</Heading>
+ </ConfigContainer>
);
-export default EventLogs;
+export default Pools;
diff --git a/airflow/ui/src/views/Activity/EventLogs.tsx
b/airflow/ui/src/views/Config/Variables.tsx
similarity index 77%
copy from airflow/ui/src/views/Activity/EventLogs.tsx
copy to airflow/ui/src/views/Config/Variables.tsx
index 82b50e0..168f887 100644
--- a/airflow/ui/src/views/Activity/EventLogs.tsx
+++ b/airflow/ui/src/views/Config/Variables.tsx
@@ -18,12 +18,14 @@
*/
import React from 'react';
-import { Center, Heading } from '@chakra-ui/react';
+import { Heading } from '@chakra-ui/react';
-const EventLogs: React.FC = () => (
- <Center height="100vh">
- <Heading>Event Logs</Heading>
- </Center>
+import ConfigContainer from './ConfigContainer';
+
+const Variables: React.FC = () => (
+ <ConfigContainer current="Variables">
+ <Heading>Variables</Heading>
+ </ConfigContainer>
);
-export default EventLogs;
+export default Variables;
diff --git a/airflow/ui/src/views/Config/index.tsx
b/airflow/ui/src/views/Config/index.tsx
index bfb5ad8..bdd6dcb 100644
--- a/airflow/ui/src/views/Config/index.tsx
+++ b/airflow/ui/src/views/Config/index.tsx
@@ -18,12 +18,14 @@
*/
import React from 'react';
-import { Center, Heading } from '@chakra-ui/react';
+import { Heading } from '@chakra-ui/react';
+
+import ConfigContainer from './ConfigContainer';
const Config: React.FC = () => (
- <Center height="100vh">
- <Heading>Config</Heading>
- </Center>
+ <ConfigContainer current="airflow.cfg">
+ <Heading>airflow.cfg</Heading>
+ </ConfigContainer>
);
export default Config;
diff --git a/airflow/ui/src/views/Docs.tsx b/airflow/ui/src/views/Docs.tsx
index 9aaae2c..754b803 100644
--- a/airflow/ui/src/views/Docs.tsx
+++ b/airflow/ui/src/views/Docs.tsx
@@ -18,12 +18,183 @@
*/
import React from 'react';
-import { Center, Heading } from '@chakra-ui/react';
+import {
+ Box,
+ Button,
+ Flex,
+ Heading,
+ Icon,
+ Link,
+ List,
+ ListItem,
+ Text,
+ useColorModeValue,
+} from '@chakra-ui/react';
+import { FaGithub } from 'react-icons/fa';
+import { FiExternalLink, FiGlobe } from 'react-icons/fi';
-const Docs: React.FC = () => (
- <Center height="100vh">
- <Heading>Docs</Heading>
- </Center>
-);
+import AppContainer from 'components/AppContainer';
+
+const Docs: React.FC = () => {
+ // TEMP: This static list needs to be provided w/ a (currently non-existent)
API endpoint
+ const providers = [
+ { path: 'amazon', name: 'Amazon' },
+ { path: 'apache-beam', name: 'Apache Beam' },
+ { path: 'apache-cassandra', name: 'Apache Cassandra' },
+ { path: 'apache-druid', name: 'Apache Druid' },
+ { path: 'apache-hdfs', name: 'Apache HDFS' },
+ { path: 'apache-hive', name: 'Apache Hive' },
+ { path: 'apache-kylin', name: 'Apache Kylin' },
+ { path: 'apache-livy', name: 'Apache Livy' },
+ { path: 'apache-pig', name: 'Apache Pig' },
+ { path: 'apache-pinot', name: 'Apache Pinot' },
+ { path: 'apache-spark', name: 'Apache Spark' },
+ { path: 'apache-sqoop', name: 'Apache Sqoop' },
+ { path: 'celery', name: 'Celery' },
+ { path: 'cloudant', name: 'IBM Cloudant' },
+ { path: 'cncf-kubernetes', name: 'Kubernetes' },
+ { path: 'databricks', name: 'Databricks' },
+ { path: 'datadog', name: 'Datadog' },
+ { path: 'dingding', name: 'Dingding' },
+ { path: 'discord', name: 'Discord' },
+ { path: 'docker', name: 'Docker' },
+ { path: 'elasticsearch', name: 'Elasticsearch' },
+ { path: 'exasol', name: 'Exasol' },
+ { path: 'facebook', name: 'Facebook' },
+ { path: 'ftp', name: 'File Transfer Protocol (FTP)' },
+ { path: 'google', name: 'Google' },
+ { path: 'grpc', name: 'gRPC' },
+ { path: 'hashicorp', name: 'Hashicorp' },
+ { path: 'http', name: 'Hypertext Transfer Protocol (HTTP)' },
+ { path: 'imap', name: 'Internet Message Access Protocol (IMAP)' },
+ { path: 'jdbc', name: 'Java Database Connectivity (JDBC)' },
+ { path: 'jenkins', name: 'Jenkins' },
+ { path: 'jira', name: 'Jira' },
+ { path: 'microsoft-azure', name: 'Microsoft Azure' },
+ { path: 'microsoft-mssql', name: 'Microsoft SQL Server (MSSQL)' },
+ { path: 'microsoft-winrm', name: 'Windows Remote Management (WinRM)' },
+ { path: 'mongo', name: 'MongoDB' },
+ { path: 'mysql', name: 'MySQL' },
+ { path: 'neo4j', name: 'Neo4J' },
+ { path: 'odbc', name: 'ODBC' },
+ { path: 'openfaas', name: 'OpenFaaS' },
+ { path: 'opsgenie', name: 'Opsgenie' },
+ { path: 'oracle', name: 'Oracle' },
+ { path: 'pagerduty', name: 'Pagerduty' },
+ { path: 'papermill', name: 'Papermill' },
+ { path: 'plexus', name: 'Plexus' },
+ { path: 'postgres', name: 'PostgreSQL' },
+ { path: 'presto', name: 'Presto' },
+ { path: 'qubole', name: 'Qubole' },
+ { path: 'redis', name: 'Redis' },
+ { path: 'salesforce', name: 'Salesforce' },
+ { path: 'samba', name: 'Samba' },
+ { path: 'segment', name: 'Segment' },
+ { path: 'sendgrid', name: 'Sendgrid' },
+ { path: 'sftp', name: 'SFTP' },
+ { path: 'singularity', name: 'Singularity' },
+ { path: 'slack', name: 'Slack' },
+ { path: 'snowflake', name: 'Snowflake' },
+ { path: 'sqlite', name: 'SQLite' },
+ { path: 'ssh', name: 'SSH' },
+ { path: 'telegram', name: 'Telegram' },
+ { path: 'vertica', name: 'Vertica' },
+ { path: 'yandex', name: 'Yandex' },
+ { path: 'zendesk', name: 'Zendesk' },
+ ];
+
+ const webURL = process.env.WEBSERVER_URL;
+
+ return (
+ <AppContainer>
+ <Box mx="auto" my={8} maxWidth="900px">
+ <Flex mt={8}>
+ <Box flex="1">
+ <Heading as="h1">Documentation</Heading>
+ <Text mt={4}>
+ Apache Airflow Core, which includes webserver, scheduler, CLI
and other components
+ that are needed for minimal Airflow installation.
+ </Text>
+ <Button
+ as="a"
+
href="https://airflow.apache.org/docs/apache-airflow/stable/index.html"
+ variant="solid"
+ rightIcon={<FiExternalLink />}
+ mt={4}
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ Apache Airflow Docs
+ </Button>
+ </Box>
+ <Box ml={8} p={4} bg={useColorModeValue('gray.100', 'gray.700')}
borderRadius="md">
+ <Heading as="h3" size="sm">Links</Heading>
+ <List mt={4} spacing={2}>
+ <ListItem>
+ <Link href="https://airflow.apache.org/" isExternal
color="teal.500">
+ <Icon as={FiGlobe} mr={1} />
+ Apache Airflow Website
+ </Link>
+ </ListItem>
+ <ListItem>
+ <Link href="https://github.com/apache/airflow" isExternal
color="teal.500">
+ <Icon as={FaGithub} mr={1} />
+ apache/airflow on GitHub
+ </Link>
+ </ListItem>
+ </List>
+ </Box>
+ </Flex>
+ <Box mt={10}>
+ <Heading as="h3" size="lg">REST API Reference</Heading>
+ <Flex mt={4}>
+ <Button
+ as="a"
+ href={`${webURL}/api/v1/ui/`}
+ target="_blank"
+ rel="noopener noreferrer"
+ variant="outline"
+ rightIcon={<FiExternalLink />}
+ mr={2}
+ >
+ Swagger
+ </Button>
+ <Button
+ as="a"
+ href={`${webURL}/redoc`}
+ target="_blank"
+ rel="noopener noreferrer"
+ variant="outline"
+ rightIcon={<FiExternalLink />}
+ >
+ Redoc
+ </Button>
+ </Flex>
+ </Box>
+ <Box mt={10}>
+ <Heading as="h3" size="lg">Providers Packages</Heading>
+ <Text mt={4}>
+ Providers packages include integrations with third party
integrations.
+ They are updated independently of the Apache Airflow core.
+ </Text>
+
+ <List spacing={2} mt={4} style={{ columns: 3 }}>
+ {providers.map((p) => (
+ <ListItem key={p.path}>
+ <Link
+
href={`https://airflow.apache.org/docs/apache-airflow-providers-${p.path}/stable/index.html`}
+ isExternal
+ color="teal.500"
+ >
+ {p.name}
+ </Link>
+ </ListItem>
+ ))}
+ </List>
+ </Box>
+ </Box>
+ </AppContainer>
+ );
+};
export default Docs;
diff --git a/airflow/ui/src/views/Login.tsx b/airflow/ui/src/views/Login.tsx
index 45f4cc9..af5d714 100644
--- a/airflow/ui/src/views/Login.tsx
+++ b/airflow/ui/src/views/Login.tsx
@@ -31,7 +31,9 @@ import {
Alert,
AlertIcon,
} from '@chakra-ui/react';
-import { MdLock, MdPerson } from 'react-icons/md';
+import { FiLock, FiUser } from 'react-icons/fi';
+
+import AppContainer from 'components/AppContainer';
import { useAuthContext } from 'auth/context';
@@ -46,60 +48,62 @@ const Login: React.FC = () => {
};
return (
- <Box display="flex" alignItems="center" justifyContent="center"
height="80vh">
- <Box as="form" width="100%" maxWidth="400px" mx="auto"
onSubmit={onSubmit}>
- <FormControl>
- <FormLabel htmlFor="username">Username</FormLabel>
- <InputGroup>
- <InputLeftElement>
- <Icon as={MdPerson} color="gray.300" />
- </InputLeftElement>
- <Input
- autoFocus
- autoCapitalize="none"
- name="username"
- placeholder="Username"
- data-testid="username"
- value={username}
- onChange={(e) => setUsername(e.target.value)}
- isRequired
- />
- </InputGroup>
- </FormControl>
- <FormControl mt={4}>
- <FormLabel htmlFor="password">Password</FormLabel>
- <InputGroup>
- <InputLeftElement>
- <Icon as={MdLock} color="gray.300" />
- </InputLeftElement>
- <Input
- type="password"
- name="password"
- placeholder="Password"
- data-testid="password"
- value={password}
- onChange={(e) => setPassword(e.target.value)}
- isRequired
- />
- </InputGroup>
- </FormControl>
- <Button
- width="100%"
- mt={4}
- type="submit"
- disabled={!username || !password}
- data-testid="submit"
- >
- {loading ? <Spinner size="md" speed="0.85s" /> : 'Log in'}
- </Button>
- {error && (
- <Alert status="error" my="4" key={error.message}>
- <AlertIcon />
- {error.message}
- </Alert>
- )}
+ <AppContainer>
+ <Box display="flex" alignItems="center" justifyContent="center"
height="80vh">
+ <Box as="form" width="100%" maxWidth="400px" mx="auto"
onSubmit={onSubmit}>
+ <FormControl>
+ <FormLabel htmlFor="username">Username</FormLabel>
+ <InputGroup>
+ <InputLeftElement>
+ <Icon as={FiUser} color="gray.300" />
+ </InputLeftElement>
+ <Input
+ autoFocus
+ autoCapitalize="none"
+ name="username"
+ placeholder="Username"
+ data-testid="username"
+ value={username}
+ onChange={(e) => setUsername(e.target.value)}
+ isRequired
+ />
+ </InputGroup>
+ </FormControl>
+ <FormControl mt={4}>
+ <FormLabel htmlFor="password">Password</FormLabel>
+ <InputGroup>
+ <InputLeftElement>
+ <Icon as={FiLock} color="gray.300" />
+ </InputLeftElement>
+ <Input
+ type="password"
+ name="password"
+ placeholder="Password"
+ data-testid="password"
+ value={password}
+ onChange={(e) => setPassword(e.target.value)}
+ isRequired
+ />
+ </InputGroup>
+ </FormControl>
+ <Button
+ width="100%"
+ mt={4}
+ type="submit"
+ disabled={!username || !password}
+ data-testid="submit"
+ >
+ {loading ? <Spinner size="md" speed="0.85s" /> : 'Log in'}
+ </Button>
+ {error && (
+ <Alert status="error" my="4" key={error.message}>
+ <AlertIcon />
+ {error.message}
+ </Alert>
+ )}
+ </Box>
</Box>
- </Box>
+ </AppContainer>
);
};
diff --git a/airflow/ui/src/views/Pipelines.tsx
b/airflow/ui/src/views/Pipelines.tsx
index a9afa5b..1a5b05c 100644
--- a/airflow/ui/src/views/Pipelines.tsx
+++ b/airflow/ui/src/views/Pipelines.tsx
@@ -19,14 +19,14 @@
import React from 'react';
import { Center, Heading } from '@chakra-ui/react';
-import AppHeader from 'components/AppHeader';
+import AppContainer from 'components/AppContainer';
const Pipelines: React.FC = () => (
- <AppHeader>
+ <AppContainer>
<Center height="100%" width="100%">
<Heading>Pipelines</Heading>
</Center>
- </AppHeader>
+ </AppContainer>
);
export default Pipelines;
diff --git a/airflow/ui/test/Login.test.tsx b/airflow/ui/test/Login.test.tsx
index 5c297f3..454af03 100644
--- a/airflow/ui/test/Login.test.tsx
+++ b/airflow/ui/test/Login.test.tsx
@@ -45,7 +45,9 @@ test('App shows Login screen by default', () => {
describe('test login component', () => {
test('Button is disabled when there is no username or password', () => {
const { getByTestId } = render(
- <Login />,
+ <BrowserRouter>
+ <Login />
+ </BrowserRouter>,
);
const button = getByTestId('submit');
@@ -54,7 +56,9 @@ describe('test login component', () => {
test('Button is clickable only when username and password exist', () => {
const { getByTestId } = render(
- <Login />,
+ <BrowserRouter>
+ <Login />
+ </BrowserRouter>,
);
const button = getByTestId('submit');
@@ -69,7 +73,9 @@ describe('test login component', () => {
test('Login page shows loading after submit', async () => {
const { getByTestId, getByText } = render(
- <Login />,
+ <BrowserRouter>
+ <Login />
+ </BrowserRouter>,
);
const button = getByTestId('submit');
@@ -90,7 +96,11 @@ describe('test login component', () => {
.replyWithError('Unauthorized');
const { getByTestId, getByText } = render(
- <AuthProvider><Login /></AuthProvider>,
+ <BrowserRouter>
+ <AuthProvider>
+ <Login />
+ </AuthProvider>
+ </BrowserRouter>,
{ wrapper: QueryWrapper },
);
diff --git a/airflow/ui/test/utils.tsx b/airflow/ui/test/utils.tsx
index 935637c..1c14c09 100644
--- a/airflow/ui/test/utils.tsx
+++ b/airflow/ui/test/utils.tsx
@@ -20,7 +20,7 @@
import React from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
-export const url: string = process.env.API_URL || '';
+export const url: string = `${process.env.WEBSERVER_URL}/api/v1/` || '';
export const defaultHeaders = {
'access-control-allow-origin': url,
diff --git a/airflow/ui/yarn.lock b/airflow/ui/yarn.lock
index bebe69c..6006374 100644
--- a/airflow/ui/yarn.lock
+++ b/airflow/ui/yarn.lock
@@ -3932,6 +3932,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
+dayjs@^1.10.4:
+ version "1.10.4"
+ resolved
"https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2"
+ integrity
sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw==
+
[email protected], debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
version "2.6.9"
resolved
"https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"