@skillcdn/console 0.4.0 → 0.4.2
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 +20 -2
- package/SKILLCDN.md +7 -5
- package/dist/api.d.ts +5 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +5 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +579 -45
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +34 -3
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +277 -6
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +60 -7
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/plugin-check.d.ts +21 -0
- package/dist/cli/plugin-check.d.ts.map +1 -0
- package/dist/cli/plugin-check.js +374 -0
- package/dist/cli/plugin-check.js.map +1 -0
- package/dist/cli/plugin-cli.d.ts +20 -0
- package/dist/cli/plugin-cli.d.ts.map +1 -0
- package/dist/cli/plugin-cli.js +123 -0
- package/dist/cli/plugin-cli.js.map +1 -0
- package/dist/cli/plugin-init.d.ts +20 -0
- package/dist/cli/plugin-init.d.ts.map +1 -0
- package/dist/cli/plugin-init.js +74 -0
- package/dist/cli/plugin-init.js.map +1 -0
- package/dist/cli/plugin-main.d.ts +3 -0
- package/dist/cli/plugin-main.d.ts.map +1 -0
- package/dist/cli/plugin-main.js +28 -0
- package/dist/cli/plugin-main.js.map +1 -0
- package/dist/client.d.ts +109 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +67 -2
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +1 -1
- package/dist/compat.js +2 -2
- package/dist/components/board.d.ts +8 -0
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +59 -15
- package/dist/components/board.js.map +1 -1
- package/dist/components/event-feed.d.ts +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +33 -1
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +6 -3
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/pack-connect.d.ts +3 -1
- package/dist/components/pack-connect.d.ts.map +1 -1
- package/dist/components/pack-connect.js +4 -4
- package/dist/components/pack-connect.js.map +1 -1
- package/dist/components/pack-token.d.ts +3 -1
- package/dist/components/pack-token.d.ts.map +1 -1
- package/dist/components/pack-token.js +1 -1
- package/dist/components/pack-token.js.map +1 -1
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +14 -3
- package/dist/components/people.js.map +1 -1
- package/dist/components/plugin-frame.d.ts +52 -0
- package/dist/components/plugin-frame.d.ts.map +1 -0
- package/dist/components/plugin-frame.js +314 -0
- package/dist/components/plugin-frame.js.map +1 -0
- package/dist/components/plugin-settings.d.ts +38 -0
- package/dist/components/plugin-settings.d.ts.map +1 -0
- package/dist/components/plugin-settings.js +348 -0
- package/dist/components/plugin-settings.js.map +1 -0
- package/dist/components/remote-pictures.d.ts +6 -0
- package/dist/components/remote-pictures.d.ts.map +1 -0
- package/dist/components/remote-pictures.js +8 -0
- package/dist/components/remote-pictures.js.map +1 -0
- package/dist/components/shell.d.ts +2 -0
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +26 -1
- package/dist/components/shell.js.map +1 -1
- package/dist/components/sign-in.d.ts.map +1 -1
- package/dist/components/sign-in.js +49 -2
- package/dist/components/sign-in.js.map +1 -1
- package/dist/components/ui.d.ts +3 -2
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +6 -3
- package/dist/components/ui.js.map +1 -1
- package/dist/components/workspace.d.ts +7 -1
- package/dist/components/workspace.d.ts.map +1 -1
- package/dist/components/workspace.js +1 -1
- package/dist/components/workspace.js.map +1 -1
- package/dist/console.css +384 -5
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +146 -29
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +11 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +35 -21
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +163 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +174 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +154 -1
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +14 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +14 -0
- package/dist/limits.js.map +1 -1
- package/dist/plugin/args.d.ts +148 -0
- package/dist/plugin/args.d.ts.map +1 -0
- package/dist/plugin/args.js +369 -0
- package/dist/plugin/args.js.map +1 -0
- package/dist/plugin/bridge.d.ts +146 -0
- package/dist/plugin/bridge.d.ts.map +1 -0
- package/dist/plugin/bridge.js +125 -0
- package/dist/plugin/bridge.js.map +1 -0
- package/dist/plugin/capabilities.d.ts +20 -0
- package/dist/plugin/capabilities.d.ts.map +1 -0
- package/dist/plugin/capabilities.js +133 -0
- package/dist/plugin/capabilities.js.map +1 -0
- package/dist/plugin/context.d.ts +374 -0
- package/dist/plugin/context.d.ts.map +1 -0
- package/dist/plugin/context.js +2 -0
- package/dist/plugin/context.js.map +1 -0
- package/dist/plugin/define.d.ts +208 -0
- package/dist/plugin/define.d.ts.map +1 -0
- package/dist/plugin/define.js +482 -0
- package/dist/plugin/define.js.map +1 -0
- package/dist/plugin/definition-error.d.ts +11 -0
- package/dist/plugin/definition-error.d.ts.map +1 -0
- package/dist/plugin/definition-error.js +17 -0
- package/dist/plugin/definition-error.js.map +1 -0
- package/dist/plugin/limits.d.ts +67 -0
- package/dist/plugin/limits.d.ts.map +1 -0
- package/dist/plugin/limits.js +70 -0
- package/dist/plugin/limits.js.map +1 -0
- package/dist/plugin/load-worker.d.ts +7 -0
- package/dist/plugin/load-worker.d.ts.map +1 -0
- package/dist/plugin/load-worker.js +41 -0
- package/dist/plugin/load-worker.js.map +1 -0
- package/dist/plugin/load.d.ts +18 -0
- package/dist/plugin/load.d.ts.map +1 -0
- package/dist/plugin/load.js +62 -0
- package/dist/plugin/load.js.map +1 -0
- package/dist/plugin/manifest.d.ts +237 -0
- package/dist/plugin/manifest.d.ts.map +1 -0
- package/dist/plugin/manifest.js +411 -0
- package/dist/plugin/manifest.js.map +1 -0
- package/dist/plugin/refusals.d.ts +49 -0
- package/dist/plugin/refusals.d.ts.map +1 -0
- package/dist/plugin/refusals.js +79 -0
- package/dist/plugin/refusals.js.map +1 -0
- package/dist/plugin/rest-schemas.d.ts +556 -0
- package/dist/plugin/rest-schemas.d.ts.map +1 -0
- package/dist/plugin/rest-schemas.js +262 -0
- package/dist/plugin/rest-schemas.js.map +1 -0
- package/dist/plugin/sandbox.d.ts +23 -0
- package/dist/plugin/sandbox.d.ts.map +1 -0
- package/dist/plugin/sandbox.js +99 -0
- package/dist/plugin/sandbox.js.map +1 -0
- package/dist/plugin/testing.d.ts +45 -0
- package/dist/plugin/testing.d.ts.map +1 -0
- package/dist/plugin/testing.js +420 -0
- package/dist/plugin/testing.js.map +1 -0
- package/dist/plugin/web-react.d.ts +11 -0
- package/dist/plugin/web-react.d.ts.map +1 -0
- package/dist/plugin/web-react.js +34 -0
- package/dist/plugin/web-react.js.map +1 -0
- package/dist/plugin/web.d.ts +95 -0
- package/dist/plugin/web.d.ts.map +1 -0
- package/dist/plugin/web.js +245 -0
- package/dist/plugin/web.js.map +1 -0
- package/dist/plugin-nav.d.ts +29 -0
- package/dist/plugin-nav.d.ts.map +1 -0
- package/dist/plugin-nav.js +92 -0
- package/dist/plugin-nav.js.map +1 -0
- package/dist/plugin-testing.d.ts +2 -0
- package/dist/plugin-testing.d.ts.map +1 -0
- package/dist/plugin-testing.js +4 -0
- package/dist/plugin-testing.js.map +1 -0
- package/dist/plugin-web-react.d.ts +3 -0
- package/dist/plugin-web-react.d.ts.map +1 -0
- package/dist/plugin-web-react.js +3 -0
- package/dist/plugin-web-react.js.map +1 -0
- package/dist/plugin-web.d.ts +4 -0
- package/dist/plugin-web.d.ts.map +1 -0
- package/dist/plugin-web.js +5 -0
- package/dist/plugin-web.js.map +1 -0
- package/dist/plugin.d.ts +11 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +15 -0
- package/dist/plugin.js.map +1 -0
- package/dist/router.d.ts +28 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +47 -0
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +6 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +8 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +147 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +55 -5
- package/dist/schemas.js.map +1 -1
- package/dist/tokens.d.ts +2 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +101 -0
- package/dist/tokens.js.map +1 -0
- package/dist/vocabulary.d.ts +11 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +24 -0
- package/dist/vocabulary.js.map +1 -1
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +5 -2
- package/dist/web.js.map +1 -1
- package/docs/specs/cli.md +16 -9
- package/docs/specs/plugins.md +487 -0
- package/docs/specs/rest.md +25 -13
- package/package.json +21 -3
- package/skills/building-a-console/SKILL.md +25 -26
- package/skills/building-a-console/references/configuring.md +40 -0
- package/skills/building-a-console/references/the-default-console.md +1 -1
- package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
- package/skills/building-a-plugin/SKILL.md +137 -0
- package/skills/deploying-a-console/SKILL.md +24 -26
- package/skills/deploying-a-console/references/access.md +13 -9
- package/skills/deploying-a-console/references/resources.md +25 -10
- package/skills/deploying-a-console/references/running-it-again.md +15 -0
- package/skills/deploying-a-console/references/sizing.md +2 -0
- package/skills/planning-the-board/SKILL.md +96 -0
- package/skills/planning-the-board/references/a-task.md +36 -0
- package/skills/planning-the-board/references/setting-up.md +44 -0
- package/skills/working-the-board/SKILL.md +43 -34
- package/skills/working-the-board/references/the-pack.md +19 -0
- package/src/api.ts +16 -0
- package/src/cli/cli.ts +736 -45
- package/src/cli/format.ts +356 -9
- package/src/cli/help.ts +71 -6
- package/src/cli/plugin-check.ts +470 -0
- package/src/cli/plugin-cli.ts +148 -0
- package/src/cli/plugin-init.ts +90 -0
- package/src/cli/plugin-main.ts +30 -0
- package/src/client.ts +324 -1
- package/src/compat.ts +2 -2
- package/src/components/board.tsx +62 -14
- package/src/components/event-feed.tsx +42 -1
- package/src/components/markdown.tsx +6 -3
- package/src/components/pack-connect.tsx +10 -7
- package/src/components/pack-token.tsx +4 -1
- package/src/components/people.tsx +17 -7
- package/src/components/plugin-frame.tsx +437 -0
- package/src/components/plugin-settings.tsx +1111 -0
- package/src/components/remote-pictures.ts +19 -0
- package/src/components/shell.tsx +82 -14
- package/src/components/sign-in.tsx +114 -13
- package/src/components/ui.tsx +6 -3
- package/src/components/workspace.tsx +18 -0
- package/src/console.tsx +309 -23
- package/src/data.ts +47 -22
- package/src/i18n/en.ts +183 -1
- package/src/i18n/ko.ts +159 -1
- package/src/index.ts +12 -0
- package/src/limits.ts +14 -0
- package/src/plugin/args.ts +526 -0
- package/src/plugin/bridge.ts +314 -0
- package/src/plugin/capabilities.ts +150 -0
- package/src/plugin/context.ts +416 -0
- package/src/plugin/define.ts +792 -0
- package/src/plugin/definition-error.ts +19 -0
- package/src/plugin/limits.ts +70 -0
- package/src/plugin/load-worker.ts +50 -0
- package/src/plugin/load.ts +82 -0
- package/src/plugin/manifest.ts +561 -0
- package/src/plugin/refusals.ts +94 -0
- package/src/plugin/rest-schemas.ts +354 -0
- package/src/plugin/sandbox.ts +123 -0
- package/src/plugin/testing.ts +612 -0
- package/src/plugin/web-react.ts +45 -0
- package/src/plugin/web.ts +403 -0
- package/src/plugin-nav.ts +132 -0
- package/src/plugin-testing.ts +10 -0
- package/src/plugin-web-react.ts +4 -0
- package/src/plugin-web.ts +35 -0
- package/src/plugin.ts +126 -0
- package/src/router.ts +73 -0
- package/src/routes.ts +9 -0
- package/src/schemas.ts +60 -4
- package/src/styles/console.css +384 -5
- package/src/tokens.ts +101 -0
- package/src/vocabulary.ts +30 -0
- package/src/web.ts +5 -2
- package/template/plugin/.gitignore +2 -0
- package/template/plugin/README.md +26 -0
- package/template/plugin/SKILLCDN.md +13 -0
- package/template/plugin/build.mjs +40 -0
- package/template/plugin/console-plugin.json +50 -0
- package/template/plugin/package.json +27 -0
- package/template/plugin/skills/__key__/SKILL.md +23 -0
- package/template/plugin/src/check.test.ts +34 -0
- package/template/plugin/src/server/index.test.ts +41 -0
- package/template/plugin/src/server/index.ts +72 -0
- package/template/plugin/src/web/index.css +51 -0
- package/template/plugin/src/web/index.ts +115 -0
- package/template/plugin/tsconfig.json +17 -0
- package/skills/working-the-board/references/setting-up.md +0 -3
- /package/skills/{working-the-board → planning-the-board}/references/project-rules.md +0 -0
|
@@ -10,8 +10,15 @@ import { formatInstant, PersonChip } from "./ui.js";
|
|
|
10
10
|
// as which agent when through one, and what. It takes its data as props and nothing from the
|
|
11
11
|
// network; the sentence is the language's (ADR-0012).
|
|
12
12
|
|
|
13
|
-
/** What an event says, without its actor: the words after the name, in the words given. */
|
|
13
|
+
/** What an event says, without its actor: the words after the name, in the words given; a board's event a plugin caused says so (ADR-0052). */
|
|
14
14
|
export function describeEvent(event: RestEvent, words: Messages = en): string {
|
|
15
|
+
const sentence = sentenceOf(event, words);
|
|
16
|
+
return event.plugin !== null && !event.kind.startsWith("plugin.")
|
|
17
|
+
? `${sentence} ${words.feed.throughPlugin(event.plugin)}`
|
|
18
|
+
: sentence;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sentenceOf(event: RestEvent, words: Messages): string {
|
|
15
22
|
const { data } = event;
|
|
16
23
|
const { feed, vocabulary } = words;
|
|
17
24
|
const task =
|
|
@@ -34,6 +41,15 @@ export function describeEvent(event: RestEvent, words: Messages = en): string {
|
|
|
34
41
|
const question = data.question ?? feed.aQuestion;
|
|
35
42
|
const goal = data.title ?? feed.aGoal;
|
|
36
43
|
const category = data.name ?? data.key ?? feed.aCategory;
|
|
44
|
+
const plugin = event.plugin ?? feed.aPlugin;
|
|
45
|
+
const grantee =
|
|
46
|
+
data.everyone === true
|
|
47
|
+
? feed.everyone
|
|
48
|
+
: data.login !== undefined
|
|
49
|
+
? data.login
|
|
50
|
+
: data.key !== undefined
|
|
51
|
+
? feed.membersOf(data.name ?? data.key)
|
|
52
|
+
: feed.someone;
|
|
37
53
|
switch (event.kind) {
|
|
38
54
|
case "person.joined":
|
|
39
55
|
return feed.event.personJoined();
|
|
@@ -115,6 +131,31 @@ export function describeEvent(event: RestEvent, words: Messages = en): string {
|
|
|
115
131
|
return feed.event.documentRestored(page);
|
|
116
132
|
case "document.file_attached":
|
|
117
133
|
return feed.event.fileAttached(data.label ?? feed.aFile, page);
|
|
134
|
+
case "plugin.enabled":
|
|
135
|
+
return feed.event.pluginEnabled(plugin);
|
|
136
|
+
case "plugin.disabled":
|
|
137
|
+
return feed.event.pluginDisabled(plugin);
|
|
138
|
+
case "plugin.configured":
|
|
139
|
+
return feed.event.pluginConfigured(fields, plugin);
|
|
140
|
+
case "plugin.granted":
|
|
141
|
+
return feed.event.pluginGranted(data.role ?? feed.role.other, grantee, plugin);
|
|
142
|
+
case "plugin.revoked":
|
|
143
|
+
return feed.event.pluginRevoked(data.role ?? feed.role.other, grantee, plugin);
|
|
144
|
+
case "plugin.upgraded":
|
|
145
|
+
return feed.event.pluginUpgraded(
|
|
146
|
+
plugin,
|
|
147
|
+
data.version === undefined ? feed.something : String(data.version),
|
|
148
|
+
);
|
|
149
|
+
case "plugin.command":
|
|
150
|
+
return feed.event.pluginCommand(
|
|
151
|
+
data.command ?? feed.something,
|
|
152
|
+
plugin,
|
|
153
|
+
feed.outcome[data.outcome ?? ""] ?? data.outcome ?? feed.something,
|
|
154
|
+
);
|
|
155
|
+
case "plugin.purged":
|
|
156
|
+
return feed.event.pluginPurged(plugin);
|
|
157
|
+
case "plugin.failed":
|
|
158
|
+
return feed.event.pluginFailed(plugin, data.problem ?? feed.something);
|
|
118
159
|
}
|
|
119
160
|
}
|
|
120
161
|
|
|
@@ -2,11 +2,13 @@ import ReactMarkdown from "react-markdown";
|
|
|
2
2
|
import remarkGfm from "remark-gfm";
|
|
3
3
|
import { isDocumentPath } from "../documents.js";
|
|
4
4
|
import { type Picture, PictureThumb } from "./pictures.js";
|
|
5
|
+
import { useRemotePictureSrc } from "./remote-pictures.js";
|
|
5
6
|
|
|
6
7
|
// What people and agents write is untrusted. It is rendered as React elements, never as HTML:
|
|
7
8
|
// raw HTML in the source stays text, links are limited to a few schemes and open elsewhere,
|
|
8
|
-
// and
|
|
9
|
-
// is a document's path (ADR-0009)
|
|
9
|
+
// and a picture from elsewhere, named over https, is read through the console (ADR-0048),
|
|
10
|
+
// lazily and without a referrer. A link whose destination is a document's path (ADR-0009)
|
|
11
|
+
// leads to the document's page, when the page knows where.
|
|
10
12
|
|
|
11
13
|
const SAFE_LINK = /^(https?:|mailto:)/i;
|
|
12
14
|
const SAFE_IMAGE = /^https:\/\//i;
|
|
@@ -29,6 +31,7 @@ export interface MarkdownProps {
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export function Markdown(props: MarkdownProps) {
|
|
34
|
+
const remote = useRemotePictureSrc();
|
|
32
35
|
return (
|
|
33
36
|
<div className="sc-prose">
|
|
34
37
|
<ReactMarkdown
|
|
@@ -62,7 +65,7 @@ export function Markdown(props: MarkdownProps) {
|
|
|
62
65
|
img({ src, alt, title }) {
|
|
63
66
|
const named = typeof src === "string" ? props.fileSrc?.(src) : undefined;
|
|
64
67
|
const url =
|
|
65
|
-
named ?? (typeof src === "string" && SAFE_IMAGE.test(src) ? src : undefined);
|
|
68
|
+
named ?? (typeof src === "string" && SAFE_IMAGE.test(src) ? remote(src) : undefined);
|
|
66
69
|
if (url === undefined) {
|
|
67
70
|
return <span className="sc-prose-image-text">{alt ?? ""}</span>;
|
|
68
71
|
}
|
|
@@ -6,8 +6,9 @@ import { Button, Time } from "./ui.js";
|
|
|
6
6
|
// A private pack connected with a code a person approves (ADR-0036): an owner, or for the
|
|
7
7
|
// workspace's token an administrator, asks, the console asks the deployment and shows what it
|
|
8
8
|
// answered, the code and the page, and the person approves there; the page asks the console
|
|
9
|
-
//
|
|
10
|
-
//
|
|
9
|
+
// to claim the token at the interval the deployment named, until the token is kept or the
|
|
10
|
+
// request ended; reading how the request stands, when the page opens, changes nothing
|
|
11
|
+
// (ADR-0046). Takes its data through the callbacks and keeps nothing itself.
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* How a connection stands, as the component needs it: what the API answers for a project's
|
|
@@ -20,8 +21,10 @@ export type PackConnectState =
|
|
|
20
21
|
export interface PackConnectProps {
|
|
21
22
|
/** Asks the deployment to connect; answers what the person is shown. */
|
|
22
23
|
readonly onStart: () => Promise<RestPackConnectStarted>;
|
|
23
|
-
/** How the connection stands
|
|
24
|
+
/** How the connection stands, read when the page opens; changes nothing. */
|
|
24
25
|
readonly poll: (signal?: AbortSignal) => Promise<PackConnectState>;
|
|
26
|
+
/** Asks the console to claim the token at the deployment: pending still, connected, or ended. */
|
|
27
|
+
readonly claim: (signal?: AbortSignal) => Promise<PackConnectState>;
|
|
25
28
|
/** Gives the connection up. */
|
|
26
29
|
readonly onCancel: () => Promise<void>;
|
|
27
30
|
/** Called once the token is kept. */
|
|
@@ -47,7 +50,7 @@ const MIN_INTERVAL_SECONDS = 2;
|
|
|
47
50
|
export function PackConnect(props: PackConnectProps) {
|
|
48
51
|
const words = useWords();
|
|
49
52
|
const pack = words.projects.packToken;
|
|
50
|
-
const { poll, onConnected } = props;
|
|
53
|
+
const { poll, claim, onConnected } = props;
|
|
51
54
|
const [shown, setShown] = useState<Shown>({ kind: "idle" });
|
|
52
55
|
const [problem, setProblem] = useState<string | undefined>(undefined);
|
|
53
56
|
|
|
@@ -64,7 +67,7 @@ export function PackConnect(props: PackConnectProps) {
|
|
|
64
67
|
return () => controller.abort();
|
|
65
68
|
}, [poll]);
|
|
66
69
|
|
|
67
|
-
// While waiting, the console is asked
|
|
70
|
+
// While waiting, the console is asked to claim at the interval the deployment named; a new
|
|
68
71
|
// answer, pending still, is a new state, which schedules the next asking.
|
|
69
72
|
useEffect(() => {
|
|
70
73
|
if (shown.kind !== "waiting") {
|
|
@@ -72,7 +75,7 @@ export function PackConnect(props: PackConnectProps) {
|
|
|
72
75
|
}
|
|
73
76
|
const controller = new AbortController();
|
|
74
77
|
const timer = setTimeout(() => {
|
|
75
|
-
|
|
78
|
+
claim(controller.signal)
|
|
76
79
|
.then((state) => {
|
|
77
80
|
if (controller.signal.aborted) {
|
|
78
81
|
return;
|
|
@@ -102,7 +105,7 @@ export function PackConnect(props: PackConnectProps) {
|
|
|
102
105
|
controller.abort();
|
|
103
106
|
clearTimeout(timer);
|
|
104
107
|
};
|
|
105
|
-
}, [shown,
|
|
108
|
+
}, [shown, claim, onConnected]);
|
|
106
109
|
|
|
107
110
|
const start = () => {
|
|
108
111
|
setProblem(undefined);
|
|
@@ -20,8 +20,10 @@ export interface PackTokenWaysProps {
|
|
|
20
20
|
readonly busy?: boolean | undefined;
|
|
21
21
|
/** Asks the deployment to connect; answers what the person is shown. */
|
|
22
22
|
readonly onStart: () => Promise<RestPackConnectStarted>;
|
|
23
|
-
/** How the connection stands
|
|
23
|
+
/** How the connection stands, read when the page opens; changes nothing. */
|
|
24
24
|
readonly poll: (signal?: AbortSignal) => Promise<PackConnectState>;
|
|
25
|
+
/** Asks the console to claim the token at the deployment (ADR-0046). */
|
|
26
|
+
readonly claim: (signal?: AbortSignal) => Promise<PackConnectState>;
|
|
25
27
|
/** Gives the connection up. */
|
|
26
28
|
readonly onCancelConnect: () => Promise<void>;
|
|
27
29
|
/** Called once the token is kept. */
|
|
@@ -46,6 +48,7 @@ export function PackTokenWays(props: PackTokenWaysProps) {
|
|
|
46
48
|
<PackConnect
|
|
47
49
|
onStart={props.onStart}
|
|
48
50
|
poll={props.poll}
|
|
51
|
+
claim={props.claim}
|
|
49
52
|
onCancel={props.onCancelConnect}
|
|
50
53
|
onConnected={props.onConnected}
|
|
51
54
|
busy={props.busy}
|
|
@@ -57,6 +57,19 @@ export function PeopleList(props: PeopleListProps) {
|
|
|
57
57
|
person.source !== "organization" &&
|
|
58
58
|
props.me?.id !== person.id;
|
|
59
59
|
const organization = props.organization ?? undefined;
|
|
60
|
+
/** What let the person in, by its name: the organization, the domain of their login, or an administrator. */
|
|
61
|
+
const noteOf = (person: RestPerson): string => {
|
|
62
|
+
if (person.source === "organization") {
|
|
63
|
+
return organization === undefined
|
|
64
|
+
? people.source.organization
|
|
65
|
+
: people.ofOrganization(organization.name);
|
|
66
|
+
}
|
|
67
|
+
if (person.source === "domain") {
|
|
68
|
+
const at = person.login.lastIndexOf("@");
|
|
69
|
+
return at === -1 ? people.source.domain : people.ofDomain(person.login.slice(at + 1));
|
|
70
|
+
}
|
|
71
|
+
return people.source.listed;
|
|
72
|
+
};
|
|
60
73
|
return (
|
|
61
74
|
<div className="sc-people-page">
|
|
62
75
|
{organization !== undefined && (
|
|
@@ -79,8 +92,8 @@ export function PeopleList(props: PeopleListProps) {
|
|
|
79
92
|
<span className="sc-person-you">{words.common.you}</span>
|
|
80
93
|
)}
|
|
81
94
|
<span className="sc-person-name sc-muted">
|
|
82
|
-
{person.name
|
|
83
|
-
<span className="sc-person-note">
|
|
95
|
+
{person.name !== null && <>{person.login} · </>}
|
|
96
|
+
<span className="sc-person-note">{noteOf(person)}</span>
|
|
84
97
|
</span>
|
|
85
98
|
{props.onChangeRole === undefined ? (
|
|
86
99
|
<RoleBadge role={person.role} />
|
|
@@ -163,12 +176,9 @@ export function PeopleList(props: PeopleListProps) {
|
|
|
163
176
|
<li key={person.id} className="sc-person-row sc-person-row-out">
|
|
164
177
|
<PersonChip person={person} size="md" />
|
|
165
178
|
<span className="sc-person-name sc-muted">
|
|
166
|
-
{person.name
|
|
179
|
+
{person.name !== null && <>{person.login} · </>}
|
|
167
180
|
<span className="sc-person-note">
|
|
168
|
-
{"
|
|
169
|
-
{person.source === "organization"
|
|
170
|
-
? people.leftOrganization
|
|
171
|
-
: people.source[person.source]}
|
|
181
|
+
{person.source === "organization" ? people.leftOrganization : noteOf(person)}
|
|
172
182
|
{person.deactivatedAt !== null && (
|
|
173
183
|
<>
|
|
174
184
|
{" "}
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type { ConsoleClient, PluginClient } from "../client.js";
|
|
3
|
+
import {
|
|
4
|
+
type BridgeCall,
|
|
5
|
+
type BridgeEventKind,
|
|
6
|
+
type BridgeToFrame,
|
|
7
|
+
isBridgePath,
|
|
8
|
+
isBridgeToPage,
|
|
9
|
+
PLUGIN_BRIDGE_VERSION,
|
|
10
|
+
} from "../plugin/bridge.js";
|
|
11
|
+
import { MAX_PLUGIN_WIDGET_HEIGHT } from "../plugin/limits.js";
|
|
12
|
+
import type { Navigation } from "../router.js";
|
|
13
|
+
import type { RestEvent, RestMe, RestPlugin, RestProject } from "../schemas.js";
|
|
14
|
+
import { CONSOLE_TOKENS } from "../tokens.js";
|
|
15
|
+
|
|
16
|
+
// A plugin's page or widget on the console's page (ADR-0054, docs/specs/plugins.md, "Pages"):
|
|
17
|
+
// a sandboxed frame whose document the console serves, and the console's side of the bridge.
|
|
18
|
+
// The frame speaks over postMessage alone; this component accepts the frame's window alone,
|
|
19
|
+
// performs every call under the plugin's own routes with the person's session, and refuses the
|
|
20
|
+
// rest before any request. Nothing the frame says is rendered as HTML.
|
|
21
|
+
|
|
22
|
+
/** How tall a page's frame may grow: far above any page, below what would break the layout. */
|
|
23
|
+
const MAX_PAGE_HEIGHT = 20_000;
|
|
24
|
+
/** How often the plugin's own events are asked for while a frame listens for them. */
|
|
25
|
+
const EVENTS_POLL_MS = 5_000;
|
|
26
|
+
|
|
27
|
+
export interface PluginFrameProps {
|
|
28
|
+
readonly plugin: RestPlugin;
|
|
29
|
+
/** The page shown, or the widget; one of the two. */
|
|
30
|
+
readonly page?: string | undefined;
|
|
31
|
+
readonly widget?: string | undefined;
|
|
32
|
+
/** The project the page is on, or nothing on the workspace. */
|
|
33
|
+
readonly project?: RestProject | undefined;
|
|
34
|
+
/** What follows the page in the address, handed to the frame. */
|
|
35
|
+
readonly path?: string | undefined;
|
|
36
|
+
readonly me: RestMe;
|
|
37
|
+
readonly language: string;
|
|
38
|
+
readonly client: ConsoleClient;
|
|
39
|
+
readonly navigation: Navigation;
|
|
40
|
+
/** The project's feed, for the frame to hear of the plugin's commands; none on the workspace. */
|
|
41
|
+
readonly events?: readonly RestEvent[] | undefined;
|
|
42
|
+
/** What the frame is called, for assistive technology. */
|
|
43
|
+
readonly title: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A refusal of the bridge, as the frame receives it: the console's code and a message. */
|
|
47
|
+
export class BridgeRefusal extends Error {
|
|
48
|
+
readonly code: string;
|
|
49
|
+
|
|
50
|
+
constructor(code: string, message: string) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = "BridgeRefusal";
|
|
53
|
+
this.code = code;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const text = (value: unknown, what: string): string => {
|
|
58
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
59
|
+
throw new BridgeRefusal("plugin.invalid_args", `${what} is text`);
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const record = (value: unknown): Record<string, unknown> =>
|
|
65
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
66
|
+
? (value as Record<string, unknown>)
|
|
67
|
+
: {};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Performs one `call` of the frame through the plugin's own routes and nothing else: a command,
|
|
71
|
+
* a query, the records, the files. The answer, and what to transfer with it; a method the bridge
|
|
72
|
+
* does not know, or a request outside the plugin, is refused before anything is sent.
|
|
73
|
+
*/
|
|
74
|
+
export async function performBridgeCall(
|
|
75
|
+
plugin: PluginClient,
|
|
76
|
+
projectKey: string | undefined,
|
|
77
|
+
call: BridgeCall,
|
|
78
|
+
): Promise<{ readonly value: unknown; readonly transfer?: Transferable[] }> {
|
|
79
|
+
const params = record(call.params);
|
|
80
|
+
const project = projectKey === undefined ? {} : { project: projectKey };
|
|
81
|
+
switch (call.method) {
|
|
82
|
+
case "command": {
|
|
83
|
+
const name = text(params.name, "name");
|
|
84
|
+
return {
|
|
85
|
+
value: await plugin.command(name, { args: record(params.args), ...project }),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
case "query": {
|
|
89
|
+
const name = text(params.name, "name");
|
|
90
|
+
return { value: await plugin.query(name, record(params.args), project) };
|
|
91
|
+
}
|
|
92
|
+
case "records.list": {
|
|
93
|
+
const collection = text(params.collection, "collection");
|
|
94
|
+
return {
|
|
95
|
+
value: await plugin.records(collection, {
|
|
96
|
+
where: params.where === undefined ? undefined : record(params.where),
|
|
97
|
+
order: typeof params.order === "string" ? params.order : undefined,
|
|
98
|
+
limit: typeof params.limit === "number" ? params.limit : undefined,
|
|
99
|
+
after: typeof params.after === "string" ? params.after : undefined,
|
|
100
|
+
q: typeof params.q === "string" ? params.q : undefined,
|
|
101
|
+
project: typeof params.project === "string" ? params.project : undefined,
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
case "records.get": {
|
|
106
|
+
const collection = text(params.collection, "collection");
|
|
107
|
+
const id = text(params.id, "id");
|
|
108
|
+
return {
|
|
109
|
+
value: await plugin.record(
|
|
110
|
+
collection,
|
|
111
|
+
id,
|
|
112
|
+
typeof params.version === "number" ? params.version : undefined,
|
|
113
|
+
),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
case "records.versions": {
|
|
117
|
+
const collection = text(params.collection, "collection");
|
|
118
|
+
const id = text(params.id, "id");
|
|
119
|
+
return { value: (await plugin.recordVersions(collection, id)).items };
|
|
120
|
+
}
|
|
121
|
+
case "files.get": {
|
|
122
|
+
const id = text(params.id, "id");
|
|
123
|
+
const file = await plugin.readFile(id);
|
|
124
|
+
const bytes = new Uint8Array(file.bytes).buffer;
|
|
125
|
+
return {
|
|
126
|
+
value: { name: file.name, type: file.contentType, bytes },
|
|
127
|
+
transfer: [bytes],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
case "files.put": {
|
|
131
|
+
const name = text(params.name, "name");
|
|
132
|
+
const type = text(params.type, "type");
|
|
133
|
+
const bytes = params.bytes;
|
|
134
|
+
if (!(bytes instanceof ArrayBuffer)) {
|
|
135
|
+
throw new BridgeRefusal("plugin.invalid_args", "bytes is an ArrayBuffer");
|
|
136
|
+
}
|
|
137
|
+
const handle = await plugin.upload({ name, bytes: new Uint8Array(bytes), contentType: type });
|
|
138
|
+
return { value: { handle: handle.handle } };
|
|
139
|
+
}
|
|
140
|
+
default:
|
|
141
|
+
throw new BridgeRefusal(
|
|
142
|
+
"plugin.forbidden",
|
|
143
|
+
`${String(call.method)} is not a method of the bridge`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The console's computed design tokens, as the page's root has them: what a frame is told to look like. */
|
|
149
|
+
export function readTheme(): Record<string, string> {
|
|
150
|
+
if (typeof document === "undefined" || typeof getComputedStyle === "undefined") {
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
const style = getComputedStyle(document.documentElement);
|
|
154
|
+
const theme: Record<string, string> = {};
|
|
155
|
+
for (const token of CONSOLE_TOKENS) {
|
|
156
|
+
const value = style.getPropertyValue(token).trim();
|
|
157
|
+
if (value.length > 0) {
|
|
158
|
+
theme[token] = value.slice(0, 200);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return theme;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** What a failure of a call says to the frame: the console's code and words, or the console's own failure. */
|
|
165
|
+
function errorOf(error: unknown): { readonly code: string; readonly message: string } {
|
|
166
|
+
if (error instanceof BridgeRefusal) {
|
|
167
|
+
return { code: error.code, message: error.message };
|
|
168
|
+
}
|
|
169
|
+
if (
|
|
170
|
+
typeof error === "object" &&
|
|
171
|
+
error !== null &&
|
|
172
|
+
"code" in error &&
|
|
173
|
+
typeof error.code === "string" &&
|
|
174
|
+
"message" in error &&
|
|
175
|
+
typeof error.message === "string"
|
|
176
|
+
) {
|
|
177
|
+
return { code: error.code, message: error.message };
|
|
178
|
+
}
|
|
179
|
+
return { code: "plugin.failed", message: "the call failed" };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function PluginFrame(props: PluginFrameProps) {
|
|
183
|
+
const { plugin, page, widget, project, me, language, client, navigation, events } = props;
|
|
184
|
+
const frame = useRef<HTMLIFrameElement>(null);
|
|
185
|
+
const isWidget = widget !== undefined;
|
|
186
|
+
const [height, setHeight] = useState(isWidget ? 160 : 480);
|
|
187
|
+
const subscribed = useRef<Set<BridgeEventKind>>(new Set());
|
|
188
|
+
const ready = useRef(false);
|
|
189
|
+
const lastPluginEvent = useRef(0);
|
|
190
|
+
const seenEvents = useRef(0);
|
|
191
|
+
const path = props.path ?? "";
|
|
192
|
+
const scoped = useMemo(() => client.plugin(plugin.key), [client, plugin.key]);
|
|
193
|
+
const src = useMemo(
|
|
194
|
+
() =>
|
|
195
|
+
scoped.frameUrl({
|
|
196
|
+
...(page === undefined ? {} : { page }),
|
|
197
|
+
...(widget === undefined ? {} : { widget }),
|
|
198
|
+
...(project === undefined ? {} : { project: project.key }),
|
|
199
|
+
}),
|
|
200
|
+
[scoped, page, widget, project],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const post = useCallback((message: BridgeToFrame, transfer?: Transferable[]) => {
|
|
204
|
+
// The frame's origin is opaque, so the target is any: the frame itself checks the sender.
|
|
205
|
+
frame.current?.contentWindow?.postMessage(message, "*", transfer ?? []);
|
|
206
|
+
}, []);
|
|
207
|
+
|
|
208
|
+
// The frame's messages: from its own window alone, each as the bridge spells it.
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (typeof window === "undefined") {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const onMessage = (event: MessageEvent) => {
|
|
214
|
+
const target = frame.current?.contentWindow;
|
|
215
|
+
if (target === null || target === undefined || event.source !== target) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!isBridgeToPage(event.data)) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const message = event.data;
|
|
222
|
+
switch (message.t) {
|
|
223
|
+
case "ready":
|
|
224
|
+
ready.current = true;
|
|
225
|
+
post({
|
|
226
|
+
v: PLUGIN_BRIDGE_VERSION,
|
|
227
|
+
t: "init",
|
|
228
|
+
plugin: { key: plugin.key, version: plugin.version },
|
|
229
|
+
...(page === undefined ? {} : { page }),
|
|
230
|
+
...(widget === undefined ? {} : { widget }),
|
|
231
|
+
project: project === undefined ? null : { key: project.key, name: project.name },
|
|
232
|
+
person: {
|
|
233
|
+
id: me.person?.id ?? "",
|
|
234
|
+
login: me.person?.login ?? "",
|
|
235
|
+
name: me.person?.name ?? null,
|
|
236
|
+
language,
|
|
237
|
+
admin: me.person?.role === "admin",
|
|
238
|
+
},
|
|
239
|
+
roles: plugin.roles,
|
|
240
|
+
path,
|
|
241
|
+
language,
|
|
242
|
+
theme: readTheme(),
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
case "call":
|
|
246
|
+
void performBridgeCall(scoped, project?.key, message)
|
|
247
|
+
.then(({ value, transfer }) =>
|
|
248
|
+
post(
|
|
249
|
+
{ v: PLUGIN_BRIDGE_VERSION, t: "result", id: message.id, ok: true, value },
|
|
250
|
+
transfer,
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
.catch((error: unknown) =>
|
|
254
|
+
post({
|
|
255
|
+
v: PLUGIN_BRIDGE_VERSION,
|
|
256
|
+
t: "result",
|
|
257
|
+
id: message.id,
|
|
258
|
+
ok: false,
|
|
259
|
+
error: errorOf(error),
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
return;
|
|
263
|
+
case "navigate":
|
|
264
|
+
if (isBridgePath(message.path)) {
|
|
265
|
+
navigation.go(message.path);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
case "resize":
|
|
269
|
+
setHeight(
|
|
270
|
+
Math.max(
|
|
271
|
+
40,
|
|
272
|
+
Math.min(
|
|
273
|
+
Math.ceil(message.height),
|
|
274
|
+
isWidget ? MAX_PLUGIN_WIDGET_HEIGHT : MAX_PAGE_HEIGHT,
|
|
275
|
+
),
|
|
276
|
+
),
|
|
277
|
+
);
|
|
278
|
+
return;
|
|
279
|
+
case "title":
|
|
280
|
+
if (!isWidget && typeof document !== "undefined") {
|
|
281
|
+
document.title = message.text;
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
case "download": {
|
|
285
|
+
const anchor = document.createElement("a");
|
|
286
|
+
anchor.href = scoped.fileUrl(message.id);
|
|
287
|
+
anchor.download = "";
|
|
288
|
+
anchor.rel = "noopener";
|
|
289
|
+
anchor.click();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
case "subscribe":
|
|
293
|
+
subscribed.current = new Set(message.kinds);
|
|
294
|
+
return;
|
|
295
|
+
default:
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
window.addEventListener("message", onMessage);
|
|
300
|
+
return () => window.removeEventListener("message", onMessage);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// The console's look and language reach the frame as they change.
|
|
304
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: the language is the trigger; the tokens change with it
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
if (ready.current) {
|
|
307
|
+
post({ v: PLUGIN_BRIDGE_VERSION, t: "theme", theme: readTheme() });
|
|
308
|
+
}
|
|
309
|
+
}, [language, post]);
|
|
310
|
+
|
|
311
|
+
// The address moved within the page: the frame is told what follows the page now.
|
|
312
|
+
useEffect(() => {
|
|
313
|
+
if (ready.current) {
|
|
314
|
+
post({ v: PLUGIN_BRIDGE_VERSION, t: "path", path });
|
|
315
|
+
}
|
|
316
|
+
}, [path, post]);
|
|
317
|
+
|
|
318
|
+
// The plugin's commands, as the project's feed tells of them: a frame that listens hears of each.
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
if (events === undefined) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const fresh = events.slice(seenEvents.current);
|
|
324
|
+
seenEvents.current = events.length;
|
|
325
|
+
if (!ready.current) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
for (const event of fresh) {
|
|
329
|
+
if (event.kind === "plugin.command" && event.plugin === plugin.key) {
|
|
330
|
+
if (subscribed.current.has("command")) {
|
|
331
|
+
post({ v: PLUGIN_BRIDGE_VERSION, t: "event", kind: "command", data: event.data });
|
|
332
|
+
}
|
|
333
|
+
if (subscribed.current.has("records")) {
|
|
334
|
+
post({ v: PLUGIN_BRIDGE_VERSION, t: "event", kind: "records", data: event.data });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}, [events, plugin.key, post]);
|
|
339
|
+
|
|
340
|
+
// The plugin's own events, asked for while a frame listens for them.
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
if (typeof window === "undefined") {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const timer = setInterval(() => {
|
|
346
|
+
if (!ready.current || !subscribed.current.has("plugin")) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
scoped
|
|
350
|
+
.pluginEvents({ after: lastPluginEvent.current, limit: 50 })
|
|
351
|
+
.then((page) => {
|
|
352
|
+
for (const event of page.items) {
|
|
353
|
+
lastPluginEvent.current = Math.max(lastPluginEvent.current, event.id);
|
|
354
|
+
post({ v: PLUGIN_BRIDGE_VERSION, t: "event", kind: "plugin", data: event });
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
.catch(() => undefined);
|
|
358
|
+
}, EVENTS_POLL_MS);
|
|
359
|
+
return () => clearInterval(timer);
|
|
360
|
+
}, [scoped, post]);
|
|
361
|
+
|
|
362
|
+
// What the plugin emitted before the frame came is not replayed: the stream is read from now.
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
scoped
|
|
365
|
+
.pluginEvents({ after: 0, limit: 1 })
|
|
366
|
+
.then((page) => {
|
|
367
|
+
if (page.more || page.items.length > 0) {
|
|
368
|
+
// The newest number is past the first page; a frame hears of what comes after it listens.
|
|
369
|
+
return scoped.pluginEvents({ after: 0, limit: 200 }).then((all) => {
|
|
370
|
+
lastPluginEvent.current = all.items.at(-1)?.id ?? 0;
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
return undefined;
|
|
374
|
+
})
|
|
375
|
+
.catch(() => undefined);
|
|
376
|
+
}, [scoped]);
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
<iframe
|
|
380
|
+
ref={frame}
|
|
381
|
+
className={isWidget ? "sc-plugin-frame sc-plugin-frame-widget" : "sc-plugin-frame"}
|
|
382
|
+
src={src}
|
|
383
|
+
title={props.title}
|
|
384
|
+
// As the document's own policy says: scripts, and forms firing their submit event, which
|
|
385
|
+
// the policy's form-action 'none' keeps from going anywhere.
|
|
386
|
+
sandbox="allow-scripts allow-forms"
|
|
387
|
+
referrerPolicy="origin"
|
|
388
|
+
style={{ height: `${height}px` }}
|
|
389
|
+
/>
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export interface PluginWidgetsProps {
|
|
394
|
+
readonly plugins: readonly RestPlugin[];
|
|
395
|
+
readonly place: "home:top" | "home:bottom";
|
|
396
|
+
readonly me: RestMe;
|
|
397
|
+
readonly language: string;
|
|
398
|
+
readonly client: ConsoleClient;
|
|
399
|
+
readonly navigation: Navigation;
|
|
400
|
+
/** The widget's name in the person's language. */
|
|
401
|
+
readonly nameOf: (name: Readonly<Record<string, string>>) => string;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** The widgets at a place of the front page, in a grid of six columns: a third takes two, a half three, the full six. */
|
|
405
|
+
export function PluginWidgets(props: PluginWidgetsProps) {
|
|
406
|
+
const shown = props.plugins.flatMap((plugin) =>
|
|
407
|
+
plugin.widgets
|
|
408
|
+
.filter((widget) => widget.place === props.place)
|
|
409
|
+
.map((widget) => ({ plugin, widget })),
|
|
410
|
+
);
|
|
411
|
+
if (shown.length === 0) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
return (
|
|
415
|
+
<section
|
|
416
|
+
className={`sc-widgets ${props.place === "home:top" ? "sc-widgets-top" : "sc-widgets-bottom"}`}
|
|
417
|
+
>
|
|
418
|
+
{shown.map(({ plugin, widget }) => (
|
|
419
|
+
<article
|
|
420
|
+
key={`${plugin.key}:${widget.key}`}
|
|
421
|
+
className={`sc-widget sc-widget-${widget.size}`}
|
|
422
|
+
>
|
|
423
|
+
<h2 className="sc-widget-title">{props.nameOf(widget.name)}</h2>
|
|
424
|
+
<PluginFrame
|
|
425
|
+
plugin={plugin}
|
|
426
|
+
widget={widget.key}
|
|
427
|
+
me={props.me}
|
|
428
|
+
language={props.language}
|
|
429
|
+
client={props.client}
|
|
430
|
+
navigation={props.navigation}
|
|
431
|
+
title={props.nameOf(widget.name)}
|
|
432
|
+
/>
|
|
433
|
+
</article>
|
|
434
|
+
))}
|
|
435
|
+
</section>
|
|
436
|
+
);
|
|
437
|
+
}
|