Files
gpustack-ui/src/layouts/extraRender.tsx
T
lofyerandClaude Opus 4.8 99f9ae19cb feat(branding): rebrand footer vendor to MesaStack, trim footer/topbar
- Footer vendor label unified to "MesaStack" across all locales.
- Remove footer Help button; keep version as plain text (no click).
- Remove top-right GitHub star widget (delete unused github-star.tsx);
  keep version as plain text (no click), retain upgrade "New" badge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-04 14:39:20 +08:00

296 lines
7.5 KiB
TypeScript

import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user';
import PluginExtraField from '@/components/plugin-extra-fields';
import externalLinks from '@/constants/external-links';
import { logout } from '@/pages/login/apis';
import { getGPUStackPlugin } from '@/plugins';
import { useModel } from '@@/plugin-model';
import {
DiscordOutlined,
GithubOutlined,
HomeOutlined,
ReadOutlined
} from '@ant-design/icons';
import { DropdownActions, IconFont } from '@gpustack/core-ui';
import { history, useIntl, useNavigate } from '@umijs/max';
import { Avatar, Divider } from 'antd';
import { useAtom } from 'jotai';
import { useMemo } from 'react';
import styled from 'styled-components';
import { DEFAULT_ENTER_PAGE } from '../config/settings';
const NewLabel = styled.span`
position: relative;
top: -4px;
right: 4px;
padding: 2px 4px;
display: flex;
color: #fff;
height: 15px;
justify-content: center;
align-items: center;
background-color: var(--ant-orange-5);
border-radius: 6px 6px 6px 0;
transform: scale(0.9);
.text {
transform: scale(0.8);
line-height: 1em;
}
`;
const IconWrapper = styled.span`
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--ant-color-text-secondary);
`;
const Wrapper = styled.div`
display: flex;
align-items: center;
gap: 24px;
height: 32px;
`;
const DropdownWrapper = styled.div`
min-width: 160px;
box-shadow: var(--ant-box-shadow-secondary);
background-color: var(--ant-color-bg-elevated);
border-radius: var(--ant-border-radius-lg);
padding: var(--ant-padding-xs);
.ant-dropdown-menu {
padding: 0;
box-shadow: none;
background-color: transparent;
border-radius: 0;
a {
color: var(--ant-color-text);
}
}
`;
const CustomItem = styled.div`
display: flex;
align-items: center;
gap: 8px;
height: 32px;
justify-content: flex-start;
height: 32px;
padding: 0 var(--ant-padding-xs);
cursor: pointer;
&.user-info {
cursor: default;
justify-content: space-between;
.user-name {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 100px;
}
}
&:not(.user-info):hover {
background-color: var(--ant-control-item-bg-hover);
}
background-color: var(--ant-color-bg-elevated);
`;
export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
const { isDarkTheme } = props;
const plugin = getGPUStackPlugin();
const intl = useIntl();
const [version] = useAtom(GPUStackVersionAtom);
const [updateCheck] = useAtom(UpdateCheckAtom);
const initialInfo = useModel('@@initialState') || {
initialState: undefined,
loading: false,
setInitialState: null
};
const { initialState } = initialInfo;
const navigate = useNavigate();
const loginPath = DEFAULT_ENTER_PAGE.login;
// only admin can see upgrade info, and current version is a prod version(exclude dev/rc)
const showUpgrade = useMemo(() => {
return (
initialState?.currentUser?.is_admin &&
updateCheck.latest_version &&
updateCheck.latest_version !== version?.version &&
version?.isProd
);
}, [
updateCheck.latest_version,
version.version,
version.isProd,
initialState?.currentUser?.is_admin
]);
const avatarStyle = useMemo(() => {
if (isDarkTheme) {
return {
color: 'var(--ant-color-text)',
border: 'none'
};
}
return {};
}, [isDarkTheme]);
const handleLogout = async () => {
await logout();
navigate(loginPath);
};
const helpList = [
{
key: 'site',
icon: <HomeOutlined />,
label: 'MesaStack',
url: externalLinks.site
},
{
key: 'github',
icon: <GithubOutlined />,
label: intl.formatMessage({ id: 'common.issue.report' }),
url: externalLinks.reportIssue
},
{
key: 'faq',
icon: <IconFont type="icon-fankuifaqs"></IconFont>,
label: intl.formatMessage({ id: 'common.button.faq' }),
url: externalLinks.faq
},
{
key: 'Discord',
icon: <DiscordOutlined />,
label: 'Discord',
url: externalLinks.discord
},
{
key: 'docs',
icon: <ReadOutlined />,
label: intl.formatMessage({ id: 'common.button.docs' }),
url: externalLinks.documentation
}
];
const helpMenu = {
items: helpList.map((item) => ({
key: item.key,
label: (
<a
className="flex flex-center gap-8"
href={item.url}
target="_blank"
rel="noreferrer"
>
{item.icon}
{item.label}
</a>
)
}))
};
const userMenu = {
items: [
{
key: 'settings',
label: (
<span className="flex flex-center">
<IconFont type="icon-preferences" />
<span className="m-l-8" style={{ marginLeft: 8 }}>
{intl?.formatMessage?.({ id: 'common.preferences' })}
</span>
</span>
),
onClick: () => {
history.push('/preferences');
}
}
]
};
const userPopupRender = (originNode: React.ReactNode) => {
return (
<DropdownWrapper>
<CustomItem className="user-info border-bottom">
<span className="flex-center gap-8">
<Avatar
size={24}
style={{ ...avatarStyle }}
src={initialState?.currentUser?.avatar_url}
icon={
<IconFont type="icon-user-filled" className="font-size-24" />
}
/>
<span className="user-name">
{initialState?.currentUser?.username}
</span>
</span>
</CustomItem>
<Divider style={{ marginBlock: 4 }} />
{originNode}
<Divider style={{ marginBlock: 4 }} />
<CustomItem onClick={handleLogout} className="border-top">
<IconFont type="icon-logout" style={{ fontSize: 17 }} />
<span>{intl?.formatMessage?.({ id: 'common.button.logout' })}</span>
</CustomItem>
</DropdownWrapper>
);
};
const helpPopupRender = (originNode: React.ReactNode) => {
return <DropdownWrapper>{originNode}</DropdownWrapper>;
};
return (
<Wrapper>
<PluginExtraField name="OrgSwitcher" isDarkTheme={isDarkTheme} />
<div
style={{
display: 'flex',
alignItems: 'center'
}}
>
<span
style={{
color: 'var(--ant-color-text-tertiary)'
}}
>
{version.version}
</span>
{showUpgrade && (
<NewLabel>
<span className="text">
{intl.formatMessage({ id: 'common.text.new' })}
</span>
</NewLabel>
)}
</div>
{!plugin && (
<DropdownActions menu={{ ...helpMenu }} popupRender={helpPopupRender}>
<IconWrapper>
<IconFont
type="icon-help"
className="font-size-20"
style={{ color: 'var(--ant-color-text-tertiary)' }}
/>
</IconWrapper>
</DropdownActions>
)}
<PluginExtraField name="GlobalSettings" />
<DropdownActions menu={{ ...userMenu }} popupRender={userPopupRender}>
<IconWrapper>
<Avatar
size={24}
style={{ ...avatarStyle }}
src={initialState?.currentUser?.avatar_url}
icon={<IconFont type="icon-user-filled" className="font-size-24" />}
/>
</IconWrapper>
</DropdownActions>
</Wrapper>
);
};