dbf3ad93f1
* chore(deps): update dependency prettier to v3.2.3 * chore(js): fix updated prettier whitespace --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Gabe Kangas <gabek@real-ity.com>
368 lines
11 KiB
TypeScript
368 lines
11 KiB
TypeScript
import React, { FC, ReactNode, useContext, useEffect, useState } from 'react';
|
|
import Link from 'next/link';
|
|
import Head from 'next/head';
|
|
import { differenceInSeconds } from 'date-fns';
|
|
import { useRouter } from 'next/router';
|
|
import { Layout, Menu, Alert, Button, Space, Tooltip } from 'antd';
|
|
|
|
import classNames from 'classnames';
|
|
import dynamic from 'next/dynamic';
|
|
import { upgradeVersionAvailable } from '../../utils/apis';
|
|
import { parseSecondsToDurationString } from '../../utils/format';
|
|
|
|
import { OwncastLogo } from '../common/OwncastLogo/OwncastLogo';
|
|
import { ServerStatusContext } from '../../utils/server-status-context';
|
|
import { AlertMessageContext } from '../../utils/alert-message-context';
|
|
|
|
import { TextFieldWithSubmit } from './TextFieldWithSubmit';
|
|
import { TEXTFIELD_PROPS_STREAM_TITLE } from '../../utils/config-constants';
|
|
import { ComposeFederatedPost } from './ComposeFederatedPost';
|
|
import { UpdateArgs } from '../../types/config-section';
|
|
import { FatalErrorStateModal } from '../modals/FatalErrorStateModal/FatalErrorStateModal';
|
|
|
|
// Lazy loaded components
|
|
|
|
const SettingOutlined = dynamic(() => import('@ant-design/icons/SettingOutlined'), {
|
|
ssr: false,
|
|
}); // Lazy loaded components
|
|
|
|
const HomeOutlined = dynamic(() => import('@ant-design/icons/HomeOutlined'), {
|
|
ssr: false,
|
|
});
|
|
|
|
const LineChartOutlined = dynamic(() => import('@ant-design/icons/LineChartOutlined'), {
|
|
ssr: false,
|
|
});
|
|
|
|
const ToolOutlined = dynamic(() => import('@ant-design/icons/ToolOutlined'), {
|
|
ssr: false,
|
|
});
|
|
|
|
const PlayCircleFilled = dynamic(() => import('@ant-design/icons/PlayCircleFilled'), {
|
|
ssr: false,
|
|
});
|
|
|
|
const MinusSquareFilled = dynamic(() => import('@ant-design/icons/MinusSquareFilled'), {
|
|
ssr: false,
|
|
});
|
|
|
|
const QuestionCircleOutlined = dynamic(() => import('@ant-design/icons/QuestionCircleOutlined'), {
|
|
ssr: false,
|
|
});
|
|
|
|
const MessageOutlined = dynamic(() => import('@ant-design/icons/MessageOutlined'), {
|
|
ssr: false,
|
|
});
|
|
|
|
const ExperimentOutlined = dynamic(() => import('@ant-design/icons/ExperimentOutlined'), {
|
|
ssr: false,
|
|
});
|
|
|
|
const EditOutlined = dynamic(() => import('@ant-design/icons/EditOutlined'), {
|
|
ssr: false,
|
|
});
|
|
|
|
const FediverseOutlined = dynamic(() => import('../../assets/images/icons/fediverse.svg'), {
|
|
ssr: false,
|
|
});
|
|
|
|
export type MainLayoutProps = {
|
|
children: ReactNode;
|
|
};
|
|
|
|
export const MainLayout: FC<MainLayoutProps> = ({ children }) => {
|
|
const context = useContext(ServerStatusContext);
|
|
const { serverConfig, online, broadcaster, versionNumber, error: serverError } = context || {};
|
|
const { instanceDetails, chatDisabled, federation } = serverConfig;
|
|
const { enabled: federationEnabled } = federation;
|
|
|
|
const [currentStreamTitle, setCurrentStreamTitle] = useState('');
|
|
const [postModalDisplayed, setPostModalDisplayed] = useState(false);
|
|
|
|
const alertMessage = useContext(AlertMessageContext);
|
|
|
|
const router = useRouter();
|
|
const { route } = router || {};
|
|
|
|
const { Header, Footer, Content, Sider } = Layout;
|
|
|
|
const [upgradeVersion, setUpgradeVersion] = useState('');
|
|
const checkForUpgrade = async () => {
|
|
try {
|
|
const result = await upgradeVersionAvailable(versionNumber);
|
|
setUpgradeVersion(result);
|
|
} catch (error) {
|
|
console.log('==== error', error);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
checkForUpgrade();
|
|
}, [versionNumber]);
|
|
|
|
useEffect(() => {
|
|
setCurrentStreamTitle(instanceDetails.streamTitle);
|
|
}, [instanceDetails]);
|
|
|
|
const handleStreamTitleChanged = ({ value }: UpdateArgs) => {
|
|
setCurrentStreamTitle(value);
|
|
};
|
|
|
|
const handleCreatePostButtonPressed = () => {
|
|
setPostModalDisplayed(true);
|
|
};
|
|
|
|
const appClass = classNames({
|
|
'app-container': true,
|
|
online,
|
|
});
|
|
|
|
const upgradeVersionString = `${upgradeVersion}` || '';
|
|
const upgradeMessage = `Upgrade to v${upgradeVersionString}`;
|
|
const openMenuItems = upgradeVersion ? ['utilities-menu'] : [];
|
|
|
|
const clearAlertMessage = () => {
|
|
alertMessage.setMessage(null);
|
|
};
|
|
|
|
const headerAlertMessage = alertMessage.message ? (
|
|
<Alert message={alertMessage.message} afterClose={clearAlertMessage} banner closable />
|
|
) : null;
|
|
|
|
// status indicator items
|
|
const streamDurationString = broadcaster
|
|
? parseSecondsToDurationString(differenceInSeconds(new Date(), new Date(broadcaster.time)))
|
|
: '';
|
|
|
|
const statusIcon = online ? <PlayCircleFilled /> : <MinusSquareFilled />;
|
|
const statusMessage = online ? `Online ${streamDurationString}` : 'Offline';
|
|
|
|
const statusIndicator = (
|
|
<div className="online-status-indicator">
|
|
<span className="status-label">{statusMessage}</span>
|
|
<span className="status-icon">{statusIcon}</span>
|
|
</div>
|
|
);
|
|
|
|
const integrationsMenu = [
|
|
{
|
|
label: <Link href="/admin/webhooks">Webhooks</Link>,
|
|
key: '/admin/webhooks',
|
|
},
|
|
{
|
|
label: <Link href="/admin/access-tokens">Access Tokens</Link>,
|
|
key: '/admin/access-tokens',
|
|
},
|
|
{
|
|
label: <Link href="/admin/actions">External Actions</Link>,
|
|
key: '/admin/actions',
|
|
},
|
|
];
|
|
|
|
const chatMenu = [
|
|
{
|
|
label: <Link href="/admin/chat/messages">Messages</Link>,
|
|
key: '/admin/chat/messages',
|
|
},
|
|
{
|
|
label: <Link href="/admin/chat/users">Users</Link>,
|
|
key: '/admin/chat/users',
|
|
},
|
|
{
|
|
label: <Link href="/admin/chat/emojis">Emojis</Link>,
|
|
key: '/admin/chat/emojis',
|
|
},
|
|
];
|
|
|
|
const utilitiesMenu = [
|
|
{
|
|
label: <Link href="/admin/hardware-info">Hardware</Link>,
|
|
key: '/admin/hardware-info',
|
|
},
|
|
{
|
|
label: <Link href="/admin/stream-health">Stream Health</Link>,
|
|
key: '/admin/stream-health',
|
|
},
|
|
{
|
|
label: <Link href="/admin/logs">Logs</Link>,
|
|
key: '/admin/logs',
|
|
},
|
|
federationEnabled && {
|
|
label: <Link href="/admin/federation/actions">Social Actions</Link>,
|
|
key: '/admin/federation/actions',
|
|
},
|
|
];
|
|
|
|
const configurationMenu = [
|
|
{
|
|
label: <Link href="/admin/config/general">General</Link>,
|
|
key: '/admin/config/general',
|
|
},
|
|
{
|
|
label: <Link href="/admin/config/server">Server Setup</Link>,
|
|
key: '/admin/config/server',
|
|
},
|
|
{
|
|
label: <Link href="/admin/config-video">Video</Link>,
|
|
key: '/admin/config-video',
|
|
},
|
|
{
|
|
label: <Link href="/admin/config-chat">Chat</Link>,
|
|
key: '/admin/config-chat',
|
|
},
|
|
{
|
|
label: <Link href="/admin/config-federation">Social</Link>,
|
|
key: '/admin/config-federation',
|
|
},
|
|
{
|
|
label: <Link href="/admin/config-notify">Notifications</Link>,
|
|
key: '/admin/config-notify',
|
|
},
|
|
];
|
|
|
|
const menuItems = [
|
|
{ label: <Link href="/admin">Home</Link>, icon: <HomeOutlined />, key: '/admin' },
|
|
{
|
|
label: <Link href="/admin/viewer-info">Viewers</Link>,
|
|
icon: <LineChartOutlined />,
|
|
key: '/admin/viewer-info',
|
|
},
|
|
!chatDisabled && {
|
|
label: <span>Chat & Users</span>,
|
|
icon: <MessageOutlined />,
|
|
children: chatMenu,
|
|
key: 'chat-and-users',
|
|
},
|
|
federationEnabled && {
|
|
key: '/admin/federation/followers',
|
|
label: <Link href="/admin/federation/followers">Followers</Link>,
|
|
icon: (
|
|
<span
|
|
role="img"
|
|
aria-label="message"
|
|
className="anticon anticon-message ant-menu-item-icon"
|
|
>
|
|
{/* Wrapping the icon in span for consistency with other icons used
|
|
directly from antd */}
|
|
<FediverseOutlined />
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'configuration',
|
|
label: 'Configuration',
|
|
icon: <SettingOutlined />,
|
|
children: configurationMenu,
|
|
},
|
|
{
|
|
key: 'utilities',
|
|
label: 'Utilities',
|
|
icon: <ToolOutlined />,
|
|
children: utilitiesMenu,
|
|
},
|
|
{
|
|
key: 'integrations',
|
|
label: 'Integrations',
|
|
icon: <ExperimentOutlined />,
|
|
children: integrationsMenu,
|
|
},
|
|
upgradeVersion && {
|
|
key: '/admin/upgrade',
|
|
label: <Link href="/admin/upgrade">{upgradeMessage}</Link>,
|
|
},
|
|
{
|
|
key: '/admin/help',
|
|
label: <Link href="/admin/help">Help</Link>,
|
|
icon: <QuestionCircleOutlined />,
|
|
},
|
|
];
|
|
|
|
const [openKeys, setOpenKeys] = useState(openMenuItems);
|
|
|
|
const onOpenChange = (keys: string[]) => {
|
|
setOpenKeys(keys);
|
|
};
|
|
|
|
useEffect(() => {
|
|
menuItems.forEach(item =>
|
|
item?.children?.forEach(child => {
|
|
if (child?.key === route) setOpenKeys([...openMenuItems, item.key]);
|
|
}),
|
|
);
|
|
}, []);
|
|
|
|
return (
|
|
<Layout id="admin-page" className={appClass}>
|
|
<Head>
|
|
<title>Owncast Admin</title>
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon/favicon-32x32.png" />
|
|
</Head>
|
|
|
|
{serverError?.type === 'OWNCAST_SERVICE_UNREACHABLE' && (
|
|
<FatalErrorStateModal title="Server Unreachable" message={serverError.msg} />
|
|
)}
|
|
|
|
<Sider width={240} className="side-nav">
|
|
<h1 className="owncast-title">
|
|
<span className="logo-container">
|
|
<OwncastLogo variant="simple" />
|
|
</span>
|
|
<span className="title-label">Owncast Admin</span>
|
|
</h1>
|
|
<Menu
|
|
mode="inline"
|
|
className="menu-container"
|
|
items={menuItems}
|
|
selectedKeys={[route || '/admin']}
|
|
openKeys={openKeys}
|
|
onOpenChange={onOpenChange}
|
|
/>
|
|
</Sider>
|
|
|
|
<Layout className="layout-main">
|
|
<Header className="layout-header">
|
|
<Space direction="horizontal">
|
|
<Tooltip title="Compose post to your social followers">
|
|
<Button
|
|
type="link"
|
|
icon={<EditOutlined />}
|
|
size="small"
|
|
onClick={handleCreatePostButtonPressed}
|
|
style={{ display: federationEnabled ? 'block' : 'none', margin: '10px' }}
|
|
>
|
|
Compose Post
|
|
</Button>
|
|
</Tooltip>
|
|
</Space>
|
|
<div className="global-stream-title-container">
|
|
<TextFieldWithSubmit
|
|
fieldName="streamTitle"
|
|
{...TEXTFIELD_PROPS_STREAM_TITLE}
|
|
placeholder="What are you streaming now? (Stream title)"
|
|
value={currentStreamTitle}
|
|
initialValue={instanceDetails.streamTitle}
|
|
onChange={handleStreamTitleChanged}
|
|
/>
|
|
</div>
|
|
<Space direction="horizontal">{statusIndicator}</Space>
|
|
</Header>
|
|
|
|
{headerAlertMessage}
|
|
|
|
<Content className="main-content-container">{children}</Content>
|
|
|
|
<Footer className="footer-container">
|
|
<a href="https://owncast.online/?source=admin" target="_blank" rel="noopener noreferrer">
|
|
About Owncast v{versionNumber}
|
|
</a>
|
|
</Footer>
|
|
</Layout>
|
|
|
|
<ComposeFederatedPost
|
|
open={postModalDisplayed}
|
|
handleClose={() => setPostModalDisplayed(false)}
|
|
/>
|
|
</Layout>
|
|
);
|
|
};
|