File size: 6,068 Bytes
2f1e7ff
eb60b6b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2f1e7ff
eb60b6b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2f1e7ff
 
 
eb60b6b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2f1e7ff
eb60b6b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import PersistenceWarning from "./PersistenceWarning";
import { useState } from "react";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import {
  AppShell,
  AppShellMain,
  AppShellSidebar,
  AppShellSidebarContent,
  AppShellSidebarFooter,
  AppShellSidebarHeader,
  Button,
  Field,
  FieldLabel,
  GlobalHeader,
  GlobalHeaderActions,
  GlobalHeaderBrand,
  GlobalHeaderInner,
  Select,
  SideNav,
  SideNavGroup,
  SideNavGroupLabel,
  SideNavItem,
  SideNavLink,
  SideNavList,
  SkipLink,
  StatusIndicator,
} from "lumen-ui-kit";
import { logoutSession } from "../lib/api";
import { useWorkspace } from "../workspaces/WorkspaceProvider";
import { workspacePath, workspaceSection } from "../workspaces/workspaceRoutes";
import CreateWorkspaceDialog from "./CreateWorkspaceDialog";

const groups = [
  { label: "Explore", items: [["", "Overview"], ["graph", "Graph"]] },
  { label: "Memory", items: [["data", "Data"], ["observations", "Observations"], ["vectors", "Vectors"]] },
  { label: "Operations", items: [["tasks", "Tasks"], ["ingest", "Ingest"], ["settings", "Settings"]] },
] as const;

export default function Layout() {
  const navigate = useNavigate();
  const location = useLocation();
  const {
    workspaces,
    activeWorkspace,
    switchWorkspace,
    canManageWorkspaces,
  } = useWorkspace();
  const [createOpen, setCreateOpen] = useState(false);
  const section = workspaceSection(location.pathname);
  const visible = workspaces.filter((workspace) => !workspace.archived);

  async function logout() {
    try { await logoutSession(); }
    finally { navigate("/login"); }
  }

  const navigation = activeWorkspace ? (
    <SideNav expression="compact" aria-label="Console sections">
      <SideNavList>
        {groups.map((group) => (
          <SideNavGroup key={group.label}>
            <SideNavGroupLabel>{group.label}</SideNavGroupLabel>
            <SideNavList>
              {group.items.map(([to, label]) => (
                <SideNavItem key={to}>
                  <SideNavLink
                    href={`/console${workspacePath(activeWorkspace.slug, to)}`}
                    current={section === to}
                    onClick={(event) => {
                      event.preventDefault();
                      navigate(workspacePath(activeWorkspace.slug, to));
                    }}
                  >
                    {label}
                  </SideNavLink>
                </SideNavItem>
              ))}
            </SideNavList>
          </SideNavGroup>
        ))}
      </SideNavList>
    </SideNav>
  ) : null;

  return (
    <div className="flex min-h-screen flex-col bg-lumen-background">
      <PersistenceWarning />
      <SkipLink href="#console-main">Skip to main content</SkipLink>
      <GlobalHeader sticky className="border-b border-lumen-border">
        <GlobalHeaderInner className="max-w-none">
          <GlobalHeaderBrand href="/console/">
            <span className="grid size-8 place-items-center bg-lumen-primary text-xs font-bold text-lumen-on-primary">B</span>
            <span className="font-semibold">BrainAPI</span>
          </GlobalHeaderBrand>
          <div className="mx-4 hidden min-w-0 flex-1 items-center gap-1 overflow-x-auto md:flex" role="tablist" aria-label="Workspaces">
            {visible.map((workspace) => (
              <Button
                key={workspace.slug}
                role="tab"
                aria-selected={workspace.slug === activeWorkspace?.slug}
                variant={workspace.slug === activeWorkspace?.slug ? "primary" : "ghost"}
                size="small"
                className="shrink-0"
                onClick={() => switchWorkspace(workspace.slug)}
                title={workspace.slug}
              >
                {workspace.display_name}
              </Button>
            ))}
            {canManageWorkspaces ? (
              <Button size="small" variant="secondary" className="shrink-0" aria-label="Create workspace" onClick={() => setCreateOpen(true)}>+</Button>
            ) : null}
          </div>
          <div className="mr-2 min-w-0 flex-1 md:hidden">
            {visible.length > 1 ? (
              <Field>
                <FieldLabel htmlFor="mobile-workspace">Workspace</FieldLabel>
                <Select id="mobile-workspace" value={activeWorkspace?.slug ?? ""} onChange={(event) => switchWorkspace(event.target.value)}>
                  {visible.map((workspace) => <option key={workspace.slug} value={workspace.slug}>{workspace.display_name}</option>)}
                </Select>
              </Field>
            ) : <span className="text-sm font-medium">{activeWorkspace?.display_name}</span>}
          </div>
          <GlobalHeaderActions>
            <StatusIndicator status="success" className="hidden sm:inline-flex">Live</StatusIndicator>
            <Button size="small" variant="secondary" onClick={logout}>Log out</Button>
          </GlobalHeaderActions>
        </GlobalHeaderInner>
      </GlobalHeader>
      <AppShell layout="sidebar" className="min-h-0 flex-1 overflow-hidden">
        <AppShellSidebar className="hidden min-h-0 lg:flex">
          <AppShellSidebarHeader>
            <p className="text-xs uppercase text-lumen-muted-foreground">Workspace</p>
            <p className="font-medium">{activeWorkspace?.display_name ?? "None"}</p>
            <p className="font-mono text-xs text-lumen-muted-foreground">{activeWorkspace?.brain_id}</p>
          </AppShellSidebarHeader>
          <AppShellSidebarContent>{navigation}</AppShellSidebarContent>
          <AppShellSidebarFooter className="text-xs text-lumen-muted-foreground">{canManageWorkspaces ? "System credentials" : "Scoped credentials"}</AppShellSidebarFooter>
        </AppShellSidebar>
        <AppShellMain as="main" id="console-main" className="min-h-0 min-w-0 overflow-hidden">
          <Outlet key={activeWorkspace?.brain_id} />
        </AppShellMain>
      </AppShell>
      <CreateWorkspaceDialog open={createOpen} onClose={() => setCreateOpen(false)} />
    </div>
  );
}