@dxos/plugin-script 0.6.10-main.bbdfaa4 → 0.6.10-staging.00555f6

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.
Files changed (91) hide show
  1. package/README.md +45 -0
  2. package/dist/lib/browser/ScriptEditor-WNCGR5OU.mjs +452 -0
  3. package/dist/lib/browser/ScriptEditor-WNCGR5OU.mjs.map +7 -0
  4. package/dist/lib/browser/{TypescriptEditor-PIRIDDUS.mjs → TypescriptEditor-XTGRVCMY.mjs} +2 -2
  5. package/dist/lib/browser/{chunk-ISMNAD3P.mjs → chunk-DL5XQRQM.mjs} +13 -7
  6. package/dist/lib/browser/{chunk-ISMNAD3P.mjs.map → chunk-DL5XQRQM.mjs.map} +3 -3
  7. package/dist/lib/browser/{chunk-D4O723CR.mjs → chunk-DX5CSOBP.mjs} +22 -5
  8. package/dist/lib/browser/chunk-DX5CSOBP.mjs.map +7 -0
  9. package/dist/lib/browser/chunk-RHEZ5LUU.mjs +229 -0
  10. package/dist/lib/browser/chunk-RHEZ5LUU.mjs.map +7 -0
  11. package/dist/lib/browser/edge/index.mjs +1 -1
  12. package/dist/lib/browser/index.mjs +88 -195
  13. package/dist/lib/browser/index.mjs.map +4 -4
  14. package/dist/lib/browser/meta.json +1 -1
  15. package/dist/lib/node/ScriptEditor-RR7AVOQL.cjs +465 -0
  16. package/dist/lib/node/ScriptEditor-RR7AVOQL.cjs.map +7 -0
  17. package/dist/lib/node/{TypescriptEditor-OTWJUIWB.cjs → TypescriptEditor-CRONCQSI.cjs} +7 -7
  18. package/dist/lib/node/{TypescriptEditor-OTWJUIWB.cjs.map → TypescriptEditor-CRONCQSI.cjs.map} +2 -2
  19. package/dist/lib/node/chunk-4GTXIELQ.cjs +252 -0
  20. package/dist/lib/node/chunk-4GTXIELQ.cjs.map +7 -0
  21. package/dist/lib/node/{chunk-HN3AVLUL.cjs → chunk-PFH3ROEH.cjs} +16 -10
  22. package/dist/lib/node/{chunk-HN3AVLUL.cjs.map → chunk-PFH3ROEH.cjs.map} +3 -3
  23. package/dist/lib/node/{chunk-ITLHJMWP.cjs → chunk-RXI576K3.cjs} +24 -7
  24. package/dist/lib/node/chunk-RXI576K3.cjs.map +7 -0
  25. package/dist/lib/node/edge/index.cjs +8 -8
  26. package/dist/lib/node/edge/index.cjs.map +1 -1
  27. package/dist/lib/node/index.cjs +85 -194
  28. package/dist/lib/node/index.cjs.map +4 -4
  29. package/dist/lib/node/meta.json +1 -1
  30. package/dist/types/src/ScriptPlugin.d.ts +1 -4
  31. package/dist/types/src/ScriptPlugin.d.ts.map +1 -1
  32. package/dist/types/src/bundler/bundler.d.ts +39 -0
  33. package/dist/types/src/bundler/bundler.d.ts.map +1 -0
  34. package/dist/types/src/bundler/bundler.test.d.ts +2 -0
  35. package/dist/types/src/bundler/bundler.test.d.ts.map +1 -0
  36. package/dist/types/src/bundler/index.d.ts +2 -0
  37. package/dist/types/src/bundler/index.d.ts.map +1 -0
  38. package/dist/types/src/compiler/compiler.browser.test.d.ts +2 -0
  39. package/dist/types/src/compiler/compiler.browser.test.d.ts.map +1 -0
  40. package/dist/types/src/compiler/compiler.d.ts +9 -34
  41. package/dist/types/src/compiler/compiler.d.ts.map +1 -1
  42. package/dist/types/src/components/DetailsPanel.d.ts +9 -0
  43. package/dist/types/src/components/DetailsPanel.d.ts.map +1 -0
  44. package/dist/types/src/components/ScriptBlock/FrameContainer/FrameContainer.d.ts +2 -2
  45. package/dist/types/src/components/ScriptBlock/FrameContainer/FrameContainer.d.ts.map +1 -1
  46. package/dist/types/src/components/ScriptBlock/ScriptBlock.d.ts.map +1 -1
  47. package/dist/types/src/components/ScriptEditor.d.ts +3 -2
  48. package/dist/types/src/components/ScriptEditor.d.ts.map +1 -1
  49. package/dist/types/src/components/Toolbar.d.ts +9 -7
  50. package/dist/types/src/components/Toolbar.d.ts.map +1 -1
  51. package/dist/types/src/components/Toolbar.stories.d.ts +1 -1
  52. package/dist/types/src/components/TypescriptEditor.d.ts +3 -1
  53. package/dist/types/src/components/TypescriptEditor.d.ts.map +1 -1
  54. package/dist/types/src/components/TypescriptEditor.stories.d.ts +1 -1
  55. package/dist/types/src/components/index.d.ts +2 -2
  56. package/dist/types/src/components/index.d.ts.map +1 -1
  57. package/dist/types/src/data/templates.md.d.ts +3 -0
  58. package/dist/types/src/data/templates.md.d.ts.map +1 -0
  59. package/dist/types/src/edge/functions.d.ts.map +1 -1
  60. package/dist/types/src/index.d.ts +1 -0
  61. package/dist/types/src/index.d.ts.map +1 -1
  62. package/dist/types/src/translations.d.ts +3 -2
  63. package/dist/types/src/translations.d.ts.map +1 -1
  64. package/package.json +38 -33
  65. package/src/ScriptPlugin.tsx +29 -10
  66. package/src/bundler/bundler.test.ts +44 -0
  67. package/src/bundler/bundler.ts +246 -0
  68. package/src/bundler/index.ts +5 -0
  69. package/src/compiler/compiler.browser.test.ts +18 -0
  70. package/src/compiler/compiler.ts +31 -190
  71. package/src/compiler/index.ts +1 -1
  72. package/src/components/DetailsPanel.tsx +218 -0
  73. package/src/components/ScriptBlock/FrameContainer/FrameContainer.tsx +2 -2
  74. package/src/components/ScriptBlock/ScriptBlock.tsx +10 -7
  75. package/src/components/ScriptEditor.tsx +71 -13
  76. package/src/components/Toolbar.tsx +69 -91
  77. package/src/components/TypescriptEditor.tsx +18 -4
  78. package/src/data/templates.md.ts +44 -0
  79. package/src/edge/functions.ts +9 -2
  80. package/src/index.ts +1 -0
  81. package/src/translations.ts +4 -3
  82. package/dist/lib/browser/ScriptEditor-MSQDR6D7.mjs +0 -241
  83. package/dist/lib/browser/ScriptEditor-MSQDR6D7.mjs.map +0 -7
  84. package/dist/lib/browser/chunk-D4O723CR.mjs.map +0 -7
  85. package/dist/lib/node/ScriptEditor-KBSACFYK.cjs +0 -258
  86. package/dist/lib/node/ScriptEditor-KBSACFYK.cjs.map +0 -7
  87. package/dist/lib/node/chunk-ITLHJMWP.cjs.map +0 -7
  88. package/dist/types/src/compiler/compiler.test.d.ts +0 -2
  89. package/dist/types/src/compiler/compiler.test.d.ts.map +0 -1
  90. package/src/compiler/compiler.test.ts +0 -17
  91. /package/dist/lib/browser/{TypescriptEditor-PIRIDDUS.mjs.map → TypescriptEditor-XTGRVCMY.mjs.map} +0 -0
@@ -0,0 +1,218 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useEffect, useRef, useState } from 'react';
6
+
7
+ import { log } from '@dxos/log';
8
+ import {
9
+ Avatar,
10
+ DensityProvider,
11
+ Icon,
12
+ Input,
13
+ type ThemedClassName,
14
+ Toolbar,
15
+ useControlledValue,
16
+ useDynamicRef,
17
+ useTranslation,
18
+ } from '@dxos/react-ui';
19
+ import { mx } from '@dxos/react-ui-theme';
20
+
21
+ import { SCRIPT_PLUGIN } from '../meta';
22
+
23
+ type State = 'pending' | 'responding';
24
+
25
+ type Item = { type: 'request' | 'response'; text: string };
26
+
27
+ export type DetailsPanelProps = ThemedClassName<{
28
+ functionUrl?: string;
29
+ binding?: string;
30
+ onBindingChange?: (binding: string) => void;
31
+ }>;
32
+
33
+ export const DetailsPanel = ({ classNames, functionUrl, binding: _binding, onBindingChange }: DetailsPanelProps) => {
34
+ const { t } = useTranslation(SCRIPT_PLUGIN);
35
+
36
+ const bindingInputRef = useRef<HTMLInputElement>(null);
37
+ const [binding, setBinding] = useControlledValue(_binding ?? '');
38
+ const handleBlur = () => {
39
+ onBindingChange?.(binding);
40
+ };
41
+
42
+ const inputRef = useRef<HTMLInputElement>(null);
43
+ const [input, setInput] = useState('');
44
+ const [result, setResult] = useState('');
45
+ const resultRef = useDynamicRef(result);
46
+ const [state, setState] = useState<State | null>(null);
47
+
48
+ // TODO(burdon): Persistent history (non-space ECHO data?)
49
+ const [history, setHistory] = useState<Item[]>([]);
50
+
51
+ const scrollerRef = useRef<HTMLDivElement>(null);
52
+ const handleScroll = () => {
53
+ if (scrollerRef.current) {
54
+ scrollerRef.current.scrollTop = scrollerRef.current.scrollHeight;
55
+ }
56
+ };
57
+
58
+ const controller = useRef<AbortController>();
59
+ useEffect(() => {
60
+ return () => {
61
+ handleStop();
62
+ };
63
+ }, []);
64
+
65
+ const handleStop = () => {
66
+ controller.current?.abort('stop');
67
+ controller.current = undefined;
68
+ };
69
+
70
+ // TODO(burdon): Halt streaming request?
71
+ const handleClear = () => {
72
+ handleStop();
73
+ setInput('');
74
+ setResult('');
75
+ setHistory([]);
76
+ inputRef.current?.focus();
77
+ };
78
+
79
+ const handleDone = () => {
80
+ controller.current = undefined;
81
+ setHistory((history) => [...history, { type: 'response', text: resultRef.current }]);
82
+ setResult('');
83
+ setState(null);
84
+ handleScroll();
85
+ };
86
+
87
+ const handleRun = async (input: string) => {
88
+ if (input.trim().length === 0) {
89
+ inputRef.current?.focus();
90
+ return;
91
+ }
92
+
93
+ setHistory((history) => [...history, { type: 'request', text: input }]);
94
+ setInput('');
95
+ setResult('');
96
+
97
+ // Returns a promise that resolves when a value has been received.
98
+ try {
99
+ setState('pending');
100
+
101
+ // Throws DOMException when aborted.
102
+ controller.current = new AbortController();
103
+ const response = await fetch(functionUrl!, {
104
+ signal: controller.current.signal,
105
+ method: 'POST',
106
+ body: input,
107
+ });
108
+
109
+ // TODO(burdon): Error handling.
110
+ if (!response.ok) {
111
+ setState(null);
112
+ return;
113
+ }
114
+
115
+ setState('responding');
116
+ const reader = response.body!.getReader();
117
+ const pump = async ({ done, value }: ReadableStreamReadResult<Uint8Array>): Promise<void> => {
118
+ setResult((result) => result + new TextDecoder().decode(value));
119
+ handleScroll();
120
+ if (!controller.current || done) {
121
+ handleDone();
122
+ return;
123
+ }
124
+
125
+ return reader.read().then(pump);
126
+ };
127
+
128
+ void reader
129
+ .read()
130
+ .then(pump)
131
+ .catch((err) => {
132
+ if (err.name !== 'AbortError') {
133
+ log.catch(err);
134
+ }
135
+ });
136
+ } catch (err) {
137
+ setState(null);
138
+ if (err !== 'stop') {
139
+ log.catch(err);
140
+ }
141
+ }
142
+ };
143
+
144
+ return (
145
+ <DensityProvider density='fine'>
146
+ <div className={mx('flex flex-col h-full overflow-hidden', classNames)}>
147
+ {/* TODO(burdon): Replace with Thread? */}
148
+ <div ref={scrollerRef} className='flex flex-col gap-6 h-full p-4 overflow-x-hidden overflow-y-auto'>
149
+ <MessageThread state={state} result={result} history={history} />
150
+ </div>
151
+
152
+ <Toolbar.Root classNames='p-2'>
153
+ <Input.Root>
154
+ <Input.TextInput
155
+ ref={inputRef}
156
+ autoFocus
157
+ placeholder={t('function request placeholder')}
158
+ value={input}
159
+ onChange={(ev) => setInput(ev.target.value)}
160
+ onKeyDown={(ev) => ev.key === 'Enter' && handleRun(input)}
161
+ />
162
+ </Input.Root>
163
+ <Input.Root>
164
+ <Input.TextInput
165
+ ref={bindingInputRef}
166
+ classNames='!w-[10rem]'
167
+ placeholder={t('binding placeholder')}
168
+ value={binding}
169
+ onChange={(ev) => setBinding(ev.target.value.toUpperCase())}
170
+ onBlur={handleBlur}
171
+ />
172
+ </Input.Root>
173
+ <Toolbar.Button onClick={() => handleRun(input)}>
174
+ <Icon icon='ph--play--regular' size={4} />
175
+ </Toolbar.Button>
176
+ <Toolbar.Button onClick={() => (state ? handleStop() : handleClear())}>
177
+ <Icon icon={state ? 'ph--stop--regular' : 'ph--trash--regular'} size={4} />
178
+ </Toolbar.Button>
179
+ </Toolbar.Root>
180
+ </div>
181
+ </DensityProvider>
182
+ );
183
+ };
184
+
185
+ const RobotAvatar = () => (
186
+ <Avatar.Root size={6} variant='circle'>
187
+ <Avatar.Frame>
188
+ <Avatar.Icon icon='ph--robot--regular' />
189
+ </Avatar.Frame>
190
+ </Avatar.Root>
191
+ );
192
+
193
+ // TODO(burdon): Replace with thread.
194
+ // TODO(burdon): Delete individual messages.
195
+ // TODO(burdon): Re-run question.
196
+ const MessageThread = ({ state, result, history }: { state: State | null; result: string; history: Item[] }) => {
197
+ return (
198
+ <>
199
+ {history.map(({ type, text }, i) => (
200
+ <div key={i} className='grid grid-cols-[3rem_1fr]'>
201
+ <div>{type === 'response' && <RobotAvatar />}</div>
202
+ <div className={mx('flex', type === 'request' && 'justify-end')}>
203
+ <span className={mx(type === 'request' && 'p-1 px-2 rounded-lg bg-hoverSurface')}>{text}</span>
204
+ </div>
205
+ </div>
206
+ ))}
207
+ <div className='grid grid-cols-[3rem_1fr]'>
208
+ <div>
209
+ {(state === 'pending' && <Icon icon='ph--spinner--regular' size={6} classNames='animate-spin-slow' />) ||
210
+ (result.length > 0 && <RobotAvatar />)}
211
+ </div>
212
+ <div>
213
+ <span>{result}</span>
214
+ </div>
215
+ </div>
216
+ </>
217
+ );
218
+ };
@@ -11,11 +11,11 @@ import { baseSurface, mx } from '@dxos/react-ui-theme';
11
11
  import { createProtoRpcPeer } from '@dxos/rpc';
12
12
  import { createIFramePort } from '@dxos/rpc-tunnel';
13
13
 
14
- import { type CompilerResult } from '../../../compiler';
14
+ import { type BundlerResult } from '../../../bundler';
15
15
 
16
16
  export type FrameContainerProps = {
17
17
  containerUrl: string;
18
- result: CompilerResult;
18
+ result: BundlerResult;
19
19
  debug?: boolean;
20
20
  };
21
21
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { Check, Play, Warning } from '@phosphor-icons/react';
6
6
  // @ts-ignore
7
- import esbuildWasmURL from 'esbuild-wasm/esbuild.wasm?url';
7
+ import wasmUrl from 'esbuild-wasm/esbuild.wasm?url';
8
8
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
9
9
 
10
10
  import { createDocAccessor, DocAccessor } from '@dxos/react-client/echo';
@@ -13,7 +13,7 @@ import { mx, getSize } from '@dxos/react-ui-theme';
13
13
 
14
14
  import { FrameContainer } from './FrameContainer';
15
15
  import { Splitter, SplitterSelector, type View } from './Splitter';
16
- import { Compiler, type CompilerResult, initializeCompiler } from '../../compiler';
16
+ import { Bundler, type BundlerResult, initializeBundler } from '../../bundler';
17
17
  import { type ScriptType } from '../../types';
18
18
  import { ScriptEditor } from '../ScriptEditor';
19
19
 
@@ -55,11 +55,14 @@ export const ScriptBlock = ({
55
55
  const [view, setView] = useState<View>(controlledView ?? 'editor');
56
56
  useEffect(() => handleSetView(controlledView ?? 'editor'), [controlledView]);
57
57
 
58
- const [result, setResult] = useState<CompilerResult>();
59
- const compiler = useMemo(() => new Compiler({ platform: 'browser', providedModules: PROVIDED_MODULES }), []);
58
+ const [result, setResult] = useState<BundlerResult>();
59
+ const bundler = useMemo(
60
+ () => new Bundler({ platform: 'browser', sandboxedModules: PROVIDED_MODULES, remoteModules: {} }),
61
+ [],
62
+ );
60
63
  useEffect(() => {
61
64
  // TODO(burdon): Create useCompiler hook (with initialization).
62
- void initializeCompiler({ wasmURL: esbuildWasmURL });
65
+ void initializeBundler({ wasmUrl });
63
66
  }, []);
64
67
  useEffect(() => {
65
68
  // TODO(burdon): Throttle and listen for update.
@@ -68,7 +71,7 @@ export const ScriptBlock = ({
68
71
  return;
69
72
  }
70
73
 
71
- const result = await compiler.compile(DocAccessor.getValue(source));
74
+ const result = await bundler.bundle(DocAccessor.getValue(source));
72
75
  setResult(result);
73
76
  });
74
77
 
@@ -90,7 +93,7 @@ export const ScriptBlock = ({
90
93
  if (!source) {
91
94
  return;
92
95
  }
93
- const result = await compiler.compile(DocAccessor.getValue(source));
96
+ const result = await bundler.bundle(DocAccessor.getValue(source));
94
97
  setResult(result);
95
98
  if (auto && view === 'editor') {
96
99
  setView('preview');
@@ -3,6 +3,9 @@
3
3
  //
4
4
 
5
5
  import { type DID } from 'iso-did/types';
6
+ import { format } from 'prettier';
7
+ import prettierPluginEstree from 'prettier/plugins/estree';
8
+ import prettierPluginTypescript from 'prettier/plugins/typescript';
6
9
  import React, { useCallback, useMemo, useState } from 'react';
7
10
 
8
11
  import { log } from '@dxos/log';
@@ -13,8 +16,10 @@ import { useTranslation } from '@dxos/react-ui';
13
16
  import { createDataExtensions, listener } from '@dxos/react-ui-editor';
14
17
  import { mx } from '@dxos/react-ui-theme';
15
18
 
19
+ import { DetailsPanel } from './DetailsPanel';
16
20
  import { Toolbar } from './Toolbar';
17
- import { TypescriptEditor } from './TypescriptEditor';
21
+ import { TypescriptEditor, type TypescriptEditorProps } from './TypescriptEditor';
22
+ import { Bundler } from '../bundler';
18
23
  import {
19
24
  getUserFunctionUrlInMetadata,
20
25
  publicKeyToDid,
@@ -27,9 +32,9 @@ import { FunctionType, type ScriptType } from '../types';
27
32
  export type ScriptEditorProps = {
28
33
  script: ScriptType;
29
34
  role?: string;
30
- };
35
+ } & Pick<TypescriptEditorProps, 'env'>;
31
36
 
32
- export const ScriptEditor = ({ script, role }: ScriptEditorProps) => {
37
+ export const ScriptEditor = ({ env, script, role }: ScriptEditorProps) => {
33
38
  const { t } = useTranslation(SCRIPT_PLUGIN);
34
39
  const client = useClient();
35
40
  const identity = useIdentity();
@@ -38,6 +43,7 @@ export const ScriptEditor = ({ script, role }: ScriptEditorProps) => {
38
43
  space,
39
44
  Filter.schema(FunctionType, (fn) => fn.source === script),
40
45
  );
46
+
41
47
  const extensions = useMemo(
42
48
  () => [
43
49
  listener({
@@ -56,6 +62,8 @@ export const ScriptEditor = ({ script, role }: ScriptEditorProps) => {
56
62
  ],
57
63
  [script, script.source, space, identity],
58
64
  );
65
+
66
+ const [showPanel, setShowPanel] = useState(false);
59
67
  const initialValue = useMemo(() => script.source?.content, [script.source]);
60
68
  const existingFunctionUrl = fn && getUserFunctionUrlInMetadata(getMeta(fn));
61
69
  const [error, setError] = useState<string>();
@@ -67,6 +75,24 @@ export const ScriptEditor = ({ script, role }: ScriptEditorProps) => {
67
75
  [fn],
68
76
  );
69
77
 
78
+ const handleFormat = useCallback(async () => {
79
+ if (!script.source) {
80
+ return;
81
+ }
82
+
83
+ try {
84
+ script.source.content = await format(script.source.content, {
85
+ parser: 'typescript',
86
+ plugins: [prettierPluginEstree, prettierPluginTypescript],
87
+ semi: true,
88
+ singleQuote: true,
89
+ });
90
+ } catch (err: any) {
91
+ // TODO(wittjosiah): Show error in UI.
92
+ log.catch(err);
93
+ }
94
+ }, [script.source]);
95
+
70
96
  const handleDeploy = useCallback(async () => {
71
97
  if (!script.source || !identity || !space) {
72
98
  return;
@@ -78,12 +104,22 @@ export const ScriptEditor = ({ script, role }: ScriptEditorProps) => {
78
104
  const existingFunctionId = existingFunctionUrl?.split('/').at(-1);
79
105
  const ownerDid = (existingFunctionUrl?.split('/').at(-2) as DID) ?? publicKeyToDid(identity.identityKey);
80
106
 
107
+ const bundler = new Bundler({
108
+ platform: 'browser',
109
+ sandboxedModules: [],
110
+ remoteModules: {},
111
+ });
112
+ const buildResult = await bundler.bundle(script.source.content);
113
+ if (buildResult.error || !buildResult.bundle) {
114
+ throw buildResult.error;
115
+ }
116
+
81
117
  const { result, functionId, functionVersionNumber, errorMessage } = await uploadWorkerFunction({
82
118
  clientConfig: client.config,
83
119
  halo: client.halo,
84
120
  ownerDid,
85
121
  functionId: existingFunctionId,
86
- source: script.source.content,
122
+ source: buildResult.bundle,
87
123
  });
88
124
  if (result !== 'success' || functionId === undefined || functionVersionNumber === undefined) {
89
125
  throw new Error(errorMessage);
@@ -103,25 +139,47 @@ export const ScriptEditor = ({ script, role }: ScriptEditorProps) => {
103
139
  }
104
140
  }, [fn, existingFunctionUrl, script, script.name, script.source]);
105
141
 
142
+ const functionUrl = useMemo(() => {
143
+ if (!existingFunctionUrl) {
144
+ return;
145
+ }
146
+
147
+ const baseUrl = new URL('functions/', client.config.values.runtime?.services?.edge?.url);
148
+
149
+ // Leading slashes cause the URL to be treated as an absolute path.
150
+ const relativeUrl = existingFunctionUrl.replace(/^\//, '');
151
+ const url = new URL(`./${relativeUrl}`, baseUrl.toString());
152
+ space && url.searchParams.set('spaceId', space.id);
153
+ url.protocol = 'https';
154
+ return url.toString();
155
+ }, [existingFunctionUrl, space]);
156
+
106
157
  return (
107
158
  <div
108
159
  role='none'
109
160
  className={mx(role === 'article' && 'row-span-2', 'flex flex-col is-full bs-full overflow-hidden')}
110
161
  >
111
- <Toolbar
112
- binding={fn?.binding ?? ''}
113
- onBindingChange={handleBindingChange}
114
- deployed={Boolean(existingFunctionUrl) && !script.changed}
115
- onDeploy={handleDeploy}
116
- functionUrl={existingFunctionUrl}
117
- error={error}
118
- />
119
162
  <TypescriptEditor
120
163
  id={script.id}
164
+ env={env}
121
165
  initialValue={initialValue}
122
166
  extensions={extensions}
123
- className='flex is-full bs-full overflow-hidden'
167
+ className='flex is-full bs-full overflow-hidden border-b border-separator'
168
+ />
169
+
170
+ <Toolbar
171
+ deployed={Boolean(existingFunctionUrl) && !script.changed}
172
+ functionUrl={functionUrl}
173
+ error={error}
174
+ showPanel={showPanel}
175
+ onDeploy={handleDeploy}
176
+ onFormat={handleFormat}
177
+ onTogglePanel={async () => setShowPanel((showDetails) => !showDetails)}
124
178
  />
179
+
180
+ {showPanel && (
181
+ <DetailsPanel functionUrl={functionUrl} binding={fn?.binding} onBindingChange={handleBindingChange} />
182
+ )}
125
183
  </div>
126
184
  );
127
185
  };
@@ -2,14 +2,14 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Check, Checks, CircleNotch, Link, PencilSimple, UploadSimple, WarningCircle, X } from '@phosphor-icons/react';
6
- import React, { useEffect, useRef, useState } from 'react';
5
+ import { Checks, CircleNotch, Link, MagicWand, UploadSimple, WarningCircle } from '@phosphor-icons/react';
6
+ import React, { useState } from 'react';
7
7
 
8
8
  import {
9
- Button,
10
9
  DensityProvider,
11
10
  ElevationProvider,
12
- Input,
11
+ Icon,
12
+ type ThemedClassName,
13
13
  Toolbar as NaturalToolbar,
14
14
  Tooltip,
15
15
  useTranslation,
@@ -18,48 +18,28 @@ import { getSize, mx } from '@dxos/react-ui-theme';
18
18
 
19
19
  import { SCRIPT_PLUGIN } from '../meta';
20
20
 
21
- export type ToolbarProps = {
22
- binding: string;
23
- onBindingChange: (binding: string) => void;
21
+ export type ToolbarProps = ThemedClassName<{
24
22
  deployed?: boolean;
25
- onDeploy?: () => Promise<void>;
26
- functionUrl?: string;
27
23
  error?: string;
28
- };
24
+ functionUrl?: string;
25
+ showPanel?: boolean;
26
+ onDeploy?: () => Promise<void>;
27
+ onFormat?: () => Promise<void>;
28
+ onTogglePanel?: () => Promise<void>;
29
+ }>;
29
30
 
30
31
  export const Toolbar = ({
31
- binding: _binding,
32
- onBindingChange,
32
+ classNames,
33
33
  deployed,
34
- onDeploy,
35
- functionUrl,
36
34
  error,
35
+ functionUrl,
36
+ showPanel,
37
+ onDeploy,
38
+ onFormat,
39
+ onTogglePanel,
37
40
  }: ToolbarProps) => {
38
41
  const { t } = useTranslation(SCRIPT_PLUGIN);
39
42
  const [pending, setPending] = useState(false);
40
- const [editing, setEditing] = useState(false);
41
- const [binding, setBinding] = useState(_binding);
42
- const inputRef = useRef<HTMLInputElement>(null);
43
-
44
- useEffect(() => {
45
- if (!editing && _binding !== binding) {
46
- setBinding(_binding);
47
- }
48
- }, [_binding]);
49
-
50
- const handleEdit = () => {
51
- setEditing(true);
52
- inputRef.current?.focus();
53
- };
54
-
55
- const handleSave = () => {
56
- setEditing(false);
57
- onBindingChange(binding);
58
- };
59
-
60
- const handleCancel = () => {
61
- setEditing(false);
62
- };
63
43
 
64
44
  const handleCopyLink = async () => {
65
45
  if (!functionUrl) {
@@ -75,49 +55,45 @@ export const Toolbar = ({
75
55
  setPending(false);
76
56
  };
77
57
 
58
+ // TODO(burdon): Factor out common toolbar components with sheet/editor?
78
59
  return (
79
60
  <DensityProvider density='fine'>
80
61
  <ElevationProvider elevation='chrome'>
81
- <NaturalToolbar.Root
82
- classNames='p-1 is-full shrink-0 overflow-x-auto overflow-y-hidden'
83
- style={{ contain: 'layout' }}
84
- >
85
- {/* TODO(wittjosiah): Binding input shouldn't be in the toolbar. */}
86
- <Input.Root>
87
- <Input.Label>{t('binding label')}</Input.Label>
88
- <Input.TextInput
89
- ref={inputRef}
90
- classNames='!is-auto'
91
- disabled={!editing}
92
- value={binding}
93
- onChange={(event) => setBinding(event.target.value.toUpperCase())}
94
- />
95
- {editing ? (
96
- <>
97
- <Button variant='ghost' onClick={handleCancel}>
98
- <X className={getSize(4)} />
99
- </Button>
100
- <Button variant='ghost' onClick={handleSave}>
101
- <Check className={getSize(4)} />
102
- </Button>
103
- </>
104
- ) : (
105
- <Tooltip.Root>
106
- <Tooltip.Trigger asChild>
107
- <Button variant='ghost' onClick={handleEdit}>
108
- <PencilSimple className={getSize(4)} />
109
- </Button>
110
- </Tooltip.Trigger>
111
- <Tooltip.Portal>
112
- <Tooltip.Content>
113
- <Tooltip.Arrow />
114
- {t('edit binding label')}
115
- </Tooltip.Content>
116
- </Tooltip.Portal>
117
- </Tooltip.Root>
118
- )}
119
- </Input.Root>
62
+ <NaturalToolbar.Root classNames={['p-1', classNames]} style={{ contain: 'layout' }}>
63
+ {onFormat && (
64
+ <Tooltip.Root>
65
+ <Tooltip.Trigger asChild>
66
+ <NaturalToolbar.Button variant='ghost' onClick={onFormat}>
67
+ <MagicWand className={getSize(4)} />
68
+ </NaturalToolbar.Button>
69
+ </Tooltip.Trigger>
70
+ <Tooltip.Portal>
71
+ <Tooltip.Content>
72
+ <Tooltip.Arrow />
73
+ {t('format label')}
74
+ </Tooltip.Content>
75
+ </Tooltip.Portal>
76
+ </Tooltip.Root>
77
+ )}
78
+
120
79
  <div role='separator' className='grow' />
80
+
81
+ {functionUrl && (
82
+ <Tooltip.Root>
83
+ <Tooltip.Trigger asChild>
84
+ <NaturalToolbar.Button variant='ghost' onClick={handleCopyLink}>
85
+ <Link className={getSize(4)} />
86
+ </NaturalToolbar.Button>
87
+ </Tooltip.Trigger>
88
+ <Tooltip.Portal>
89
+ <Tooltip.Content>
90
+ <Tooltip.Arrow />
91
+ {t('copy link label')}
92
+ </Tooltip.Content>
93
+ </Tooltip.Portal>
94
+ </Tooltip.Root>
95
+ )}
96
+
121
97
  {/* TODO(wittjosiah): Better treatment for status indicators in toolbar? */}
122
98
  {error ? (
123
99
  <Tooltip.Root>
@@ -144,21 +120,7 @@ export const Toolbar = ({
144
120
  </Tooltip.Portal>
145
121
  </Tooltip.Root>
146
122
  ) : null}
147
- {functionUrl && (
148
- <Tooltip.Root>
149
- <Tooltip.Trigger asChild>
150
- <NaturalToolbar.Button variant='ghost' onClick={handleCopyLink}>
151
- <Link className={getSize(4)} />
152
- </NaturalToolbar.Button>
153
- </Tooltip.Trigger>
154
- <Tooltip.Portal>
155
- <Tooltip.Content>
156
- <Tooltip.Arrow />
157
- {t('copy link label')}
158
- </Tooltip.Content>
159
- </Tooltip.Portal>
160
- </Tooltip.Root>
161
- )}
123
+
162
124
  <Tooltip.Root>
163
125
  <Tooltip.Trigger asChild>
164
126
  <NaturalToolbar.Button variant='ghost' onClick={handleDeploy} disabled={pending}>
@@ -176,6 +138,22 @@ export const Toolbar = ({
176
138
  </Tooltip.Content>
177
139
  </Tooltip.Portal>
178
140
  </Tooltip.Root>
141
+
142
+ {onTogglePanel && (
143
+ <Tooltip.Root>
144
+ <Tooltip.Trigger asChild>
145
+ <NaturalToolbar.Button variant='ghost' onClick={onTogglePanel}>
146
+ <Icon icon={showPanel ? 'ph--caret-up--regular' : 'ph--caret-down--regular'} size={4} />
147
+ </NaturalToolbar.Button>
148
+ </Tooltip.Trigger>
149
+ <Tooltip.Portal>
150
+ <Tooltip.Content>
151
+ <Tooltip.Arrow />
152
+ {t('toggle details label')}
153
+ </Tooltip.Content>
154
+ </Tooltip.Portal>
155
+ </Tooltip.Root>
156
+ )}
179
157
  </NaturalToolbar.Root>
180
158
  </ElevationProvider>
181
159
  </DensityProvider>