123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368 |
- 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>
- );
- };
|