0.3.16
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { matchPath, NavLink, Outlet, useLocation } from 'react-router-dom';
|
||||
import { NavLink, Outlet, useLocation } from 'react-router-dom';
|
||||
import { Icon } from './Icon';
|
||||
import { OmniSearch } from './OmniSearch';
|
||||
import { NotificationBell } from './NotificationBell';
|
||||
@@ -16,13 +16,12 @@ import { Confirm } from './ui';
|
||||
|
||||
function Rail({ open, onNavigate }: { open: boolean; onNavigate: () => void }) {
|
||||
const { unread } = useNotifications();
|
||||
const location = useLocation();
|
||||
|
||||
// A group holding the current page is open regardless of what was collapsed last time:
|
||||
// a rail that hides the page you are on is a rail that has lost its place.
|
||||
// Every category is visible. Children can be folded independently and the choice is
|
||||
// remembered between visits.
|
||||
const [collapsed, setCollapsed] = useState<Record<string, boolean>>(() => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('memby-admin-nav') ?? '{}') as Record<string, boolean>;
|
||||
return JSON.parse(localStorage.getItem('memby-admin-nav-v2') ?? '{}') as Record<string, boolean>;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
@@ -30,37 +29,18 @@ function Rail({ open, onNavigate }: { open: boolean; onNavigate: () => void }) {
|
||||
|
||||
const storeCollapsed = (next: Record<string, boolean>) => {
|
||||
try {
|
||||
localStorage.setItem('memby-admin-nav', JSON.stringify(next));
|
||||
localStorage.setItem('memby-admin-nav-v2', JSON.stringify(next));
|
||||
} catch {
|
||||
// Private-mode storage refusals cost the memory of which group is open, and
|
||||
// nothing else.
|
||||
}
|
||||
};
|
||||
|
||||
// The labelled groups are an accordion. Following a destination opens its group and
|
||||
// folds the one the operator has just left, including navigation from search or Back.
|
||||
useEffect(() => {
|
||||
const activeGroup = nav.find((group) =>
|
||||
group.items.some((item) => matchPath({ path: item.path, end: true }, location.pathname)),
|
||||
);
|
||||
if (!activeGroup) return;
|
||||
setCollapsed((current) => {
|
||||
const next = { ...current };
|
||||
nav.forEach((group) => {
|
||||
if (group.collapsible !== false) next[group.id] = group.id !== activeGroup.id;
|
||||
});
|
||||
storeCollapsed(next);
|
||||
return next;
|
||||
});
|
||||
}, [location.pathname]);
|
||||
|
||||
const toggle = (id: string, defaultCollapsed = false) => {
|
||||
setCollapsed((current) => {
|
||||
const next = { ...current };
|
||||
const collapseSelected = !(current[id] ?? defaultCollapsed);
|
||||
nav.forEach((group) => {
|
||||
if (group.collapsible !== false) next[group.id] = group.id === id ? collapseSelected : true;
|
||||
});
|
||||
next[id] = collapseSelected;
|
||||
storeCollapsed(next);
|
||||
return next;
|
||||
});
|
||||
@@ -71,11 +51,8 @@ function Rail({ open, onNavigate }: { open: boolean; onNavigate: () => void }) {
|
||||
{nav.map((group) => {
|
||||
const items = group.items.filter((item) => !item.hidden);
|
||||
if (items.length === 0) return null;
|
||||
const holdsCurrent = items.some((item) =>
|
||||
matchPath({ path: item.path, end: true }, location.pathname),
|
||||
);
|
||||
const collapsible = group.collapsible !== false;
|
||||
const expanded = holdsCurrent || !collapsible || !(collapsed[group.id] ?? group.defaultCollapsed ?? false);
|
||||
const expanded = !collapsible || !(collapsed[group.id] ?? group.defaultCollapsed ?? false);
|
||||
return (
|
||||
<div className="rail-group" key={group.id}>
|
||||
{group.label && collapsible ? (
|
||||
|
||||
+72
-367
@@ -1,18 +1,8 @@
|
||||
import type { IconName } from './components/Icon';
|
||||
|
||||
/* The console's table of contents, and the only place a page is declared.
|
||||
*
|
||||
* The rail, the page search and the router all read it, so a page cannot be in the menu
|
||||
* and 404, or be reachable and unnamed. It used to have to agree with a matching list in
|
||||
* Go as well — the gateway rendered the rail and decided which /admin URLs were legal —
|
||||
* which is a duplication the single-page console removes: the gateway now serves the
|
||||
* console for any /admin path and the routing is entirely here.
|
||||
*
|
||||
* `hidden` marks a destination that is reachable and titled but not in the rail: a page
|
||||
* about one person or one television belongs to the thing it is about, not to a menu.
|
||||
* Everyday is deliberately frequency-based; the remaining sections are organised by the
|
||||
* operator's question and begin folded so the common routes never scroll out of reach. */
|
||||
|
||||
/* The console's table of contents. The rail and page search both read this catalogue so
|
||||
* every destination has one name and one home. Detail pages stay in the catalogue for
|
||||
* routing and search, but remain out of the rail when they need an id in their URL. */
|
||||
export interface NavItem {
|
||||
id: string;
|
||||
path: string;
|
||||
@@ -21,406 +11,121 @@ export interface NavItem {
|
||||
intro: string;
|
||||
icon?: IconName;
|
||||
hidden?: boolean;
|
||||
/** badge names a live count the rail should show beside this item — today only the
|
||||
* unread activity count. */
|
||||
badge?: 'notifications';
|
||||
}
|
||||
|
||||
export interface NavGroup {
|
||||
id: string;
|
||||
label?: string;
|
||||
/** Less-frequent sections begin folded, but still open whenever they hold the current
|
||||
* page. An operator's explicit choice is remembered and takes precedence. */
|
||||
label: string;
|
||||
defaultCollapsed?: boolean;
|
||||
/** A group may opt out of the accordion when its links must remain visible. */
|
||||
collapsible?: boolean;
|
||||
items: NavItem[];
|
||||
}
|
||||
|
||||
const item = (
|
||||
id: string,
|
||||
path: string,
|
||||
label: string,
|
||||
title: string,
|
||||
intro: string,
|
||||
icon: IconName,
|
||||
extra: Pick<NavItem, 'hidden' | 'badge'> = {},
|
||||
): NavItem => ({ id, path, label, title, intro, icon, ...extra });
|
||||
|
||||
export const nav: NavGroup[] = [
|
||||
{
|
||||
id: 'everyday',
|
||||
label: 'Everyday',
|
||||
defaultCollapsed: false,
|
||||
id: 'dashboard', label: 'Dashboard', defaultCollapsed: false, collapsible: false,
|
||||
items: [item('overview', '/admin', 'Overview', 'Overview', 'What the gateway is doing right now.', 'overview')],
|
||||
},
|
||||
{
|
||||
id: 'map', label: 'Map', defaultCollapsed: false, collapsible: false,
|
||||
items: [
|
||||
{
|
||||
id: 'overview',
|
||||
path: '/admin',
|
||||
label: 'Overview',
|
||||
title: 'Overview',
|
||||
intro: 'What the gateway is doing right now.',
|
||||
icon: 'overview',
|
||||
},
|
||||
{
|
||||
id: 'activity',
|
||||
path: '/admin/activity',
|
||||
label: 'Activity',
|
||||
title: 'Activity',
|
||||
intro: 'Every administrative event, newest first.',
|
||||
icon: 'bell',
|
||||
badge: 'notifications',
|
||||
},
|
||||
{
|
||||
id: 'accounts',
|
||||
path: '/admin/accounts',
|
||||
label: 'Users',
|
||||
title: 'Users',
|
||||
intro: 'Who uses Memby, and the devices they are signed in on.',
|
||||
icon: 'people',
|
||||
},
|
||||
{
|
||||
id: 'requests',
|
||||
path: '/admin/requests',
|
||||
label: 'Media requests',
|
||||
title: 'Media requests',
|
||||
intro: 'Who can ask for something the library does not have.',
|
||||
icon: 'inbox',
|
||||
},
|
||||
{
|
||||
/* The record of what Memby sent, which is a different question from the activity
|
||||
feed above it: that is the operator's own bell, this is every outbound
|
||||
notification to a viewer or an external service, whichever feature produced it. */
|
||||
id: 'notifications',
|
||||
path: '/admin/notifications',
|
||||
label: 'Notifications',
|
||||
title: 'Notifications',
|
||||
intro: 'Everything Memby sent: who it went to, over which channel, and whether it worked.',
|
||||
icon: 'send',
|
||||
},
|
||||
{
|
||||
id: 'notification-settings',
|
||||
path: '/admin/notification-settings',
|
||||
label: 'TV notifications',
|
||||
title: 'TV notifications',
|
||||
intro: 'Choose where notifications appear and who receives them.',
|
||||
icon: 'bell',
|
||||
},
|
||||
{
|
||||
id: 'media-reports',
|
||||
path: '/admin/media-reports',
|
||||
label: 'Media reports',
|
||||
title: 'Media reports',
|
||||
intro: 'Problems viewers reported with a film or episode.',
|
||||
icon: 'alert',
|
||||
},
|
||||
{
|
||||
id: 'updates',
|
||||
path: '/admin/updates',
|
||||
label: 'App updates',
|
||||
title: 'App updates',
|
||||
intro: 'Publish an optional or a required client update.',
|
||||
icon: 'upload',
|
||||
},
|
||||
{
|
||||
id: 'logs',
|
||||
path: '/admin/logs',
|
||||
label: 'Logs',
|
||||
title: 'Logs',
|
||||
intro: 'Structured gateway events as they happen.',
|
||||
icon: 'list',
|
||||
},
|
||||
item('journeys', '/admin/journeys', 'Journeys', 'User journeys', 'How viewers move through Memby and complete flows.', 'journey'),
|
||||
item('journey-viewer', '/admin/journeys/:userId', 'Journey', 'User journey', 'One viewer’s journey through Memby.', 'journey', { hidden: true }),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'people',
|
||||
label: 'Devices & access',
|
||||
defaultCollapsed: true,
|
||||
id: 'history', label: 'History', defaultCollapsed: false,
|
||||
items: [
|
||||
{
|
||||
id: 'account',
|
||||
path: '/admin/accounts/:userId',
|
||||
label: 'User',
|
||||
title: 'User',
|
||||
intro: 'Devices, recommendation setup and synced settings for one person.',
|
||||
icon: 'person',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
id: 'settings-history',
|
||||
path: '/admin/accounts/:userId/settings',
|
||||
label: 'Settings history',
|
||||
title: 'Settings history',
|
||||
intro: "Every change to one person's synced settings, and which devices took it.",
|
||||
icon: 'sliders',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
id: 'clients',
|
||||
path: '/admin/clients',
|
||||
label: 'Devices',
|
||||
title: 'Devices',
|
||||
intro: 'Which sets have reported in, what they are running and what their build understands.',
|
||||
icon: 'tv',
|
||||
},
|
||||
{
|
||||
id: 'logins',
|
||||
path: '/admin/logins',
|
||||
label: 'Sign-ins',
|
||||
title: 'Sign-in history',
|
||||
intro: 'Every connection attempt: who, which television, from where, and whether it got in.',
|
||||
icon: 'key',
|
||||
},
|
||||
{
|
||||
id: 'device',
|
||||
path: '/admin/devices/:deviceId',
|
||||
label: 'Device',
|
||||
title: 'Device',
|
||||
intro: 'One television: how often it connects, at what times, and from which addresses.',
|
||||
icon: 'tv',
|
||||
hidden: true,
|
||||
},
|
||||
item('activity', '/admin/activity', 'Activity', 'Activity', 'Every administrative event, newest first.', 'bell', { badge: 'notifications' }),
|
||||
item('logins', '/admin/logins', 'Sign-ins', 'Sign-in history', 'Every connection attempt and whether it got in.', 'key'),
|
||||
item('notifications', '/admin/notifications', 'Notifications', 'Notifications', 'Everything Memby sent and whether it worked.', 'send'),
|
||||
item('imports', '/admin/imports', 'Imports', 'Imports', 'Catalogue synchronisation history.', 'database'),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'content',
|
||||
label: 'Content & discovery',
|
||||
defaultCollapsed: true,
|
||||
id: 'stats', label: 'Stats', defaultCollapsed: false,
|
||||
items: [
|
||||
{
|
||||
id: 'library',
|
||||
path: '/admin/library',
|
||||
label: 'Library',
|
||||
title: 'Library',
|
||||
intro: 'Import and inspect the catalogue Memby ranks.',
|
||||
icon: 'library',
|
||||
},
|
||||
{
|
||||
id: 'hero',
|
||||
path: '/admin/hero',
|
||||
label: 'Home hero',
|
||||
title: 'Home hero',
|
||||
intro:
|
||||
'Choose films or television shows for the launcher spotlight while recent releases fill the remaining places.',
|
||||
icon: 'star',
|
||||
},
|
||||
{
|
||||
id: 'metadata-hero',
|
||||
path: '/admin/metadata-hero',
|
||||
label: 'Metadata hero',
|
||||
title: 'Metadata hero',
|
||||
intro: 'Order the focused-title information shown above browse rows on every television.',
|
||||
icon: 'tv',
|
||||
},
|
||||
{
|
||||
id: 'recommendations',
|
||||
path: '/admin/recommendations',
|
||||
label: 'For You',
|
||||
title: 'For You',
|
||||
intro: 'The prepared pools personalised rows are drawn from.',
|
||||
icon: 'sparkle',
|
||||
},
|
||||
{
|
||||
id: 'inspector',
|
||||
path: '/admin/inspector',
|
||||
label: 'Score inspector',
|
||||
title: 'Score inspector',
|
||||
intro: 'Re-run the ranker for one person and read every component.',
|
||||
icon: 'search',
|
||||
},
|
||||
item('views', '/admin/views', 'Views', 'App views', 'Home-screen visits, viewers and the times Memby is used.', 'overview'),
|
||||
item('engagement', '/admin/engagement', 'Engagement', 'Row engagement', 'Impressions, focus, dwell and selections per launcher row.', 'chart'),
|
||||
item('searches', '/admin/searches', 'Searches', 'Searches', 'What viewers have been looking for.', 'search'),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'experience',
|
||||
label: 'Viewing experience',
|
||||
defaultCollapsed: true,
|
||||
id: 'media', label: 'Media', defaultCollapsed: false,
|
||||
items: [
|
||||
{
|
||||
id: 'features',
|
||||
path: '/admin/features',
|
||||
label: 'Client configuration',
|
||||
title: 'Client configuration',
|
||||
intro: 'Control everything the thin TV client renders, without releasing an APK.',
|
||||
icon: 'sliders',
|
||||
},
|
||||
{
|
||||
id: 'playback',
|
||||
path: '/admin/playback',
|
||||
label: 'Playback',
|
||||
title: 'Playback',
|
||||
intro: 'Presentation policy sent with every playback launch.',
|
||||
icon: 'play',
|
||||
},
|
||||
{
|
||||
id: 'subtitles',
|
||||
path: '/admin/subtitles',
|
||||
label: 'Subtitles',
|
||||
title: 'Subtitles',
|
||||
intro: 'Which providers a viewer may fetch a missing subtitle from.',
|
||||
icon: 'captions',
|
||||
},
|
||||
{
|
||||
id: 'credits',
|
||||
path: '/admin/credits',
|
||||
label: 'Credits detection',
|
||||
title: 'Credits detection',
|
||||
intro: 'Control predictive scanning and review every completed credits scan.',
|
||||
icon: 'clock',
|
||||
},
|
||||
item('library', '/admin/library', 'Library', 'Library', 'Import and inspect the catalogue Memby ranks.', 'library'),
|
||||
item('requests', '/admin/requests', 'Requests', 'Media requests', 'Who can ask for something the library does not have.', 'inbox'),
|
||||
item('recommendations', '/admin/recommendations', 'Recommendations', 'For You', 'The prepared pools personalised rows are drawn from.', 'sparkle'),
|
||||
item('inspector', '/admin/inspector', 'Score inspector', 'Score inspector', 'Re-run the ranker for one person and read every component.', 'search'),
|
||||
item('hero', '/admin/hero', 'Home hero', 'Home hero', 'Choose films or television shows for the launcher spotlight.', 'star'),
|
||||
item('metadata-hero', '/admin/metadata-hero', 'Metadata hero', 'Metadata hero', 'Order focused-title information shown above browse rows.', 'tv'),
|
||||
item('playback', '/admin/playback', 'Playback', 'Playback', 'Presentation policy sent with every playback launch.', 'play'),
|
||||
item('subtitles', '/admin/subtitles', 'Subtitles', 'Subtitles', 'Which providers a viewer may fetch a missing subtitle from.', 'captions'),
|
||||
item('credits', '/admin/credits', 'Credits detection', 'Credits detection', 'Control predictive scanning and review completed scans.', 'clock'),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'operations',
|
||||
label: 'Operations',
|
||||
defaultCollapsed: true,
|
||||
id: 'performance', label: 'Performance', defaultCollapsed: false,
|
||||
items: [
|
||||
{
|
||||
id: 'tasks',
|
||||
path: '/admin/tasks',
|
||||
label: 'Scheduled tasks',
|
||||
title: 'Scheduled tasks',
|
||||
intro: 'What the gateway does in the background, when it last ran and whether it worked.',
|
||||
icon: 'clock',
|
||||
},
|
||||
{
|
||||
/* One task, addressed by its id — a page about a single job belongs to the job
|
||||
rather than to the rail, the stance the user and device pages take. */
|
||||
id: 'task',
|
||||
path: '/admin/tasks/:taskId',
|
||||
label: 'Task',
|
||||
title: 'Task',
|
||||
intro: 'One scheduled task: its cadence, its switch and its own run history.',
|
||||
icon: 'clock',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
id: 'imports',
|
||||
path: '/admin/imports',
|
||||
label: 'Imports',
|
||||
title: 'Imports',
|
||||
intro: 'Catalogue synchronisation history.',
|
||||
icon: 'database',
|
||||
},
|
||||
{
|
||||
/* The process, rather than the household. It sits beside Logs and Maintenance
|
||||
because the question it answers — is the container healthy — is the one an
|
||||
operator arrives with when something is slow rather than wrong. */
|
||||
id: 'runtime',
|
||||
path: '/admin/runtime',
|
||||
label: 'Runtime',
|
||||
title: 'Runtime',
|
||||
intro: 'Goroutines, memory and the background workers inside the gateway process.',
|
||||
icon: 'chip',
|
||||
},
|
||||
{
|
||||
id: 'maintenance',
|
||||
path: '/admin/maintenance',
|
||||
label: 'Maintenance',
|
||||
title: 'Maintenance',
|
||||
intro: 'Take Memby offline now or schedule daily quiet time.',
|
||||
icon: 'wrench',
|
||||
},
|
||||
{
|
||||
/* Hidden because its way in is the account menu in the top bar, not the rail:
|
||||
these are settings for the server process rather than for the household, and
|
||||
they belong beside "signed in as". It is still declared here so the page search
|
||||
can find it and the router has one place a page is named. */
|
||||
id: 'gateway-settings',
|
||||
path: '/admin/settings',
|
||||
label: 'Gateway settings',
|
||||
title: 'Gateway settings',
|
||||
intro: 'Timezone, logging and the other server-level settings for this gateway.',
|
||||
icon: 'sliders',
|
||||
hidden: true,
|
||||
},
|
||||
item('runtime', '/admin/runtime', 'Runtime', 'Runtime', 'Goroutines, memory and background workers.', 'chip'),
|
||||
item('logs', '/admin/logs', 'Logs', 'Logs', 'Structured gateway events as they happen.', 'list'),
|
||||
item('updates', '/admin/updates', 'App updates', 'App updates', 'Publish an optional or required client update.', 'upload'),
|
||||
item('tasks', '/admin/tasks', 'Scheduled tasks', 'Scheduled tasks', 'What the gateway does in the background.', 'clock'),
|
||||
item('task', '/admin/tasks/:taskId', 'Task', 'Task', 'One scheduled task and its run history.', 'clock', { hidden: true }),
|
||||
],
|
||||
},
|
||||
{
|
||||
/* External services, and the one place they are configured.
|
||||
*
|
||||
* Its own section rather than an entry under Operations, because the question it
|
||||
* answers — is everything Memby depends on working — is not the same as "what is the
|
||||
* container doing", and because four services' settings previously lived on four
|
||||
* unrelated pages: MDBList under Movie ratings, the *arr switches and request
|
||||
* policies on a page about Discord, and Tracearr nowhere at all. */
|
||||
id: 'integrations',
|
||||
label: 'Integrations',
|
||||
defaultCollapsed: true,
|
||||
id: 'users', label: 'Users', defaultCollapsed: false,
|
||||
items: [
|
||||
{
|
||||
id: 'integrations',
|
||||
path: '/admin/integrations',
|
||||
label: 'Overview',
|
||||
title: 'Integrations',
|
||||
intro: 'Every external service Memby depends on: configured, working, and what it last did.',
|
||||
icon: 'plug',
|
||||
},
|
||||
{
|
||||
/* One service, addressed by its id — a page about a single integration belongs to
|
||||
the integration rather than to the rail, the stance the user, device and task
|
||||
pages take. */
|
||||
id: 'integration',
|
||||
path: '/admin/integrations/:integrationId',
|
||||
label: 'Integration',
|
||||
title: 'Integration',
|
||||
intro: 'One external service: its switch, its settings, its jobs and its run history.',
|
||||
icon: 'plug',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
id: 'webhooks',
|
||||
path: '/admin/integrations/webhooks',
|
||||
label: 'Event webhooks',
|
||||
title: 'Event webhooks',
|
||||
intro: 'Send administrative events to Discord and, in time, elsewhere.',
|
||||
icon: 'send',
|
||||
},
|
||||
item('accounts', '/admin/accounts', 'Users', 'Users', 'Who uses Memby and the devices they are signed in on.', 'people'),
|
||||
item('account', '/admin/accounts/:userId', 'User', 'User', 'Devices, recommendation setup and synced settings for one person.', 'person', { hidden: true }),
|
||||
item('settings-history', '/admin/accounts/:userId/settings', 'Settings history', 'Settings history', 'Changes to one person’s synced settings.', 'sliders', { hidden: true }),
|
||||
item('clients', '/admin/clients', 'Devices', 'Devices', 'Which sets have reported in and what they are running.', 'tv'),
|
||||
item('device', '/admin/devices/:deviceId', 'Device', 'Device', 'One television’s connections and activity.', 'tv', { hidden: true }),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'insights',
|
||||
label: 'Insights',
|
||||
defaultCollapsed: true,
|
||||
id: 'rules', label: 'Rules', defaultCollapsed: false,
|
||||
items: [
|
||||
{
|
||||
id: 'views',
|
||||
path: '/admin/views',
|
||||
label: 'Views',
|
||||
title: 'App views',
|
||||
intro: 'Home-screen visits, viewers and the times Memby is used.',
|
||||
icon: 'overview',
|
||||
},
|
||||
{
|
||||
id: 'searches',
|
||||
path: '/admin/searches',
|
||||
label: 'Searches',
|
||||
title: 'Searches',
|
||||
intro: 'What viewers have been looking for, and what was searched just now.',
|
||||
icon: 'search',
|
||||
},
|
||||
{
|
||||
id: 'journeys',
|
||||
path: '/admin/journeys',
|
||||
label: 'Journeys',
|
||||
title: 'User journeys',
|
||||
intro: 'How viewers move through Memby, use features and complete flows.',
|
||||
icon: 'journey',
|
||||
},
|
||||
{
|
||||
id: 'engagement',
|
||||
path: '/admin/engagement',
|
||||
label: 'Row engagement',
|
||||
title: 'Row engagement',
|
||||
intro: 'Impressions, focus, dwell and selections per launcher row.',
|
||||
icon: 'chart',
|
||||
},
|
||||
item('features', '/admin/features', 'Client configuration', 'Client configuration', 'Control everything the thin TV client renders.', 'sliders'),
|
||||
item('notification-settings', '/admin/notification-settings', 'Notification rules', 'TV notifications', 'Choose where notifications appear and who receives them.', 'bell'),
|
||||
item('webhooks', '/admin/integrations/webhooks', 'Event webhooks', 'Event webhooks', 'Send administrative events to external services.', 'send'),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'violations', label: 'Violations', defaultCollapsed: false, collapsible: false,
|
||||
items: [item('media-reports', '/admin/media-reports', 'Media reports', 'Media reports', 'Problems viewers reported with a film or episode.', 'alert')],
|
||||
},
|
||||
{
|
||||
id: 'settings', label: 'Settings', defaultCollapsed: false,
|
||||
items: [
|
||||
item('integrations', '/admin/integrations', 'Integrations', 'Integrations', 'External services Memby depends on.', 'plug'),
|
||||
item('integration', '/admin/integrations/:integrationId', 'Integration', 'Integration', 'One external service, its settings and run history.', 'plug', { hidden: true }),
|
||||
item('maintenance', '/admin/maintenance', 'Maintenance', 'Maintenance', 'Take Memby offline or schedule quiet time.', 'wrench'),
|
||||
item('gateway-settings', '/admin/settings', 'Gateway settings', 'Gateway settings', 'Timezone, logging and other server-level settings.', 'sliders', { hidden: true }),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const allNavItems: NavItem[] = nav.flatMap((group) => group.items);
|
||||
|
||||
/** searchableNavItems is what the omni search offers: every page that has a URL of its
|
||||
* own, which is every page that is not addressed by an id in the path. That test is the
|
||||
* path rather than `hidden`, because the two answer different questions — a page can be
|
||||
* off the rail (gateway settings is reached from the account menu) and still be a real
|
||||
* address somebody would type. A page about one person or one television is the case
|
||||
* this excludes: without knowing which person, the URL does not exist. */
|
||||
export const searchableNavItems = nav.flatMap((group) =>
|
||||
group.items
|
||||
.filter((item) => !item.path.includes(':'))
|
||||
.map((item) => ({ ...item, group: group.label ?? '' })),
|
||||
.filter((navItem) => !navItem.path.includes(':'))
|
||||
.map((navItem) => ({ ...navItem, group: group.label })),
|
||||
);
|
||||
|
||||
export function navItem(id: string): NavItem | undefined {
|
||||
return allNavItems.find((item) => item.id === id);
|
||||
return allNavItems.find((navItem) => navItem.id === id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user