Skip to content

Commit a04835e

Browse files
authored
feat: new way to create processing + can change owner (#38)
* fix: don't return owner facets without showAll * feat: use stepper to create new processing * fix: prevent undefined pluginIcon * fix: vjsf didn't save config on change * feat: add metadata for plugins * fix: send plugin metadata * chore: title in primary color * feat: select category * chore: update packages * fix: plugins are never unlocked * chore: fetch less icons * fix: send config only when it is valid * feat: add a placeholder to autocomplete vjsf component * feat: change owner of a processing * feat: add tests to change owner * chore: don't show actions if not permissions to execute or admin the processing * chore: add a warning to change owner
1 parent 6c0b6cf commit a04835e

23 files changed

Lines changed: 880 additions & 463 deletions

api/config/custom-environment-variables.cjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ module.exports = {
55
processingsSeconds: 'DEFAULT_LIMITS_PROCESSINGS_SECONDS'
66
},
77
mongoUrl: 'MONGO_URL',
8+
pluginCategories: {
9+
__name: 'PLUGIN_CATEGORIES',
10+
__format: 'json'
11+
},
812
port: 'PORT',
913
privateDirectoryUrl: 'PRIVATE_DIRECTORY_URL',
1014
secretKeys: {

api/config/default.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module.exports = {
66
// -1 for unlimited storage
77
processingsSeconds: -1
88
},
9+
pluginCategories: ['Essentiels', 'Mes plugins', 'Données de références', 'Tests'],
910
privateDirectoryUrl: 'http://simple-directory:8080',
1011
secretKeys: {
1112
limits: null

api/config/type/schema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"dataDir",
1515
"privateDirectoryUrl",
1616
"mongoUrl",
17+
"pluginCategories",
1718
"port",
1819
"observer",
1920
"secretKeys"
@@ -36,6 +37,12 @@
3637
"mongoUrl": {
3738
"type": "string"
3839
},
40+
"pluginCategories": {
41+
"type": "array",
42+
"items": {
43+
"type": "string"
44+
}
45+
},
3946
"port": {
4047
"type": "number"
4148
},

api/src/config.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ config.util.makeImmutable(apiConfig)
1111

1212
export default apiConfig as ApiConfig
1313

14-
export type UiConfig = {}
15-
16-
export const uiConfig: UiConfig = {}
14+
export const uiConfig = {
15+
pluginCategories: apiConfig.pluginCategories,
16+
}
17+
export type UiConfig = typeof uiConfig

api/src/routers/plugins.ts

Lines changed: 77 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -31,32 +31,47 @@ fs.ensureDirSync(tmpDir)
3131

3232
tmp.setGracefulCleanup()
3333

34-
/**
35-
* For compatibility with old plugins
36-
*/
37-
const injectPluginNameConfig = (plugin: Plugin): Plugin => {
38-
if (!plugin.pluginConfigSchema.properties?.pluginName) {
39-
const version = plugin.distTag === 'latest' ? plugin.version : `${plugin.distTag} - ${plugin.version}`
40-
const defaultName = plugin.name.replace('@data-fair/processing-', '') + ' (' + version + ')'
41-
plugin.pluginConfigSchema.properties = plugin.pluginConfigSchema.properties || {}
42-
plugin.pluginConfigSchema.properties.pluginName = {
34+
const pluginMetadataSchema = {
35+
type: 'object',
36+
additionalProperties: false,
37+
properties: {
38+
name: {
4339
type: 'string',
4440
title: 'Nom du plugin',
45-
description: 'Nom du plugin affiché dans les traitements',
46-
default: defaultName
41+
layout: {
42+
cols: 4
43+
}
44+
},
45+
category: {
46+
type: 'string',
47+
title: 'Catégorie',
48+
enum: config.pluginCategories,
49+
layout: {
50+
cols: 4
51+
}
52+
},
53+
icon: {
54+
type: 'object',
55+
title: 'Icon',
56+
layout: {
57+
getItems: {
58+
url: 'https://koumoul.com/data-fair/api/v1/datasets/icons-mdi-latest/lines?q={q}&select=name,svg,svgPath&size=25',
59+
itemsResults: 'data.results',
60+
itemTitle: 'item.name',
61+
itemIcon: 'item.svg',
62+
itemKey: 'item.name'
63+
},
64+
cols: 4
65+
}
66+
},
67+
description: {
68+
type: 'string',
69+
title: 'Description du plugin'
4770
}
4871
}
49-
return plugin
50-
}
51-
52-
const preparePluginInfo = async (pluginInfo: Plugin): Promise<Plugin> => {
53-
pluginInfo = injectPluginNameConfig(pluginInfo)
54-
const pluginConfigPath = path.join(pluginsDir, pluginInfo.id + '-config.json')
55-
let customName = await fs.pathExists(pluginConfigPath) ? (await fs.readJson(pluginConfigPath)).pluginName : pluginInfo.pluginConfigSchema.properties.pluginName.default
56-
if (!customName) customName = pluginInfo.name.replace('@data-fair/processing-', '') + ' (' + pluginInfo.distTag + ' - ' + pluginInfo.version + ')'
57-
return { ...pluginInfo, customName }
5872
}
5973

74+
// Install a new plugin or update an existing one
6075
router.post('/', permissions.isSuperAdmin, async (req, res) => {
6176
const { body } = (await import('#doc/plugin/post-req/index.ts')).returnValid(req)
6277
const plugin = body as Record<string, any>
@@ -86,6 +101,8 @@ router.post('/', permissions.isSuperAdmin, async (req, res) => {
86101

87102
plugin.pluginConfigSchema = await fs.readJson(path.join(dir.path, 'src', 'plugin-config-schema.json'))
88103
plugin.processingConfigSchema = await fs.readJson(path.join(dir.path, 'src', 'processing-config-schema.json'))
104+
105+
// static metadata for the plugin
89106
await fs.writeFile(path.join(dir.path, 'plugin.json'), JSON.stringify(plugin, null, 2))
90107
await fs.move(dir.path, pluginDir, { overwrite: true })
91108
} finally {
@@ -96,14 +113,20 @@ router.post('/', permissions.isSuperAdmin, async (req, res) => {
96113
}
97114
}
98115

99-
const installedPlugin: Plugin = await preparePluginInfo(plugin as Plugin)
100-
installedPlugin.access = { public: false, privateAccess: [] }
101-
const accessFilePath = path.join(pluginsDir, installedPlugin.id + '-access.json')
102-
if (!await fs.pathExists(accessFilePath)) await fs.writeJson(accessFilePath, installedPlugin.access)
103-
const pluginConfigPath = path.join(pluginsDir, installedPlugin.id + '-config.json')
104-
if (await fs.pathExists(pluginConfigPath)) installedPlugin.config = await fs.readJson(pluginConfigPath)
116+
// set defaults access (don't overwrite if already exists (after an update))
117+
plugin.access = { public: false, privateAccess: [] }
118+
const accessFilePath = path.join(pluginsDir, plugin.id + '-access.json')
119+
if (!await fs.pathExists(accessFilePath)) await fs.writeJson(accessFilePath, plugin.access)
120+
121+
// return the existing config if it exists
122+
const pluginConfigPath = path.join(pluginsDir, plugin.id + '-config.json')
123+
if (await fs.pathExists(pluginConfigPath)) plugin.config = await fs.readJson(pluginConfigPath)
124+
125+
// return the existing metadata if it exists
126+
const pluginMetadataPath = path.join(pluginsDir, plugin.id + '-metadata.json')
127+
if (await fs.pathExists(pluginMetadataPath)) plugin.metadata = await fs.readJson(pluginMetadataPath)
105128

106-
res.send(installedPlugin)
129+
res.send(plugin)
107130
})
108131

109132
// List installed plugins (optional: privateAccess=[type]:[id])
@@ -136,7 +159,17 @@ router.get('/', async (req, res) => {
136159
} else {
137160
return res.status(400).send('privateAccess filter is required')
138161
}
139-
results.push(await preparePluginInfo(pluginInfo))
162+
163+
const pluginMetadataPath = path.join(pluginsDir, dir + '-metadata.json')
164+
const version = pluginInfo.distTag === 'latest' ? pluginInfo.version : `${pluginInfo.distTag} - ${pluginInfo.version}`
165+
pluginInfo.metadata = {
166+
name: pluginInfo.name.replace('@data-fair/processing-', '') + ' (' + version + ')',
167+
description: pluginInfo.description,
168+
...(await fs.pathExists(pluginMetadataPath) ? await fs.readJson(pluginMetadataPath) : {})
169+
}
170+
pluginInfo.pluginMetadataSchema = pluginMetadataSchema
171+
172+
results.push(pluginInfo)
140173
}
141174

142175
const aggregationResult = (
@@ -160,7 +193,14 @@ router.get('/:id', async (req, res) => {
160193
await session.reqAuthenticated(req)
161194
try {
162195
const pluginInfo: Plugin = await fs.readJson(resolvePath(pluginsDir, path.join(req.params.id, 'plugin.json')))
163-
res.send(await preparePluginInfo(pluginInfo))
196+
const pluginMetadataPath = path.join(pluginsDir, req.params.id + '-metadata.json')
197+
const version = pluginInfo.distTag === 'latest' ? pluginInfo.version : `${pluginInfo.distTag} - ${pluginInfo.version}`
198+
pluginInfo.metadata = {
199+
name: pluginInfo.name.replace('@data-fair/processing-', '') + ' (' + version + ')',
200+
description: pluginInfo.description,
201+
...(await fs.pathExists(pluginMetadataPath) ? await fs.readJson(pluginMetadataPath) : {})
202+
}
203+
res.send(pluginInfo)
164204
} catch (e: any) {
165205
if (e.code === 'ENOENT') res.status(404).send('Plugin not found')
166206
else throw e
@@ -183,6 +223,14 @@ router.put('/:id/config', permissions.isSuperAdmin, async (req, res) => {
183223
res.send(req.body)
184224
})
185225

226+
router.put('/:id/metadata', permissions.isSuperAdmin, async (req, res) => {
227+
const validate = ajv.compile(pluginMetadataSchema)
228+
const valid = validate(req.body)
229+
if (!valid) return res.status(400).send(validate.errors)
230+
await fs.writeJson(path.join(pluginsDir, req.params.id + '-metadata.json'), req.body)
231+
res.send(req.body)
232+
})
233+
186234
router.put('/:id/access', permissions.isSuperAdmin, async (req, res) => {
187235
await fs.writeJson(path.join(pluginsDir, req.params.id + '-access.json'), req.body)
188236
res.send(req.body)

api/src/routers/processings.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,9 +262,21 @@ router.patch('/:id', async (req, res) => {
262262

263263
// Restrict the parts of the processing that can be edited by API
264264
const acceptedParts = Object.keys(processingSchema.properties)
265-
.filter(k => sessionState.user.adminMode || !(processingSchema.properties)[k].readOnly)
265+
.filter(k => sessionState.user.adminMode || !(processingSchema.properties)[k].readOnly || 'owner')
266266
for (const key in req.body) {
267267
if (!acceptedParts.includes(key)) return res.status(400).send('Unsupported patch part ' + key)
268+
if (key === 'owner') {
269+
// check if the user has the right to change to this owner
270+
const isAdmin =
271+
(req.body.owner.type === 'user' && sessionState.user.id === req.body.owner.id) ||
272+
sessionState.user.organizations.some(o =>
273+
o.id === req.body.owner.id && (!o.department || o.department === req.body.owner.department) && o.role === 'admin'
274+
)
275+
276+
if (!isAdmin) {
277+
return res.status(403).send('No permission to change the owner to ' + req.body.owner)
278+
}
279+
}
268280
}
269281
req.body.updated = {
270282
id: sessionState.user.id,

api/types/plugin/schema.js

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,19 @@ export default {
99
additionalProperties: false,
1010
required: [
1111
'name',
12-
'customName',
1312
'description',
1413
'version',
1514
'distTag',
1615
'id',
16+
'metadata',
1717
'pluginConfigSchema',
18+
'pluginMetadataSchema',
1819
'processingConfigSchema'
1920
],
2021
properties: {
2122
name: {
2223
type: 'string'
2324
},
24-
customName: {
25-
type: 'string'
26-
},
2725
description: {
2826
type: 'string'
2927
},
@@ -37,25 +35,10 @@ export default {
3735
type: 'string'
3836
},
3937
pluginConfigSchema: {
40-
type: 'object',
41-
required: ['properties'],
42-
properties: {
43-
properties: {
44-
type: 'object',
45-
required: ['pluginName'],
46-
properties: {
47-
pluginName: {
48-
type: 'object',
49-
required: ['default'],
50-
properties: {
51-
default: {
52-
type: 'string'
53-
}
54-
}
55-
},
56-
}
57-
}
58-
}
38+
type: 'object'
39+
},
40+
pluginMetadataSchema: {
41+
type: 'object'
5942
},
6043
processingConfigSchema: {
6144
type: 'object'
@@ -87,6 +70,25 @@ export default {
8770
}
8871
}
8972
}
73+
},
74+
metadata: {
75+
type: 'object',
76+
additionalProperties: false,
77+
required: ['name', 'description', 'category', 'icon'],
78+
properties: {
79+
name: {
80+
type: 'string'
81+
},
82+
description: {
83+
type: 'string'
84+
},
85+
category: {
86+
type: 'string'
87+
},
88+
icon: {
89+
type: 'string'
90+
}
91+
}
9092
}
9193
}
9294
}

0 commit comments

Comments
 (0)