@anchrd/intel 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +496 -0
- package/bin/intel.mjs +134 -0
- package/dist/adapters/cloudflare/cloudflare-flow-workflow.d.ts +42 -0
- package/dist/adapters/cloudflare/cloudflare-flow-workflow.js +89 -0
- package/dist/adapters/cloudflare/cloudflare.d.ts +29 -0
- package/dist/adapters/cloudflare/cloudflare.js +416 -0
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +31 -0
- package/dist/adapters/cloudflare/cloudflare.types.js +1 -0
- package/dist/adapters/content/content.d.ts +3 -0
- package/dist/adapters/content/content.js +128 -0
- package/dist/adapters/content/content.types.d.ts +38 -0
- package/dist/adapters/content/content.types.js +1 -0
- package/dist/adapters/db/db-audit.d.ts +62 -0
- package/dist/adapters/db/db-audit.js +150 -0
- package/dist/adapters/db/db-boards.d.ts +6 -0
- package/dist/adapters/db/db-boards.js +324 -0
- package/dist/adapters/db/db-feed.d.ts +30 -0
- package/dist/adapters/db/db-feed.js +276 -0
- package/dist/adapters/db/db-flows.d.ts +42 -0
- package/dist/adapters/db/db-flows.js +1311 -0
- package/dist/adapters/db/db-grants.d.ts +133 -0
- package/dist/adapters/db/db-grants.js +269 -0
- package/dist/adapters/db/db-indexing.d.ts +3 -0
- package/dist/adapters/db/db-indexing.js +200 -0
- package/dist/adapters/db/db-oauth.d.ts +3 -0
- package/dist/adapters/db/db-oauth.js +26 -0
- package/dist/adapters/db/db-prompts.d.ts +46 -0
- package/dist/adapters/db/db-prompts.js +144 -0
- package/dist/adapters/db/db.d.ts +6 -0
- package/dist/adapters/db/db.js +1687 -0
- package/dist/adapters/db/db.types.d.ts +16 -0
- package/dist/adapters/db/db.types.js +1 -0
- package/dist/adapters/document-converter/document-converter.d.ts +2 -0
- package/dist/adapters/document-converter/document-converter.js +26 -0
- package/dist/adapters/document-converter/document-converter.types.d.ts +17 -0
- package/dist/adapters/document-converter/document-converter.types.js +1 -0
- package/dist/adapters/flow-runtime/flow-runtime.d.ts +3 -0
- package/dist/adapters/flow-runtime/flow-runtime.js +22 -0
- package/dist/adapters/flow-runtime/flow-runtime.types.d.ts +19 -0
- package/dist/adapters/flow-runtime/flow-runtime.types.js +1 -0
- package/dist/adapters/index-queue/index-queue.d.ts +10 -0
- package/dist/adapters/index-queue/index-queue.js +12 -0
- package/dist/adapters/index-queue/index-queue.types.d.ts +11 -0
- package/dist/adapters/index-queue/index-queue.types.js +1 -0
- package/dist/adapters/json-schema/json-schema.d.ts +2 -0
- package/dist/adapters/json-schema/json-schema.js +77 -0
- package/dist/adapters/openid/openid.d.ts +5 -0
- package/dist/adapters/openid/openid.js +301 -0
- package/dist/adapters/portal-tokens/portal-tokens.d.ts +8 -0
- package/dist/adapters/portal-tokens/portal-tokens.js +79 -0
- package/dist/adapters/remote-tools/remote-tools.d.ts +5 -0
- package/dist/adapters/remote-tools/remote-tools.js +129 -0
- package/dist/adapters/semantic-index/semantic-index.d.ts +2 -0
- package/dist/adapters/semantic-index/semantic-index.js +139 -0
- package/dist/adapters/semantic-index/semantic-index.types.d.ts +56 -0
- package/dist/adapters/semantic-index/semantic-index.types.js +1 -0
- package/dist/adapters/session-cookie/session-cookie.d.ts +5 -0
- package/dist/adapters/session-cookie/session-cookie.js +87 -0
- package/dist/adapters/tool-source-policy/tool-source-policy.d.ts +1 -0
- package/dist/adapters/tool-source-policy/tool-source-policy.js +22 -0
- package/dist/audit/audit.d.ts +2 -0
- package/dist/audit/audit.js +87 -0
- package/dist/audit/audit.types.d.ts +23 -0
- package/dist/audit/audit.types.js +1 -0
- package/dist/auth/auth.d.ts +2 -0
- package/dist/auth/auth.js +284 -0
- package/dist/auth/auth.types.d.ts +108 -0
- package/dist/auth/auth.types.js +1 -0
- package/dist/boards/boards.d.ts +44 -0
- package/dist/boards/boards.js +447 -0
- package/dist/boards/boards.types.d.ts +117 -0
- package/dist/boards/boards.types.js +1 -0
- package/dist/build/build.d.ts +2 -0
- package/dist/build/build.js +192 -0
- package/dist/build/build.types.d.ts +17 -0
- package/dist/build/build.types.js +1 -0
- package/dist/bundle/bundle.d.ts +4 -0
- package/dist/bundle/bundle.js +1194 -0
- package/dist/bundle/bundle.types.d.ts +33 -0
- package/dist/bundle/bundle.types.js +1 -0
- package/dist/cli/cli.d.ts +4 -0
- package/dist/cli/cli.js +262 -0
- package/dist/cli/cli.types.d.ts +14 -0
- package/dist/cli/cli.types.js +1 -0
- package/dist/contract/audit.d.ts +50 -0
- package/dist/contract/audit.js +72 -0
- package/dist/contract/board.d.ts +150 -0
- package/dist/contract/board.js +262 -0
- package/dist/contract/bundle.d.ts +147 -0
- package/dist/contract/bundle.js +139 -0
- package/dist/contract/contract.d.ts +112 -0
- package/dist/contract/contract.js +143 -0
- package/dist/contract/feed.d.ts +126 -0
- package/dist/contract/feed.js +149 -0
- package/dist/contract/flow-run.d.ts +346 -0
- package/dist/contract/flow-run.js +194 -0
- package/dist/contract/flow.d.ts +1057 -0
- package/dist/contract/flow.js +552 -0
- package/dist/contract/node.d.ts +551 -0
- package/dist/contract/node.js +456 -0
- package/dist/contract/share.d.ts +216 -0
- package/dist/contract/share.js +134 -0
- package/dist/contract/table.d.ts +171 -0
- package/dist/contract/table.js +117 -0
- package/dist/contract/tool.d.ts +122 -0
- package/dist/contract/tool.js +172 -0
- package/dist/feed/feed.d.ts +2 -0
- package/dist/feed/feed.js +77 -0
- package/dist/feed/feed.types.d.ts +23 -0
- package/dist/feed/feed.types.js +1 -0
- package/dist/flows/flows.d.ts +41 -0
- package/dist/flows/flows.js +2216 -0
- package/dist/flows/flows.types.d.ts +339 -0
- package/dist/flows/flows.types.js +1 -0
- package/dist/http/http.d.ts +9 -0
- package/dist/http/http.js +874 -0
- package/dist/http/http.types.d.ts +22 -0
- package/dist/http/http.types.js +1 -0
- package/dist/indexing/indexing.d.ts +6 -0
- package/dist/indexing/indexing.js +239 -0
- package/dist/indexing/indexing.types.d.ts +11 -0
- package/dist/indexing/indexing.types.js +1 -0
- package/dist/intel/intel.d.ts +3 -0
- package/dist/intel/intel.js +194 -0
- package/dist/intel/intel.types.d.ts +24 -0
- package/dist/intel/intel.types.js +1 -0
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +1292 -0
- package/dist/mcp/mcp.types.d.ts +25 -0
- package/dist/mcp/mcp.types.js +1 -0
- package/dist/nodes/document-links/document-links.d.ts +20 -0
- package/dist/nodes/document-links/document-links.js +57 -0
- package/dist/nodes/nodes.d.ts +2 -0
- package/dist/nodes/nodes.js +1667 -0
- package/dist/nodes/nodes.types.d.ts +511 -0
- package/dist/nodes/nodes.types.js +1 -0
- package/dist/prepare/prepare.d.ts +4 -0
- package/dist/prepare/prepare.js +16 -0
- package/dist/prepare/prepare.types.d.ts +9 -0
- package/dist/prepare/prepare.types.js +1 -0
- package/dist/prompts/prompts.d.ts +2 -0
- package/dist/prompts/prompts.js +65 -0
- package/dist/prompts/prompts.types.d.ts +71 -0
- package/dist/prompts/prompts.types.js +1 -0
- package/dist/shared/callers/callers.d.ts +28 -0
- package/dist/shared/callers/callers.js +30 -0
- package/dist/shared/csv/csv.d.ts +13 -0
- package/dist/shared/csv/csv.js +85 -0
- package/dist/shared/document-text/document-text.d.ts +21 -0
- package/dist/shared/document-text/document-text.js +31 -0
- package/dist/shared/gate-authorization/gate-authorization.d.ts +5 -0
- package/dist/shared/gate-authorization/gate-authorization.js +26 -0
- package/dist/shared/grant-expiry/grant-expiry.d.ts +19 -0
- package/dist/shared/grant-expiry/grant-expiry.js +26 -0
- package/dist/shared/intel-error/intel-error.d.ts +22 -0
- package/dist/shared/intel-error/intel-error.js +27 -0
- package/dist/shared/plain-title/plain-title.d.ts +16 -0
- package/dist/shared/plain-title/plain-title.js +45 -0
- package/dist/shared/problem-details/problem-details.d.ts +30 -0
- package/dist/shared/problem-details/problem-details.js +18 -0
- package/dist/shared/report-unexpected-error/report-unexpected-error.d.ts +1 -0
- package/dist/shared/report-unexpected-error/report-unexpected-error.js +37 -0
- package/dist/shared/safe-return-path/safe-return-path.d.ts +2 -0
- package/dist/shared/safe-return-path/safe-return-path.js +11 -0
- package/dist/shared/sha256/sha256.d.ts +2 -0
- package/dist/shared/sha256/sha256.js +14 -0
- package/dist/tools/tool-servers/tool-servers.d.ts +46 -0
- package/dist/tools/tool-servers/tool-servers.js +177 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +207 -0
- package/dist/tools/tools.types.d.ts +56 -0
- package/dist/tools/tools.types.js +1 -0
- package/examples/branding/favicon.svg +5 -0
- package/examples/branding/logo.svg +5 -0
- package/examples/branding/theme.css +16 -0
- package/examples/dev.vars.example +7 -0
- package/examples/intel.json +9 -0
- package/migrations/0000_intel_foundation.sql +203 -0
- package/migrations/0001_portal_native_tools.sql +21 -0
- package/migrations/0002_flows_in_the_knowledge_tree.sql +34 -0
- package/migrations/0003_folder_permissions.sql +211 -0
- package/migrations/0004_subflow_runs.sql +14 -0
- package/migrations/0005_flow_node_cleanup.sql +28 -0
- package/migrations/0005_tables_in_the_knowledge_tree.sql +68 -0
- package/migrations/0006_links_are_written_in_the_text.sql +20 -0
- package/migrations/0007_no_node_waits.sql +69 -0
- package/migrations/0008_three_layers.sql +130 -0
- package/migrations/0009_no_context_policy.sql +48 -0
- package/migrations/0010_no_run_waits_a_year.sql +27 -0
- package/migrations/0011_one_name_for_the_tree.sql +53 -0
- package/migrations/0012_table_snapshots.sql +29 -0
- package/migrations/0017_a_vector_per_card.sql +38 -0
- package/migrations/0018_no_context_policy_at_last.sql +97 -0
- package/migrations/0019_one_name_for_the_grants.sql +52 -0
- package/migrations/0020_cascade_purge_replay.sql +11 -0
- package/migrations/0021_a_flow_carries_its_own_grant.sql +44 -0
- package/migrations/0022_a_cursor_over_the_journal.sql +14 -0
- package/migrations/0023_a_board_and_its_tasks.sql +116 -0
- package/migrations/0024_the_archive_is_a_column.sql +33 -0
- package/migrations/0025_archiving_takes_the_subtree.sql +29 -0
- package/migrations/0026_one_feed_over_two_kinds.sql +23 -0
- package/migrations/0027_the_runs_of_every_flow.sql +27 -0
- package/migrations/0028_a_prompt_name_over_two_kinds.sql +70 -0
- package/package.json +118 -0
- package/src/contract/CLAUDE.md +152 -0
- package/src/contract/README.md +102 -0
- package/src/contract/audit.ts +85 -0
- package/src/contract/board.ts +306 -0
- package/src/contract/bundle.ts +157 -0
- package/src/contract/contract.ts +161 -0
- package/src/contract/feed.ts +165 -0
- package/src/contract/flow-run.ts +241 -0
- package/src/contract/flow.ts +675 -0
- package/src/contract/node.ts +583 -0
- package/src/contract/share.ts +196 -0
- package/src/contract/table.ts +178 -0
- package/src/contract/tool.ts +202 -0
- package/ui/README.md +110 -0
- package/ui/components.json +21 -0
- package/ui/index.html +13 -0
- package/ui/package.json +11 -0
- package/ui/scripts/lint-tokens.mjs +187 -0
- package/ui/src/access-summary/access-summary.tsx +385 -0
- package/ui/src/app/action-slot/action-slot.tsx +27 -0
- package/ui/src/app/app-sidebar/app-sidebar.tsx +71 -0
- package/ui/src/app/app-tree/app-tree.tsx +1175 -0
- package/ui/src/app/app.tsx +87 -0
- package/ui/src/app/header-search/header-search.tsx +294 -0
- package/ui/src/app/reindex-dialog/reindex-dialog.tsx +79 -0
- package/ui/src/app/settings-dialog/settings-dialog.tsx +135 -0
- package/ui/src/app/sidebar-preferences/sidebar-preferences.ts +68 -0
- package/ui/src/app/sidebar-preferences/sidebar-preferences.types.ts +15 -0
- package/ui/src/app/sidebar-resize-handle/sidebar-resize-handle.tsx +86 -0
- package/ui/src/app/tree-expansion/tree-expansion.ts +107 -0
- package/ui/src/app/tree-expansion/tree-expansion.types.ts +31 -0
- package/ui/src/app/tree-move/tree-move.tsx +427 -0
- package/ui/src/app/tree-reveal/tree-reveal.ts +64 -0
- package/ui/src/app/tree-reveal/tree-reveal.types.ts +16 -0
- package/ui/src/app/user-footer/user-footer.tsx +188 -0
- package/ui/src/app/view-menu/view-menu.tsx +115 -0
- package/ui/src/app-root/app-root.tsx +109 -0
- package/ui/src/app-root/app-root.types.ts +27 -0
- package/ui/src/archive/archive.tsx +417 -0
- package/ui/src/attachment-detail/attachment-detail.ts +52 -0
- package/ui/src/attachment-viewer/attachment-viewer.tsx +166 -0
- package/ui/src/blocknote-view/blocknote-view.tsx +30 -0
- package/ui/src/board/board-assignee/board-assignee-picker.tsx +132 -0
- package/ui/src/board/board-assignee/board-assignee.ts +48 -0
- package/ui/src/board/board-assignee/board-assignee.tsx +75 -0
- package/ui/src/board/board-chip/board-chip.tsx +46 -0
- package/ui/src/board/board-crumbs/board-crumbs.ts +56 -0
- package/ui/src/board/board-crumbs/board-crumbs.tsx +111 -0
- package/ui/src/board/board-data/board-data.ts +195 -0
- package/ui/src/board/board-data/board-data.types.ts +38 -0
- package/ui/src/board/board-dates/board-dates.ts +40 -0
- package/ui/src/board/board-dates/board-dates.tsx +105 -0
- package/ui/src/board/board-kanban/board-kanban.ts +162 -0
- package/ui/src/board/board-kanban/board-kanban.tsx +680 -0
- package/ui/src/board/board-open-task/board-open-task.ts +32 -0
- package/ui/src/board/board-panel/board-panel.tsx +259 -0
- package/ui/src/board/board-settings/board-settings.tsx +226 -0
- package/ui/src/board/board-status/board-status.tsx +60 -0
- package/ui/src/board/board-stripes/board-stripes.ts +128 -0
- package/ui/src/board/board-table/board-table.ts +53 -0
- package/ui/src/board/board-table/board-table.tsx +610 -0
- package/ui/src/board/board-task/board-task.ts +108 -0
- package/ui/src/board/board-task/board-task.tsx +335 -0
- package/ui/src/board/board-title-row/board-title-row.tsx +66 -0
- package/ui/src/branding/branding.tsx +17 -0
- package/ui/src/branding/custom-logo.ts +1 -0
- package/ui/src/branding/favicon.default.svg +5 -0
- package/ui/src/branding/favicon.svg +5 -0
- package/ui/src/components/ui/avatar.tsx +39 -0
- package/ui/src/components/ui/button.tsx +64 -0
- package/ui/src/components/ui/collapsible.tsx +20 -0
- package/ui/src/components/ui/command.tsx +160 -0
- package/ui/src/components/ui/dialog.tsx +143 -0
- package/ui/src/components/ui/dropdown-menu.tsx +187 -0
- package/ui/src/components/ui/input.tsx +21 -0
- package/ui/src/components/ui/popover.tsx +41 -0
- package/ui/src/components/ui/select.tsx +163 -0
- package/ui/src/components/ui/separator.tsx +26 -0
- package/ui/src/components/ui/sheet.tsx +136 -0
- package/ui/src/components/ui/sidebar.tsx +693 -0
- package/ui/src/components/ui/skeleton.tsx +13 -0
- package/ui/src/components/ui/switch.tsx +25 -0
- package/ui/src/components/ui/table.tsx +82 -0
- package/ui/src/components/ui/tabs.tsx +52 -0
- package/ui/src/components/ui/tooltip.tsx +51 -0
- package/ui/src/data/intel-data-provider/intel-data-provider.ts +685 -0
- package/ui/src/data/intel-data-provider/intel-data-provider.types.ts +266 -0
- package/ui/src/data/query-client.ts +18 -0
- package/ui/src/data/request-refusal/refusal-notice.tsx +36 -0
- package/ui/src/data/request-refusal/request-refusal.ts +51 -0
- package/ui/src/data/sign-in/sign-in.ts +97 -0
- package/ui/src/data/sign-in/sign-in.types.ts +15 -0
- package/ui/src/document-link/document-link.tsx +114 -0
- package/ui/src/document-markdown/document-markdown.ts +195 -0
- package/ui/src/editor-schema/editor-schema.ts +23 -0
- package/ui/src/entry-picker/entry-picker.tsx +238 -0
- package/ui/src/feed/feed.tsx +344 -0
- package/ui/src/file-preview/file-preview-view.tsx +496 -0
- package/ui/src/file-preview/file-preview.tsx +28 -0
- package/ui/src/file-preview/pdf-file-preview.tsx +5 -0
- package/ui/src/file-preview/presentation-file-preview.tsx +21 -0
- package/ui/src/file-preview/spreadsheet-file-preview.tsx +5 -0
- package/ui/src/file-preview/word-file-preview.tsx +5 -0
- package/ui/src/flow-runs/flow-runs.tsx +238 -0
- package/ui/src/flows/flows.tsx +1646 -0
- package/ui/src/flows/node-icon/node-icon.ts +34 -0
- package/ui/src/flows/node-palette/node-palette.tsx +214 -0
- package/ui/src/flows/node-palette/node-palette.types.ts +15 -0
- package/ui/src/folder-contents/folder-contents.tsx +321 -0
- package/ui/src/folder-index/folder-index.ts +55 -0
- package/ui/src/frontmatter/frontmatter.tsx +193 -0
- package/ui/src/graph-pane/graph-pane.tsx +53 -0
- package/ui/src/hooks/use-mobile.ts +19 -0
- package/ui/src/i18n/custom.json +4 -0
- package/ui/src/i18n/de.json +609 -0
- package/ui/src/i18n/en.json +609 -0
- package/ui/src/i18n/es.json +609 -0
- package/ui/src/i18n/i18n-context.tsx +65 -0
- package/ui/src/i18n/i18n-languages/i18n-languages.ts +83 -0
- package/ui/src/i18n/i18n.ts +32 -0
- package/ui/src/i18n/i18n.types.ts +49 -0
- package/ui/src/kind-icon.ts +97 -0
- package/ui/src/lib/utils.ts +6 -0
- package/ui/src/main.tsx +68 -0
- package/ui/src/modal/modal.tsx +53 -0
- package/ui/src/node-details/node-details.tsx +545 -0
- package/ui/src/node-editor/node-editor.tsx +304 -0
- package/ui/src/node-graph/graph-notice.tsx +21 -0
- package/ui/src/node-graph/node-graph.ts +214 -0
- package/ui/src/node-graph/node-graph.tsx +295 -0
- package/ui/src/node-import/node-import.tsx +418 -0
- package/ui/src/node-save-error.ts +46 -0
- package/ui/src/node-source/node-source.tsx +128 -0
- package/ui/src/node-table/node-table.tsx +107 -0
- package/ui/src/nodes/nodes.tsx +240 -0
- package/ui/src/rename-resource/rename-resource.ts +80 -0
- package/ui/src/resolved-names/resolved-names.tsx +67 -0
- package/ui/src/resource-error.ts +33 -0
- package/ui/src/resource-menu/resource-menu.tsx +1156 -0
- package/ui/src/router/router-context.ts +6 -0
- package/ui/src/router/router.tsx +67 -0
- package/ui/src/router/router.types.ts +8 -0
- package/ui/src/router/selection-search.ts +88 -0
- package/ui/src/save-button/save-button.tsx +94 -0
- package/ui/src/section-hint/section-hint.tsx +40 -0
- package/ui/src/sign-in-refused/sign-in-refused.tsx +35 -0
- package/ui/src/styles.css +189 -0
- package/ui/src/table-columns/table-columns.tsx +379 -0
- package/ui/src/theme/custom.css +1 -0
- package/ui/src/theme/theme-context.tsx +89 -0
- package/ui/src/theme/theme.ts +70 -0
- package/ui/src/time/relative-time.tsx +41 -0
- package/ui/src/time/time-context.tsx +78 -0
- package/ui/src/time/time.ts +165 -0
- package/ui/src/title-row/title-row.tsx +185 -0
- package/ui/src/tools/tools.tsx +546 -0
- package/ui/src/user-name/user-name.ts +61 -0
- package/ui/src/verbatim/verbatim.tsx +68 -0
- package/ui/tsconfig.json +17 -0
- package/ui/vite.config.ts +129 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import type { ResourceGrant } from "@contract/share.ts";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { useId, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { AppLogo } from "@/branding/branding.tsx";
|
|
6
|
+
import {
|
|
7
|
+
Tooltip,
|
|
8
|
+
TooltipContent,
|
|
9
|
+
TooltipProvider,
|
|
10
|
+
TooltipTrigger,
|
|
11
|
+
} from "@/components/ui/tooltip.tsx";
|
|
12
|
+
import type { IntelDataProvider } from "@/data/intel-data-provider/intel-data-provider.types.ts";
|
|
13
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
14
|
+
import { ResolvedNamesProvider, useResolvedName } from "@/resolved-names/resolved-names.tsx";
|
|
15
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
16
|
+
import { initials, useUserName } from "@/user-name/user-name.ts";
|
|
17
|
+
|
|
18
|
+
type Principal = ResourceGrant["principal"];
|
|
19
|
+
|
|
20
|
+
type GrantGroup = {
|
|
21
|
+
key: string;
|
|
22
|
+
principal: Principal;
|
|
23
|
+
grants: ResourceGrant[];
|
|
24
|
+
owner: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function principalKey(principal: Principal): string {
|
|
28
|
+
if (principal.type === "organization") return "organization";
|
|
29
|
+
if (principal.type === "email") return `email:${principal.email.toLowerCase()}`;
|
|
30
|
+
return `user:${principal.id}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function groupGrants(grants: ResourceGrant[], ownerIds: string[] = []): GrantGroup[] {
|
|
34
|
+
const grouped = new Map<string, GrantGroup>();
|
|
35
|
+
for (const ownerId of ownerIds) {
|
|
36
|
+
grouped.set(`user:${ownerId}`, {
|
|
37
|
+
key: `user:${ownerId}`,
|
|
38
|
+
principal: { type: "user", id: ownerId },
|
|
39
|
+
grants: [],
|
|
40
|
+
owner: true,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
for (const grant of grants) {
|
|
44
|
+
const key = principalKey(grant.principal);
|
|
45
|
+
const group = grouped.get(key);
|
|
46
|
+
if (group) group.grants.push(grant);
|
|
47
|
+
else grouped.set(key, { key, principal: grant.principal, grants: [grant], owner: false });
|
|
48
|
+
}
|
|
49
|
+
return [...grouped.values()];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What to call whoever a grant is for.
|
|
54
|
+
*
|
|
55
|
+
* ⚠️ **TWO sources for a user, and the order matters** — the same rule `useAssigneeLabel` follows,
|
|
56
|
+
* because a person must not be called one thing on a card and another on the share screen.
|
|
57
|
+
* `useUserName` answers for the reader themselves out of the session the shell already holds,
|
|
58
|
+
* without a request; the resolved map answers for everybody else. Asking the map first would make
|
|
59
|
+
* the reader's own name depend on a round trip that has no reason to exist, and would leave them
|
|
60
|
+
* nameless wherever no provider stands.
|
|
61
|
+
*
|
|
62
|
+
* ⚠️ An `email` principal is NOT resolved to a name. The address is what was granted, and it is
|
|
63
|
+
* what can be withdrawn — a name over it would hide which of two spellings the grant actually
|
|
64
|
+
* carries (#431).
|
|
65
|
+
*/
|
|
66
|
+
function usePrincipalLabel(principal: Principal): string {
|
|
67
|
+
const i18n = useI18n();
|
|
68
|
+
const userId = principal.type === "user" ? principal.id : null;
|
|
69
|
+
const own = useUserName(userId);
|
|
70
|
+
const resolved = useResolvedName(userId);
|
|
71
|
+
if (principal.type === "organization") return i18n.t("node.organization");
|
|
72
|
+
if (principal.type === "email") return principal.email;
|
|
73
|
+
// ⚠️ The fallback stays what it was: a person nobody can name is "a user account", never the
|
|
74
|
+
// principal id the row carries (#258).
|
|
75
|
+
return own ?? resolved ?? i18n.t("node.someUser");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const circleTones = [
|
|
79
|
+
"bg-muted text-muted-foreground",
|
|
80
|
+
"bg-accent text-accent-foreground",
|
|
81
|
+
"bg-secondary text-secondary-foreground",
|
|
82
|
+
] as const;
|
|
83
|
+
|
|
84
|
+
function AccessCircle({
|
|
85
|
+
group,
|
|
86
|
+
position,
|
|
87
|
+
count,
|
|
88
|
+
expanded,
|
|
89
|
+
controls,
|
|
90
|
+
activate,
|
|
91
|
+
}: {
|
|
92
|
+
group: GrantGroup;
|
|
93
|
+
position: number;
|
|
94
|
+
count: number;
|
|
95
|
+
expanded: boolean;
|
|
96
|
+
controls: string;
|
|
97
|
+
activate(element: HTMLButtonElement): void;
|
|
98
|
+
}) {
|
|
99
|
+
const i18n = useI18n();
|
|
100
|
+
const label = usePrincipalLabel(group.principal);
|
|
101
|
+
const circle = (
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
aria-label={position === 0 ? `${i18n.t("node.accessDetails", { count })}: ${label}` : label}
|
|
105
|
+
aria-expanded={expanded}
|
|
106
|
+
aria-controls={controls}
|
|
107
|
+
data-organization-access={group.principal.type === "organization" ? "" : undefined}
|
|
108
|
+
onClick={(event) => activate(event.currentTarget)}
|
|
109
|
+
className={`relative flex size-7 items-center justify-center rounded-full text-xs font-medium ring-2 ring-background outline-none transition hover:z-10 hover:scale-105 focus-visible:z-10 focus-visible:ring-ring ${circleTones[position % circleTones.length]}`}
|
|
110
|
+
>
|
|
111
|
+
{group.principal.type === "organization" ? (
|
|
112
|
+
<AppLogo className="size-4 rounded-none bg-transparent text-current [&_svg]:size-3" />
|
|
113
|
+
) : (
|
|
114
|
+
initials(label)
|
|
115
|
+
)}
|
|
116
|
+
</button>
|
|
117
|
+
);
|
|
118
|
+
return (
|
|
119
|
+
<TooltipProvider delayDuration={300}>
|
|
120
|
+
<Tooltip>
|
|
121
|
+
<TooltipTrigger asChild>{circle}</TooltipTrigger>
|
|
122
|
+
<TooltipContent>
|
|
123
|
+
{group.principal.type === "organization" ? i18n.t("node.organizationAccess") : label}
|
|
124
|
+
</TooltipContent>
|
|
125
|
+
</Tooltip>
|
|
126
|
+
</TooltipProvider>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function AccessDetail({
|
|
131
|
+
group,
|
|
132
|
+
revocableOn,
|
|
133
|
+
sources,
|
|
134
|
+
onRevoke,
|
|
135
|
+
}: {
|
|
136
|
+
group: GrantGroup;
|
|
137
|
+
// ⚠️ The node whose grants this screen may withdraw. A grant that sits on a folder ABOVE is in
|
|
138
|
+
// force here but is not this screen's to take away — the reader may not even be allowed to manage
|
|
139
|
+
// that folder. It is shown, and it is shown as inherited, and no button is offered for it (#803).
|
|
140
|
+
revocableOn?: string;
|
|
141
|
+
// The folders the inherited grants sit on, for the ones this reader may see. A grant whose folder
|
|
142
|
+
// is missing here is still shown as inherited — just without a name to go to.
|
|
143
|
+
sources?: ReadonlyArray<{ resourceId: string; title: string }>;
|
|
144
|
+
onRevoke?(grantId: string): void;
|
|
145
|
+
}) {
|
|
146
|
+
const i18n = useI18n();
|
|
147
|
+
const label = usePrincipalLabel(group.principal);
|
|
148
|
+
const inherited = group.grants.filter((grant) => grant.resourceId !== revocableOn);
|
|
149
|
+
const here = group.grants.filter((grant) => grant.resourceId === revocableOn);
|
|
150
|
+
const from = [
|
|
151
|
+
...new Set(
|
|
152
|
+
inherited.flatMap((grant) => {
|
|
153
|
+
const source = sources?.find((entry) => entry.resourceId === grant.resourceId);
|
|
154
|
+
return source ? [source.title] : [];
|
|
155
|
+
}),
|
|
156
|
+
),
|
|
157
|
+
];
|
|
158
|
+
return (
|
|
159
|
+
<li className="flex items-start justify-between gap-3 text-sm">
|
|
160
|
+
<span className="min-w-0">
|
|
161
|
+
<span className="block truncate font-medium">{label}</span>
|
|
162
|
+
<span className="text-xs text-muted-foreground">
|
|
163
|
+
{[
|
|
164
|
+
...(group.owner ? [i18n.t("node.owner")] : []),
|
|
165
|
+
...group.grants.map((grant) => i18n.t(`node.verb.${grant.verb}`)),
|
|
166
|
+
].join(", ")}
|
|
167
|
+
</span>
|
|
168
|
+
{/* Said once for the whole person, not once per verb: three inherited verbs are one fact
|
|
169
|
+
about where the access lives, and three copies of it would read as three problems. */}
|
|
170
|
+
{revocableOn !== undefined && inherited.length > 0 ? (
|
|
171
|
+
<span className="mt-0.5 block text-xs text-muted-foreground">
|
|
172
|
+
{from.length > 0
|
|
173
|
+
? i18n.t("node.accessInheritedFrom", { titles: from.join(", ") })
|
|
174
|
+
: i18n.t("node.accessInherited")}
|
|
175
|
+
</span>
|
|
176
|
+
) : null}
|
|
177
|
+
</span>
|
|
178
|
+
{onRevoke && here.length > 0 ? (
|
|
179
|
+
<span className="flex flex-wrap justify-end gap-1">
|
|
180
|
+
{here.map((grant) => (
|
|
181
|
+
<button
|
|
182
|
+
key={grant.id}
|
|
183
|
+
type="button"
|
|
184
|
+
onClick={() => onRevoke(grant.id)}
|
|
185
|
+
aria-label={i18n.t("node.revokeVerbFor", {
|
|
186
|
+
verb: i18n.t(`node.verb.${grant.verb}`),
|
|
187
|
+
recipient: label,
|
|
188
|
+
})}
|
|
189
|
+
className="rounded px-2 py-1 text-xs text-destructive outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
190
|
+
>
|
|
191
|
+
{i18n.t("node.revokeVerb", { verb: i18n.t(`node.verb.${grant.verb}`) })}
|
|
192
|
+
</button>
|
|
193
|
+
))}
|
|
194
|
+
</span>
|
|
195
|
+
) : null}
|
|
196
|
+
</li>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function AccessSummary({
|
|
201
|
+
grants,
|
|
202
|
+
ownerId,
|
|
203
|
+
ownerIds,
|
|
204
|
+
revocableOn,
|
|
205
|
+
sources,
|
|
206
|
+
onRevoke,
|
|
207
|
+
}: {
|
|
208
|
+
grants: ResourceGrant[];
|
|
209
|
+
ownerId?: string;
|
|
210
|
+
ownerIds?: string[];
|
|
211
|
+
sources?: ReadonlyArray<{ resourceId: string; title: string }>;
|
|
212
|
+
// Where withdrawal is possible — see `AccessDetail`. Absent on the read-only summary, where no
|
|
213
|
+
// row carries a button and the distinction has nothing to change.
|
|
214
|
+
revocableOn?: string;
|
|
215
|
+
onRevoke?(grantId: string): void;
|
|
216
|
+
}) {
|
|
217
|
+
const i18n = useI18n();
|
|
218
|
+
const groups = groupGrants(grants, ownerIds ?? (ownerId ? [ownerId] : []));
|
|
219
|
+
const [open, setOpen] = useState(false);
|
|
220
|
+
const [position, setPosition] = useState({ top: 8, left: 8 });
|
|
221
|
+
const detailsId = useId();
|
|
222
|
+
const organizationDescriptionId = useId();
|
|
223
|
+
if (groups.length === 0) return null;
|
|
224
|
+
const visible = groups.slice(0, 3);
|
|
225
|
+
const overflow = groups.length - visible.length;
|
|
226
|
+
const toggle = (element: HTMLButtonElement) => {
|
|
227
|
+
setOpen((current) => {
|
|
228
|
+
if (!current) {
|
|
229
|
+
const rect = element.getBoundingClientRect();
|
|
230
|
+
const panelHeight = Math.min(320, window.innerHeight - 16);
|
|
231
|
+
const below = window.innerHeight - rect.bottom;
|
|
232
|
+
setPosition({
|
|
233
|
+
top: below >= panelHeight ? rect.bottom + 4 : Math.max(8, rect.top - panelHeight - 4),
|
|
234
|
+
left: Math.max(8, Math.min(rect.left, window.innerWidth - 328)),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return !current;
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// ⚠️ ONE request for the whole summary, not one per circle: every person this component draws is
|
|
242
|
+
// asked for together, and `usePrincipalLabel` reads the answer out of the context rather than
|
|
243
|
+
// fetching. The circles and the detail list share the provider, so opening the panel adds no
|
|
244
|
+
// request (#803).
|
|
245
|
+
//
|
|
246
|
+
// ⚠️ Only `user` principals travel. An organization has no name to resolve, and an `email` grant
|
|
247
|
+
// is shown as the address it was granted to — resolving it would hide which spelling is in force.
|
|
248
|
+
const personIds = groups.flatMap((group) =>
|
|
249
|
+
group.principal.type === "user" ? [group.principal.id] : [],
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<ResolvedNamesProvider ids={personIds}>
|
|
254
|
+
<div className="relative flex w-fit items-center -space-x-2" data-access-summary="">
|
|
255
|
+
{visible.map((group, position) => (
|
|
256
|
+
<AccessCircle
|
|
257
|
+
key={group.key}
|
|
258
|
+
group={group}
|
|
259
|
+
position={position}
|
|
260
|
+
count={groups.length}
|
|
261
|
+
expanded={open}
|
|
262
|
+
controls={detailsId}
|
|
263
|
+
activate={toggle}
|
|
264
|
+
/>
|
|
265
|
+
))}
|
|
266
|
+
{overflow > 0 ? (
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
aria-label={i18n.t("node.accessDetails", { count: groups.length })}
|
|
270
|
+
aria-expanded={open}
|
|
271
|
+
aria-controls={detailsId}
|
|
272
|
+
onClick={(event) => toggle(event.currentTarget)}
|
|
273
|
+
className="relative flex size-7 items-center justify-center rounded-full bg-muted text-xs font-medium text-muted-foreground ring-2 ring-background outline-none transition hover:z-10 hover:scale-105 focus-visible:z-10 focus-visible:ring-ring"
|
|
274
|
+
>
|
|
275
|
+
+{overflow}
|
|
276
|
+
</button>
|
|
277
|
+
) : null}
|
|
278
|
+
{groups.some((group) => group.principal.type === "organization") ? (
|
|
279
|
+
<span id={organizationDescriptionId} className="sr-only">
|
|
280
|
+
{i18n.t("node.organizationAccess")}
|
|
281
|
+
</span>
|
|
282
|
+
) : null}
|
|
283
|
+
{open
|
|
284
|
+
? createPortal(
|
|
285
|
+
<div
|
|
286
|
+
id={detailsId}
|
|
287
|
+
data-access-details=""
|
|
288
|
+
className="fixed z-50 max-h-[calc(100dvh-1rem)] w-80 overflow-y-auto rounded-md border bg-popover p-4 text-popover-foreground shadow-md"
|
|
289
|
+
style={position}
|
|
290
|
+
>
|
|
291
|
+
<ul className="space-y-3">
|
|
292
|
+
{groups.map((group) => (
|
|
293
|
+
<AccessDetail
|
|
294
|
+
key={group.key}
|
|
295
|
+
group={group}
|
|
296
|
+
{...(revocableOn !== undefined ? { revocableOn } : {})}
|
|
297
|
+
{...(sources ? { sources } : {})}
|
|
298
|
+
{...(onRevoke ? { onRevoke } : {})}
|
|
299
|
+
/>
|
|
300
|
+
))}
|
|
301
|
+
</ul>
|
|
302
|
+
</div>,
|
|
303
|
+
document.querySelector('[role="dialog"]') ?? document.body,
|
|
304
|
+
)
|
|
305
|
+
: null}
|
|
306
|
+
</div>
|
|
307
|
+
</ResolvedNamesProvider>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* The ONE definition of the effective-access query (#817).
|
|
313
|
+
*
|
|
314
|
+
* ⚠️ It is a function rather than a call so that a second reader can watch the same answers without
|
|
315
|
+
* asking for them a second time. A folder table waits until every one of its rows has been decided
|
|
316
|
+
* before it draws itself, and it does that by observing these exact keys — same key, same cache
|
|
317
|
+
* entry, one request per resource. A copy of the options over there would share the key and drift
|
|
318
|
+
* on everything else, which is the quietest way to end up with two different `retry` settings.
|
|
319
|
+
*
|
|
320
|
+
* ⚠️ Both kinds share the `effective-node-grants` prefix on purpose: a share mutation invalidates
|
|
321
|
+
* that prefix, and a flow grant changes what a folder summary above it shows just as a folder grant
|
|
322
|
+
* changes what the flow below it shows. Two prefixes would mean one of the two keeps drawing an
|
|
323
|
+
* access picture that is no longer in force.
|
|
324
|
+
*
|
|
325
|
+
* ⚠️ `retry: false` is part of the contract this query has with its readers, not a tuning knob. A
|
|
326
|
+
* refusal here is an answer (#484), and a reader waiting for "every row decided" needs the failing
|
|
327
|
+
* ones to arrive as failures rather than to be tried again behind its back.
|
|
328
|
+
*
|
|
329
|
+
* ⚠️ `retryOnMount: false` says the same thing about a SECOND reader, and without it the two
|
|
330
|
+
* readers feed each other. `staleTime` governs data, not errors, so a failed query is refetched
|
|
331
|
+
* every time a fresh observer mounts on it — and the folder table mounts one the instant the
|
|
332
|
+
* failure lets it draw its rows. Measured (#817): error, rows, mount, refetch, pending, rows gone,
|
|
333
|
+
* unmount, error again — 285 transitions and still going, none of them visible as anything but a
|
|
334
|
+
* table that would not appear. A failure that is final for the first reader is final for the next.
|
|
335
|
+
*/
|
|
336
|
+
export function effectiveAccessQuery(
|
|
337
|
+
data: IntelDataProvider,
|
|
338
|
+
input: { resourceId: string; kind: "node" | "flow" },
|
|
339
|
+
) {
|
|
340
|
+
return {
|
|
341
|
+
queryKey: ["effective-node-grants", input.kind, input.resourceId],
|
|
342
|
+
queryFn: () =>
|
|
343
|
+
input.kind === "flow"
|
|
344
|
+
? data.listEffectiveFlowAccess(input.resourceId)
|
|
345
|
+
: data.listEffectiveAccess(input.resourceId),
|
|
346
|
+
retry: false,
|
|
347
|
+
retryOnMount: false,
|
|
348
|
+
// Time cannot change this answer locally. Share mutations invalidate the whole effective prefix
|
|
349
|
+
// because changing one folder also changes every descendant summary already on screen.
|
|
350
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* The compact access view for a resource the reader can manage.
|
|
356
|
+
*
|
|
357
|
+
* `node_grant_list` is itself authorized: a reader who may see the node but may not inspect its
|
|
358
|
+
* grants gets no names here. Treating that refusal as an empty summary is deliberate; rendering an
|
|
359
|
+
* error or retry would disclose that there are grants to ask about (#484).
|
|
360
|
+
*/
|
|
361
|
+
export function ResourceAccessSummary({
|
|
362
|
+
resourceId,
|
|
363
|
+
ownerId,
|
|
364
|
+
kind = "node",
|
|
365
|
+
}: {
|
|
366
|
+
resourceId: string;
|
|
367
|
+
ownerId: string;
|
|
368
|
+
// Which side of the tree the id belongs to (#530). A flow is not a node, so it is a different
|
|
369
|
+
// route — but the same answer, which is why one component draws both.
|
|
370
|
+
kind?: "node" | "flow";
|
|
371
|
+
}) {
|
|
372
|
+
const { data } = useIntelRouterContext();
|
|
373
|
+
const grants = useQuery(effectiveAccessQuery(data, { resourceId, kind }));
|
|
374
|
+
if (!grants.data) return null;
|
|
375
|
+
return (
|
|
376
|
+
<AccessSummary
|
|
377
|
+
grants={grants.data.items}
|
|
378
|
+
ownerIds={
|
|
379
|
+
grants.data.ownerIds.includes(ownerId)
|
|
380
|
+
? grants.data.ownerIds
|
|
381
|
+
: [ownerId, ...grants.data.ownerIds]
|
|
382
|
+
}
|
|
383
|
+
/>
|
|
384
|
+
);
|
|
385
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
|
|
5
|
+
// A bar owns a place for actions, and whoever owns the action renders into it from wherever its
|
|
6
|
+
// state happens to live. Two such places exist: `header-actions` in the shell, beside the
|
|
7
|
+
// breadcrumb, for what belongs to the area; and `title-actions` in the title line of an open
|
|
8
|
+
// document, for what belongs to that one document (#24).
|
|
9
|
+
//
|
|
10
|
+
// ⚠️ The slot lives above the component that fills it, so it is not in the document while that
|
|
11
|
+
// component first renders. Looking it up in an effect costs one extra render and is the only order
|
|
12
|
+
// that works; reading it during render finds nothing on the first paint.
|
|
13
|
+
//
|
|
14
|
+
// ⚠️ A portal only ever appends, so the slot decides where its content sits and mount order decides
|
|
15
|
+
// nothing. That is why the shell gives the screens a box of their own next to the search rather
|
|
16
|
+
// than one shared list (see `app.tsx`, #56).
|
|
17
|
+
export function ActionSlot({
|
|
18
|
+
name = "header-actions",
|
|
19
|
+
children,
|
|
20
|
+
}: {
|
|
21
|
+
name?: string;
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
}) {
|
|
24
|
+
const [slot, setSlot] = useState<Element | null>(null);
|
|
25
|
+
useEffect(() => setSlot(document.querySelector(`[data-slot="${name}"]`)), [name]);
|
|
26
|
+
return slot ? createPortal(children, slot) : null;
|
|
27
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router";
|
|
2
|
+
import { AppTree } from "@/app/app-tree/app-tree.tsx";
|
|
3
|
+
import { SidebarResizeHandle } from "@/app/sidebar-resize-handle/sidebar-resize-handle.tsx";
|
|
4
|
+
import { UserFooter } from "@/app/user-footer/user-footer.tsx";
|
|
5
|
+
import { AppLogo } from "@/branding/branding.tsx";
|
|
6
|
+
import {
|
|
7
|
+
Sidebar,
|
|
8
|
+
SidebarContent,
|
|
9
|
+
SidebarFooter,
|
|
10
|
+
SidebarHeader,
|
|
11
|
+
SidebarMenu,
|
|
12
|
+
SidebarMenuButton,
|
|
13
|
+
SidebarMenuItem,
|
|
14
|
+
useSidebar,
|
|
15
|
+
} from "@/components/ui/sidebar";
|
|
16
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
17
|
+
|
|
18
|
+
// One tree is the whole navigation: documents and flows side by side in the folder they share, with
|
|
19
|
+
// no Intelligence or Flows entry above them (ADR-0004). Tools stay below, next to the user, because
|
|
20
|
+
// they come live from the portal and have no tree.
|
|
21
|
+
export function AppSidebar({
|
|
22
|
+
width,
|
|
23
|
+
onWidthChange,
|
|
24
|
+
}: {
|
|
25
|
+
width: number;
|
|
26
|
+
onWidthChange(width: number): void;
|
|
27
|
+
}) {
|
|
28
|
+
const i18n = useI18n();
|
|
29
|
+
// `offcanvas` rather than `icon`: the icon rail is the mode for a list of equal entries with one
|
|
30
|
+
// icon each, and a tree says what it says through indentation, disclosure arrows and nesting —
|
|
31
|
+
// none of which survives 48px. Collapsing exists to free the canvas, so the sidebar goes.
|
|
32
|
+
//
|
|
33
|
+
// ⚠️ Offcanvas only moves the panel out of sight; it stays in the document and keeps its focus
|
|
34
|
+
// order, so a keyboard would still walk through an invisible tree. The shell therefore unmounts
|
|
35
|
+
// the panel's content itself — the registry file is off limits. On a narrow screen the sidebar is
|
|
36
|
+
// an overlay driven by `openMobile`, and `state` says nothing about it, hence the `isMobile` arm.
|
|
37
|
+
const { state, isMobile } = useSidebar();
|
|
38
|
+
const showPanel = isMobile || state === "expanded";
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Sidebar collapsible="offcanvas" variant="inset">
|
|
42
|
+
{!showPanel ? null : (
|
|
43
|
+
<>
|
|
44
|
+
<SidebarHeader>
|
|
45
|
+
<SidebarMenu>
|
|
46
|
+
<SidebarMenuItem>
|
|
47
|
+
<SidebarMenuButton size="lg" asChild>
|
|
48
|
+
<Link to="/nodes">
|
|
49
|
+
<AppLogo className="size-8" />
|
|
50
|
+
<span className="font-semibold">{i18n.t("app.name")}</span>
|
|
51
|
+
</Link>
|
|
52
|
+
</SidebarMenuButton>
|
|
53
|
+
</SidebarMenuItem>
|
|
54
|
+
</SidebarMenu>
|
|
55
|
+
</SidebarHeader>
|
|
56
|
+
<SidebarContent aria-label={i18n.t("nav.primary")}>
|
|
57
|
+
<AppTree />
|
|
58
|
+
</SidebarContent>
|
|
59
|
+
<SidebarFooter>
|
|
60
|
+
<UserFooter />
|
|
61
|
+
</SidebarFooter>
|
|
62
|
+
<SidebarResizeHandle
|
|
63
|
+
width={width}
|
|
64
|
+
label={i18n.t("shell.resizeSidebar")}
|
|
65
|
+
onWidthChange={onWidthChange}
|
|
66
|
+
/>
|
|
67
|
+
</>
|
|
68
|
+
)}
|
|
69
|
+
</Sidebar>
|
|
70
|
+
);
|
|
71
|
+
}
|