blob: 55f92b10cdb43ec20790a23c47e34821d0191617 [file]
// 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 { useMutation } from '@tanstack/react-query';
import { Button, Drawer, Layout, Menu, message } from 'antd';
import { useEffect, useState } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import dorisLogoDark from '../../design/assets/doris-logo-horizontal-dark.svg';
import { logout } from '../api/auth';
import { setCsrfToken } from '../api/csrf';
import type { UiMe } from '../api/types';
import { queryClient } from './queryClient';
interface NavigationItem {
key: string;
label: string;
index: string;
}
const navigation: NavigationItem[] = [
{ key: '/home', label: 'Cluster Overview', index: '01' },
{ key: '/playground', label: 'Playground', index: '02' },
{ key: '/system', label: 'Proc System', index: '03' },
{ key: '/log', label: 'Log', index: '04' },
{ key: '/query-profiles', label: 'Query Profiles', index: '05' },
{ key: '/sessions', label: 'Sessions', index: '06' },
{ key: '/configuration', label: 'Configuration', index: '07' },
];
function selectedPath(pathname: string): string {
return navigation.find((item) => pathname === item.key || pathname.startsWith(`${item.key}/`))?.key ?? '';
}
export function AppShell({ me }: { me: UiMe }) {
const navigate = useNavigate();
const location = useLocation();
const [mobileOpen, setMobileOpen] = useState(false);
const [messageApi, messageContext] = message.useMessage();
const logoutMutation = useMutation({
mutationFn: logout,
onSuccess: () => {
setCsrfToken(null);
queryClient.clear();
void navigate('/login', { replace: true, state: { reason: 'signed-out' } });
},
onError: () => {
void messageApi.error('Sign out failed. Please try again.');
},
});
useEffect(() => {
const handleUnauthorized = () => {
setCsrfToken(null);
queryClient.clear();
const from = `${location.pathname}${location.search}${location.hash}`;
void navigate('/login', { replace: true, state: { reason: 'expired', from } });
};
window.addEventListener('doris-ui:unauthorized', handleUnauthorized);
return () => window.removeEventListener('doris-ui:unauthorized', handleUnauthorized);
}, [location.hash, location.pathname, location.search, navigate]);
const menuItems = navigation.map((item) => ({
key: item.key,
label: (
<span className="nav-label">
<span>{item.index}</span>
{item.label}
</span>
),
}));
const onNavigate = ({ key }: { key: string }) => {
setMobileOpen(false);
void navigate(key);
};
const sidebar = (
<div className="app-sidebar-inner">
<div className="app-brand">
<img src={dorisLogoDark} alt="Apache Doris" />
</div>
<Menu
className="primary-navigation"
theme="dark"
mode="inline"
selectedKeys={[selectedPath(location.pathname)]}
items={menuItems}
onClick={onNavigate}
/>
<div className="sidebar-account">
<span className="ui-label">Signed in as</span>
<strong>{me.user}</strong>
<Button
type="text"
className="sidebar-sign-out"
loading={logoutMutation.isPending}
onClick={() => logoutMutation.mutate()}
>
Sign out
</Button>
</div>
</div>
);
return (
<Layout className="app-layout">
{messageContext}
<Layout.Sider width={240} className="desktop-sidebar">
{sidebar}
</Layout.Sider>
<Drawer
className="mobile-navigation-drawer"
placement="left"
size="default"
open={mobileOpen}
onClose={() => setMobileOpen(false)}
styles={{ body: { padding: 0 } }}
title={null}
>
{sidebar}
</Drawer>
<Layout className="app-workspace">
<Layout.Header className="app-utility-bar">
<Button className="mobile-menu-button" onClick={() => setMobileOpen(true)} aria-label="Open navigation">
Menu
</Button>
<div className="breadcrumb-text">
Console / <strong>{navigation.find((item) => item.key === selectedPath(location.pathname))?.label ?? 'Page'}</strong>
</div>
<span className="fe-connection"><i aria-hidden="true" />FE connected</span>
<span className="utility-user">{me.user}</span>
</Layout.Header>
<Layout.Content className="app-content">
<Outlet context={me} />
</Layout.Content>
</Layout>
</Layout>
);
}