config-ui/src/routes/layout/layout.tsx (119 lines of code) (raw):
/*
* 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 { useState, useEffect, useMemo } from 'react';
import { useLoaderData, Outlet, useNavigate, useLocation } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import { Layout as AntdLayout, Menu, Divider } from 'antd';
import { PageLoading, Logo, ExternalLink } from '@/components';
import { init, selectError, selectStatus } from '@/features';
import { OnboardCard } from '@/routes/onboard/components';
import { useAppDispatch, useAppSelector } from '@/hooks';
import { menuItems, menuItemsMatch, headerItems } from './config';
const { Sider, Header, Content, Footer } = AntdLayout;
const brandName = import.meta.env.DEVLAKE_BRAND_NAME ?? 'DevLake';
export const Layout = () => {
const [openKeys, setOpenKeys] = useState<string[]>([]);
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
const { version, plugins } = useLoaderData() as { version: string; plugins: string[] };
const navigate = useNavigate();
const { pathname } = useLocation();
const dispatch = useAppDispatch();
const status = useAppSelector(selectStatus);
const error = useAppSelector(selectError);
useEffect(() => {
dispatch(init(plugins));
}, []);
useEffect(() => {
const curMenuItem = menuItemsMatch[pathname];
const parentKey = curMenuItem?.parentKey;
if (parentKey) {
setOpenKeys([parentKey]);
}
}, []);
useEffect(() => {
const selectedKeys = pathname.split('/').reduce((acc, cur, i, arr) => {
if (i === 0) {
acc.push('/');
return acc;
} else {
acc.push(`${arr.slice(0, i + 1).join('/')}`);
return acc;
}
}, [] as string[]);
setSelectedKeys(selectedKeys);
}, [pathname]);
const title = useMemo(() => {
const curMenuItem = menuItemsMatch[pathname];
return curMenuItem?.label ?? '';
}, [pathname]);
if (['idle', 'loading'].includes(status)) {
return <PageLoading />;
}
if (status === 'failed') {
throw error.message;
}
return (
<AntdLayout style={{ height: '100%', overflow: 'hidden' }}>
<Helmet>
<title>
{title ? `${title} - ` : ''}
{brandName}
</title>
</Helmet>
<Sider>
{import.meta.env.DEVLAKE_TITLE_CUSTOM ? (
<h2 style={{ margin: '36px 0', textAlign: 'center', color: '#fff' }}>
{import.meta.env.DEVLAKE_TITLE_CUSTOM}
</h2>
) : (
<Logo style={{ padding: 24 }} />
)}
<Menu
mode="inline"
theme="dark"
items={menuItems}
openKeys={openKeys}
selectedKeys={selectedKeys}
onClick={({ key }) => navigate(key)}
onOpenChange={(keys) => setOpenKeys(keys)}
/>
<div style={{ position: 'absolute', right: 0, bottom: 20, left: 0, color: '#fff', textAlign: 'center' }}>
{version}
</div>
</Sider>
<AntdLayout>
<Header
style={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
padding: '0 24px',
height: 50,
background: 'transparent',
}}
>
{headerItems
.filter((item) =>
import.meta.env.DEVLAKE_COPYRIGHT_HIDE ? !['Dashboards', 'GitHub', 'Slack'].includes(item.label) : true,
)
.map((item, i, arr) => (
<ExternalLink key={item.label} link={item.link} style={{ display: 'flex', alignItems: 'center' }}>
{item.icon}
<span style={{ marginLeft: 4 }}>{item.label}</span>
{i !== arr.length - 1 && <Divider type="vertical" />}
</ExternalLink>
))}
</Header>
<Content style={{ overflowY: 'auto' }}>
<div style={{ padding: 24, margin: '0 auto', maxWidth: 1280 }}>
<OnboardCard style={{ marginBottom: 32 }} />
<Outlet />
</div>
{!import.meta.env.DEVLAKE_COPYRIGHT_HIDE && (
<Footer>
<p style={{ textAlign: 'center' }}>Apache 2.0 License</p>
</Footer>
)}
</Content>
</AntdLayout>
</AntdLayout>
);
};