Skip to content

Commit 933b179

Browse files
committed
Add update feature to plugins #73
1 parent 434c959 commit 933b179

File tree

33 files changed

+1711
-432
lines changed

33 files changed

+1711
-432
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ See [STATUS.md](server/STATUS.md) to learn more about which features will remain
1818
- [#658](https://github.com/atomicdata-dev/atomic-server/issues/658) Added JSON datatype.
1919
- [#1024](https://github.com/atomicdata-dev/atomic-server/issues/1024) Added URI datatype.
2020
- [#998](https://github.com/atomicdata-dev/atomic-server/issues/998) Added YJS datatype.
21+
- [#851](https://github.com/atomicdata-dev/atomic-server/issues/851) Deleting file resources now also deletes the file from the filesystem.
2122
BREAKING: [#1107](https://github.com/atomicdata-dev/atomic-server/issues/1107) Named nested resources are no longer supported. Value::Resource and SubResource::Resource have been removed. If you need to include multiple resources in a response use an array.
2223
BREAKING: `store.get_resource_extended()` now returns a `ResourceResponse` instead of a `Resource` due to the removal of named nested resources. Use `.into()` or `.to_single()` to convert to a `Resource`.
2324

browser/data-browser/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"@wuchale/vite-plugin": "^0.16.3",
5555
"@zip.js/zip.js": "^2.8.15",
5656
"ai": "^5.0.101",
57+
"ajv": "^8.17.1",
5758
"clsx": "^2.1.1",
5859
"codemirror-json-schema": "^0.8.1",
5960
"downshift": "^9.0.10",

browser/data-browser/src/chunks/CodeEditor/AsyncJSONEditor.tsx

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,14 @@ import {
1414
handleRefresh,
1515
} from 'codemirror-json-schema';
1616
import { linter, lintGutter, type Diagnostic } from '@codemirror/lint';
17-
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from 'react';
17+
import {
18+
useCallback,
19+
useEffect,
20+
useMemo,
21+
useRef,
22+
useState,
23+
type RefObject,
24+
} from 'react';
1825
import { styled, useTheme } from 'styled-components';
1926
import type { JSONSchema7 } from 'ai';
2027
import { addIf } from '@helpers/addIf';
@@ -65,10 +72,9 @@ const AsyncJSONEditor: React.FC<JSONEditorProps> = ({
6572
const [reports, setReports] = useState<Reports>({});
6673

6774
const reporter = useCallback((key: string, valid: boolean) => {
68-
setReports((prev) => ({ ...prev, [key]: valid }))
75+
setReports(prev => ({ ...prev, [key]: valid }));
6976
}, []);
7077

71-
7278
useOnValueChange(() => {
7379
// We can't move this to the report event because we need the most up to date reports which are modified in that event.
7480
onValidationChange?.(Object.values(reports).every(Boolean));
@@ -83,8 +89,18 @@ const AsyncJSONEditor: React.FC<JSONEditorProps> = ({
8389
[onChange],
8490
);
8591

86-
const jsonLinter = useHookIntoValidator('json', jsonParserLinterRef, reporter, !!required);
87-
const schemaLinter = useHookIntoValidator('jsonSchema', schemaLinterRef, reporter, true);
92+
const jsonLinter = useHookIntoValidator(
93+
'json',
94+
jsonParserLinterRef,
95+
reporter,
96+
!!required,
97+
);
98+
const schemaLinter = useHookIntoValidator(
99+
'jsonSchema',
100+
schemaLinterRef,
101+
reporter,
102+
true,
103+
);
88104

89105
const extensions = useMemo(
90106
() => [
@@ -93,7 +109,9 @@ const AsyncJSONEditor: React.FC<JSONEditorProps> = ({
93109
delay: 300,
94110
}),
95111
lintGutter(),
96-
addIf(!!schema,
112+
// If a schema is provided we add all the JSON Schema tooling.
113+
addIf(
114+
!!schema,
97115
linter(schemaLinter, {
98116
needsRefresh: handleRefresh,
99117
}),
@@ -102,7 +120,7 @@ const AsyncJSONEditor: React.FC<JSONEditorProps> = ({
102120
}),
103121
hoverTooltip(jsonSchemaHover()),
104122
stateExtensions(schema),
105-
)
123+
),
106124
],
107125
[jsonLinter, schemaLinter, schema],
108126
);
@@ -147,10 +165,15 @@ const AsyncJSONEditor: React.FC<JSONEditorProps> = ({
147165
);
148166
};
149167

150-
function useHookIntoValidator(key: string, validator: RefObject<(view: EditorView) => Diagnostic[]>, reporter: (key: string, valid: boolean) => void, required: boolean): (view: EditorView) => Diagnostic[] {
151-
const lastDiagnostics = useRef<Diagnostic[]>([]);
168+
function useHookIntoValidator(
169+
key: string,
170+
validator: RefObject<(view: EditorView) => Diagnostic[]>,
171+
reporter: (key: string, valid: boolean) => void,
172+
required: boolean,
173+
): (view: EditorView) => Diagnostic[] {
174+
const lastDiagnostics = useRef<Diagnostic[]>([]);
152175

153-
const validationLinter = useMemo(() => {
176+
const validationLinter = useMemo(() => {
154177
return (view: EditorView) => {
155178
const isEmpty = view.state.doc.length === 0;
156179
let diagnostics = validator.current(view);
@@ -207,15 +230,18 @@ const CodeEditorWrapper = styled.div`
207230
padding: ${p => p.theme.size(2)};
208231
box-shadow: ${p => p.theme.boxShadowSoft};
209232
border-radius: ${p => p.theme.radius};
210-
border: ${p => p.theme.darkMode ? '1px solid' : 'none'} ${p => p.theme.colors.bg2};
233+
border: ${p => (p.theme.darkMode ? '1px solid' : 'none')};
234+
${p => p.theme.colors.bg2};
211235
212236
& .cm-tooltip-arrow {
213237
display: none;
214238
}
215239
}
216240
217241
& .cm-gutters {
218-
background: transparent;
242+
background: ${p => p.theme.colors.bg};
243+
border-top-left-radius: ${p => p.theme.radius};
244+
border-bottom-left-radius: ${p => p.theme.radius};
219245
min-height: 150px;
220246
221247
& .cm-gutterElement {
@@ -241,7 +267,7 @@ const CodeEditorWrapper = styled.div`
241267
& > ul > li {
242268
background-color: none;
243269
padding: ${p => p.theme.size(2)} !important;
244-
margin:0;
270+
margin: 0;
245271
246272
&:first-of-type {
247273
border-top-left-radius: ${p => p.theme.radius};

browser/data-browser/src/views/Drive/NewPluginButton.tsx renamed to browser/data-browser/src/chunks/Plugins/NewPluginButton.tsx

Lines changed: 37 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,59 @@
11
import { Button } from '@components/Button';
22
import { Dialog, useDialog } from '@components/Dialog';
3-
import type { Resource, Server } from '@tomic/react';
3+
import type { JSONValue, Resource, Server } from '@tomic/react';
44
import { useId, useRef, useState } from 'react';
55
import { FaPlus } from 'react-icons/fa6';
6-
import {
7-
TextWriter,
8-
Uint8ArrayReader,
9-
ZipReader,
10-
type Entry,
11-
} from '@zip.js/zip.js';
126
import { styled } from 'styled-components';
137
import { Column, Row } from '@components/Row';
148
import { JSONEditor } from '@components/JSONEditor';
159
import Markdown from '@components/datatypes/Markdown';
16-
import { useCreatePlugin, type PluginMetadata } from './createPlugin';
10+
import { useCreatePlugin } from '@views/Plugin/createPlugin';
11+
import { readZip, type PluginMetadata } from './plugins';
12+
import { ConfigReference } from '@views/Plugin/ConfigReference';
1713

1814
interface NewPluginButtonProps {
1915
drive: Resource<Server.Drive>;
2016
}
2117

22-
export const NewPluginButton: React.FC<NewPluginButtonProps> = ({ drive }) => {
18+
const NewPluginButton: React.FC<NewPluginButtonProps> = ({ drive }) => {
2319
const configLabelId = useId();
2420
const [error, setError] = useState<string>();
2521
const [file, setFile] = useState<File | null>(null);
2622
const fileInputRef = useRef<HTMLInputElement>(null);
2723
const [metadata, setMetadata] = useState<PluginMetadata>();
2824
const [configValid, setConfigValid] = useState(true);
25+
const [config, setConfig] = useState<JSONValue>();
26+
2927
const { createPluginResource, installPlugin } = useCreatePlugin();
28+
29+
const reset = () => {
30+
setError(undefined);
31+
setFile(null);
32+
setMetadata(undefined);
33+
setConfig(undefined);
34+
setConfigValid(true);
35+
fileInputRef.current!.value = '';
36+
};
37+
3038
const [dialogProps, show, hide] = useDialog({
31-
onCancel: () => {
32-
setError(undefined);
33-
setFile(null);
34-
setMetadata(undefined);
35-
fileInputRef.current!.value = '';
36-
},
39+
onCancel: reset,
3740
onSuccess: async () => {
3841
if (!metadata || !file) {
3942
return setError('Please fill in all fields');
4043
}
4144

4245
try {
43-
const plugin = await createPluginResource({ metadata, file, drive });
46+
const plugin = await createPluginResource({
47+
metadata,
48+
file,
49+
drive,
50+
config,
51+
});
4452
await installPlugin(plugin, drive);
4553
} catch (err) {
4654
setError(`Failed to install plugin, error: ${err.message}`);
4755
} finally {
48-
setError(undefined);
49-
setFile(null);
50-
setMetadata(undefined);
51-
fileInputRef.current!.value = '';
56+
reset();
5257
}
5358
},
5459
});
@@ -62,6 +67,7 @@ export const NewPluginButton: React.FC<NewPluginButtonProps> = ({ drive }) => {
6267
try {
6368
const readMetadata = await readZip(targetFile);
6469
setMetadata(readMetadata);
70+
setConfig(readMetadata.defaultConfig);
6571
setFile(targetFile);
6672
setError(undefined);
6773
show();
@@ -112,11 +118,20 @@ export const NewPluginButton: React.FC<NewPluginButtonProps> = ({ drive }) => {
112118
<JSONEditor
113119
labelId={configLabelId}
114120
initialValue={JSON.stringify(metadata.defaultConfig, null, 2)}
115-
onChange={() => {}}
121+
onChange={val => {
122+
try {
123+
setConfig(JSON.parse(val));
124+
} catch (e) {
125+
// Do nothing
126+
}
127+
}}
116128
schema={metadata.configSchema}
117129
showErrorStyling={!configValid}
118130
onValidationChange={setConfigValid}
119131
/>
132+
{metadata.configSchema && (
133+
<ConfigReference schema={metadata.configSchema} />
134+
)}
120135
</Column>
121136
)}
122137
{!metadata && (
@@ -149,50 +164,7 @@ export const NewPluginButton: React.FC<NewPluginButtonProps> = ({ drive }) => {
149164
);
150165
};
151166

152-
async function readZip(file: File): Promise<PluginMetadata> {
153-
const zip = new ZipReader(new Uint8ArrayReader(await file.bytes()));
154-
const entries = await zip.getEntries();
155-
156-
if (!validateZip(entries)) {
157-
throw new Error('Invalid plugin zip file.');
158-
}
159-
160-
for (const entry of entries) {
161-
if (!entry.directory && entry.filename === 'plugin.json') {
162-
const metadata = await entry.getData(new TextWriter());
163-
164-
return JSON.parse(metadata) as PluginMetadata;
165-
}
166-
}
167-
168-
throw new Error('Plugin metadata not found in zip file.');
169-
}
170-
171-
function validateZip(entries: Entry[]): boolean {
172-
const allowedRootFiles = ['plugin.json', 'plugin.wasm'];
173-
let foundWasm = false;
174-
let foundJson = false;
175-
176-
for (const entry of entries) {
177-
if (entry.filename.startsWith('assets/')) {
178-
continue;
179-
}
180-
181-
if (!allowedRootFiles.includes(entry.filename)) {
182-
return false;
183-
}
184-
185-
if (entry.filename === 'plugin.wasm') {
186-
foundWasm = true;
187-
}
188-
189-
if (entry.filename === 'plugin.json') {
190-
foundJson = true;
191-
}
192-
}
193-
194-
return foundWasm && foundJson;
195-
}
167+
export default NewPluginButton;
196168

197169
const PluginName = styled.span`
198170
font-weight: bold;

0 commit comments

Comments
 (0)