@geode/opengeodeweb-front 10.30.0-rc.14 → 10.30.0-rc.16
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/app/components/Viewer/Generic/Model/BlocksOptions.vue +2 -2
- package/app/components/Viewer/Generic/Model/CornersOptions.vue +2 -2
- package/app/components/Viewer/Generic/Model/LinesOptions.vue +2 -2
- package/app/components/Viewer/Generic/Model/SurfacesOptions.vue +2 -2
- package/app/components/Viewer/ObjectTree/Base/CommonTreeView.vue +1 -0
- package/app/components/Viewer/ObjectTree/Base/TreeRow.vue +8 -0
- package/app/components/Viewer/ObjectTree/Views/GlobalObjects.vue +3 -0
- package/app/components/Viewer/Options/AttributeSelector.vue +13 -6
- package/app/components/Viewer/Options/ColoringTypeSelector.vue +7 -7
- package/app/utils/extension.js +4 -3
- package/package.json +1 -1
- package/server/api/cloud/extensions/run.post.js +72 -0
- package/server/api/{microservice → local}/extensions/run.post.js +10 -46
- package/server/utils/cloud.js +107 -1
- package/server/utils/extension.js +50 -0
- package/server/utils/microservices.js +51 -26
- package/server/utils/path.js +10 -0
- /package/server/api/{microservice → local}/extensions/kill.post.js +0 -0
|
@@ -238,7 +238,7 @@ const polyhedronSchema = back_schemas.opengeodeweb_back.model_component_polyhedr
|
|
|
238
238
|
<VisibilitySwitch data-testid="modelBlocksVisibilitySwitch" v-model="blocksVisibility" />
|
|
239
239
|
<ViewerOptionsColoringTypeSelector
|
|
240
240
|
:id="modelId"
|
|
241
|
-
:
|
|
241
|
+
:componentIds="targetBlockIds"
|
|
242
242
|
v-model:coloring_style_key="blocksActiveColoring"
|
|
243
243
|
v-model:color="blocksColor"
|
|
244
244
|
v-model:vertex_attribute_name="blocksVertexAttributeName"
|
|
@@ -259,7 +259,7 @@ const polyhedronSchema = back_schemas.opengeodeweb_back.model_component_polyhedr
|
|
|
259
259
|
<VisibilitySwitch v-model="blockVisibility" />
|
|
260
260
|
<ViewerOptionsColoringTypeSelector
|
|
261
261
|
:id="modelId"
|
|
262
|
-
:
|
|
262
|
+
:componentIds="[blockId]"
|
|
263
263
|
v-model:coloring_style_key="blockActiveColoring"
|
|
264
264
|
v-model:color="blockColor"
|
|
265
265
|
v-model:vertex_attribute_name="vertexAttributeName"
|
|
@@ -159,7 +159,7 @@ const vertexSchema = back_schemas.opengeodeweb_back.model_component_vertex_attri
|
|
|
159
159
|
<VisibilitySwitch data-testid="modelCornersVisibilitySwitch" v-model="cornersVisibility" />
|
|
160
160
|
<ViewerOptionsColoringTypeSelector
|
|
161
161
|
:id="modelId"
|
|
162
|
-
:
|
|
162
|
+
:componentIds="targetCornerIds"
|
|
163
163
|
v-model:coloring_style_key="cornersActiveColoring"
|
|
164
164
|
v-model:color="cornersColor"
|
|
165
165
|
v-model:vertex_attribute_name="cornersVertexAttributeName"
|
|
@@ -176,7 +176,7 @@ const vertexSchema = back_schemas.opengeodeweb_back.model_component_vertex_attri
|
|
|
176
176
|
<VisibilitySwitch v-model="cornerVisibility" />
|
|
177
177
|
<ViewerOptionsColoringTypeSelector
|
|
178
178
|
:id="modelId"
|
|
179
|
-
:
|
|
179
|
+
:componentIds="[cornerId]"
|
|
180
180
|
v-model:coloring_style_key="cornerActiveColoring"
|
|
181
181
|
v-model:color="cornerColor"
|
|
182
182
|
v-model:vertex_attribute_name="vertexAttributeName"
|
|
@@ -234,7 +234,7 @@ const edgeSchema = back_schemas.opengeodeweb_back.model_component_edge_attribute
|
|
|
234
234
|
<VisibilitySwitch data-testid="modelLinesVisibilitySwitch" v-model="linesVisibility" />
|
|
235
235
|
<ViewerOptionsColoringTypeSelector
|
|
236
236
|
:id="modelId"
|
|
237
|
-
:
|
|
237
|
+
:componentIds="targetLineIds"
|
|
238
238
|
v-model:coloring_style_key="linesActiveColoring"
|
|
239
239
|
v-model:color="linesColor"
|
|
240
240
|
v-model:vertex_attribute_name="linesVertexAttributeName"
|
|
@@ -255,7 +255,7 @@ const edgeSchema = back_schemas.opengeodeweb_back.model_component_edge_attribute
|
|
|
255
255
|
<VisibilitySwitch v-model="lineVisibility" />
|
|
256
256
|
<ViewerOptionsColoringTypeSelector
|
|
257
257
|
:id="modelId"
|
|
258
|
-
:
|
|
258
|
+
:componentIds="[lineId]"
|
|
259
259
|
v-model:coloring_style_key="lineActiveColoring"
|
|
260
260
|
v-model:color="lineColor"
|
|
261
261
|
v-model:vertex_attribute_name="vertexAttributeName"
|
|
@@ -242,7 +242,7 @@ const polygonSchema = back_schemas.opengeodeweb_back.model_component_polygon_att
|
|
|
242
242
|
<VisibilitySwitch data-testid="modelSurfacesVisibilitySwitch" v-model="surfacesVisibility" />
|
|
243
243
|
<ViewerOptionsColoringTypeSelector
|
|
244
244
|
:id="modelId"
|
|
245
|
-
:
|
|
245
|
+
:componentIds="targetSurfaceIds"
|
|
246
246
|
v-model:coloring_style_key="surfacesActiveColoring"
|
|
247
247
|
v-model:color="surfacesColor"
|
|
248
248
|
v-model:vertex_attribute_name="surfacesVertexAttributeName"
|
|
@@ -263,7 +263,7 @@ const polygonSchema = back_schemas.opengeodeweb_back.model_component_polygon_att
|
|
|
263
263
|
<VisibilitySwitch v-model="surfaceVisibility" />
|
|
264
264
|
<ViewerOptionsColoringTypeSelector
|
|
265
265
|
:id="modelId"
|
|
266
|
-
:
|
|
266
|
+
:componentIds="[surfaceId]"
|
|
267
267
|
v-model:coloring_style_key="surfaceActiveColoring"
|
|
268
268
|
v-model:color="surfaceColor"
|
|
269
269
|
v-model:vertex_attribute_name="vertexAttributeName"
|
|
@@ -48,6 +48,7 @@ function handleRowClick(event) {
|
|
|
48
48
|
<div class="d-flex align-center flex-shrink-0">
|
|
49
49
|
<v-icon
|
|
50
50
|
v-if="!item.isLeaf"
|
|
51
|
+
:data-testid="item.isOpen ? 'collapseTreeRowButton' : 'expandTreeRowButton'"
|
|
51
52
|
:icon="item.isOpen ? 'mdi-menu-down' : 'mdi-menu-right'"
|
|
52
53
|
class="me-1"
|
|
53
54
|
color="black"
|
|
@@ -69,6 +70,13 @@ function handleRowClick(event) {
|
|
|
69
70
|
/>
|
|
70
71
|
<v-btn
|
|
71
72
|
v-else-if="showEyeButton"
|
|
73
|
+
:data-testid="
|
|
74
|
+
getIndeterminate(item.raw)
|
|
75
|
+
? 'indeterminateObjectEyeButton'
|
|
76
|
+
: isSelected(item.raw)
|
|
77
|
+
? 'visibleObjectEyeButton'
|
|
78
|
+
: 'hiddenObjectEyeButton'
|
|
79
|
+
"
|
|
72
80
|
:icon="
|
|
73
81
|
getIndeterminate(item.raw)
|
|
74
82
|
? 'mdi-eye-minus-outline'
|
|
@@ -182,6 +182,7 @@ function expandAll() {
|
|
|
182
182
|
<template v-if="item.geode_object_type !== 'HorizonStack3D'">
|
|
183
183
|
<v-btn
|
|
184
184
|
v-if="item.viewer_type"
|
|
185
|
+
data-testid="focusObjectButton"
|
|
185
186
|
icon="mdi-target"
|
|
186
187
|
size="medium"
|
|
187
188
|
variant="text"
|
|
@@ -190,6 +191,7 @@ function expandAll() {
|
|
|
190
191
|
/>
|
|
191
192
|
<v-btn
|
|
192
193
|
v-if="isModel(item)"
|
|
194
|
+
data-testid="expandModelComponentsButton"
|
|
193
195
|
icon="mdi-magnify-expand"
|
|
194
196
|
size="medium"
|
|
195
197
|
class="ml-2"
|
|
@@ -206,6 +208,7 @@ function expandAll() {
|
|
|
206
208
|
/>
|
|
207
209
|
<v-btn
|
|
208
210
|
v-if="isModel(item) && hasCollectionsMap[item.id]"
|
|
211
|
+
data-testid="expandModelCollectionsButton"
|
|
209
212
|
icon="mdi-format-list-group"
|
|
210
213
|
size="medium"
|
|
211
214
|
class="ml-2"
|
|
@@ -9,9 +9,9 @@ const attributeItem = defineModel("attributeItem", { type: Number });
|
|
|
9
9
|
const attributeRange = defineModel("attributeRange", { type: Array });
|
|
10
10
|
const attributeColorMap = defineModel("attributeColorMap", { type: String });
|
|
11
11
|
|
|
12
|
-
const { id,
|
|
12
|
+
const { id, componentIds, schema } = defineProps({
|
|
13
13
|
id: { type: String, required: true },
|
|
14
|
-
|
|
14
|
+
componentIds: { type: Array, default: undefined },
|
|
15
15
|
schema: { type: Object, required: true },
|
|
16
16
|
});
|
|
17
17
|
|
|
@@ -64,14 +64,21 @@ function resetRange() {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
function hasSelectedComponent(components) {
|
|
68
|
+
return Array.isArray(components) && components.length > 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
67
71
|
function getAttributes() {
|
|
68
|
-
|
|
72
|
+
const requiresComponent = schema.properties.component_ids !== undefined;
|
|
73
|
+
if (requiresComponent && !hasSelectedComponent(componentIds)) {
|
|
69
74
|
return;
|
|
70
75
|
}
|
|
76
|
+
|
|
71
77
|
const params = { id };
|
|
72
|
-
if (
|
|
73
|
-
params.
|
|
78
|
+
if (requiresComponent) {
|
|
79
|
+
params.component_ids = componentIds;
|
|
74
80
|
}
|
|
81
|
+
|
|
75
82
|
backStore.request(
|
|
76
83
|
{ schema, params },
|
|
77
84
|
{
|
|
@@ -87,7 +94,7 @@ onMounted(() => {
|
|
|
87
94
|
});
|
|
88
95
|
|
|
89
96
|
watch(
|
|
90
|
-
() => [id,
|
|
97
|
+
() => [id, componentIds, schema],
|
|
91
98
|
() => {
|
|
92
99
|
getAttributes();
|
|
93
100
|
},
|
|
@@ -36,9 +36,9 @@ const polyhedron_attribute_color_map = defineModel("polyhedron_attribute_color_m
|
|
|
36
36
|
type: String,
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
const { id,
|
|
39
|
+
const { id, componentIds, capabilities, schemas, allowRandom } = defineProps({
|
|
40
40
|
id: { type: String, required: true },
|
|
41
|
-
|
|
41
|
+
componentIds: { type: Array, default: undefined },
|
|
42
42
|
capabilities: {
|
|
43
43
|
type: Object,
|
|
44
44
|
default: () => ({}),
|
|
@@ -215,7 +215,7 @@ watch(
|
|
|
215
215
|
v-model:attributeRange="vertex_attribute_range"
|
|
216
216
|
v-model:attributeColorMap="vertex_attribute_color_map"
|
|
217
217
|
:id="id"
|
|
218
|
-
:
|
|
218
|
+
:componentIds="componentIds"
|
|
219
219
|
:schema="vertexSchema"
|
|
220
220
|
/>
|
|
221
221
|
</template>
|
|
@@ -226,7 +226,7 @@ watch(
|
|
|
226
226
|
v-model:attributeRange="edge_attribute_range"
|
|
227
227
|
v-model:attributeColorMap="edge_attribute_color_map"
|
|
228
228
|
:id="id"
|
|
229
|
-
:
|
|
229
|
+
:componentIds="componentIds"
|
|
230
230
|
:schema="edgeSchema"
|
|
231
231
|
/>
|
|
232
232
|
</template>
|
|
@@ -237,7 +237,7 @@ watch(
|
|
|
237
237
|
v-model:attributeRange="cell_attribute_range"
|
|
238
238
|
v-model:attributeColorMap="cell_attribute_color_map"
|
|
239
239
|
:id="id"
|
|
240
|
-
:
|
|
240
|
+
:componentIds="componentIds"
|
|
241
241
|
:schema="cellSchema"
|
|
242
242
|
/>
|
|
243
243
|
</template>
|
|
@@ -248,7 +248,7 @@ watch(
|
|
|
248
248
|
v-model:attributeRange="polygon_attribute_range"
|
|
249
249
|
v-model:attributeColorMap="polygon_attribute_color_map"
|
|
250
250
|
:id="id"
|
|
251
|
-
:
|
|
251
|
+
:componentIds="componentIds"
|
|
252
252
|
:schema="polygonSchema"
|
|
253
253
|
/>
|
|
254
254
|
</template>
|
|
@@ -259,7 +259,7 @@ watch(
|
|
|
259
259
|
v-model:attributeRange="polyhedron_attribute_range"
|
|
260
260
|
v-model:attributeColorMap="polyhedron_attribute_color_map"
|
|
261
261
|
:id="id"
|
|
262
|
-
:
|
|
262
|
+
:componentIds="componentIds"
|
|
263
263
|
:schema="polyhedronSchema"
|
|
264
264
|
/>
|
|
265
265
|
</template>
|
package/app/utils/extension.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import _ from "lodash";
|
|
5
5
|
|
|
6
6
|
// Local imports
|
|
7
|
+
import { isCloudMode } from "@ogw_front/utils/stores";
|
|
7
8
|
import { useAppStore } from "@ogw_front/stores/app";
|
|
8
9
|
import { useInfraStore } from "@ogw_front/stores/infra";
|
|
9
10
|
|
|
@@ -105,9 +106,9 @@ function runExtensions() {
|
|
|
105
106
|
const { projectFolderPath } = appStore;
|
|
106
107
|
const { PROJECT: projectName } = useRuntimeConfig().public;
|
|
107
108
|
const params = { projectFolderPath, projectName };
|
|
108
|
-
|
|
109
|
+
const endpoint = isCloudMode() ? "cloud" : "local";
|
|
109
110
|
const schema = {
|
|
110
|
-
$id:
|
|
111
|
+
$id: `/api/${endpoint}/extensions/run`,
|
|
111
112
|
methods: ["POST"],
|
|
112
113
|
type: "object",
|
|
113
114
|
properties: {
|
|
@@ -130,7 +131,7 @@ function killExtension(extensionId) {
|
|
|
130
131
|
console.log(`[AppStore] Killing extension: ${extensionId}`, { params });
|
|
131
132
|
|
|
132
133
|
const schema = {
|
|
133
|
-
$id: "/api/
|
|
134
|
+
$id: "/api/local/extensions/kill",
|
|
134
135
|
methods: ["POST"],
|
|
135
136
|
type: "object",
|
|
136
137
|
properties: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geode/opengeodeweb-front",
|
|
3
|
-
"version": "10.30.0-rc.
|
|
3
|
+
"version": "10.30.0-rc.16",
|
|
4
4
|
"description": "OpenSource Vue/Nuxt/Pinia/Vuetify framework for web applications",
|
|
5
5
|
"homepage": "https://github.com/Geode-solutions/OpenGeodeWeb-Front",
|
|
6
6
|
"bugs": {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Node imports
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
|
|
4
|
+
// Third party imports
|
|
5
|
+
import { createError, defineEventHandler, readBody } from "h3";
|
|
6
|
+
|
|
7
|
+
// Local imports
|
|
8
|
+
import {
|
|
9
|
+
addMicroserviceMetadatas,
|
|
10
|
+
runExtension,
|
|
11
|
+
} from "@geode/opengeodeweb-front/server/utils/microservices.js";
|
|
12
|
+
import {
|
|
13
|
+
extensionBackendPath,
|
|
14
|
+
extensionFolderPath,
|
|
15
|
+
} from "@geode/opengeodeweb-front/server/utils/path.js";
|
|
16
|
+
import {
|
|
17
|
+
readExtensionFrontend,
|
|
18
|
+
readExtensionMetadata,
|
|
19
|
+
} from "@geode/opengeodeweb-front/server/utils/extension.js";
|
|
20
|
+
import { extensionsConf } from "@geode/opengeodeweb-front/server/utils/app_config.js";
|
|
21
|
+
import { unzipFile } from "@geode/opengeodeweb-front/server/utils/server.js";
|
|
22
|
+
|
|
23
|
+
export default defineEventHandler(async (event) => {
|
|
24
|
+
try {
|
|
25
|
+
console.log("NITRO: runExtensions", event);
|
|
26
|
+
const { projectFolderPath, projectName } = await readBody(event);
|
|
27
|
+
const extensionsConfig = extensionsConf(projectName);
|
|
28
|
+
const extensionsArray = await Promise.all(
|
|
29
|
+
Object.keys(extensionsConfig).map(async (extensionId) => {
|
|
30
|
+
const extensionPath = extensionsConfig[extensionId].path;
|
|
31
|
+
const unzippedExtensionPath = await unzipFile(
|
|
32
|
+
extensionPath,
|
|
33
|
+
extensionFolderPath(projectFolderPath, extensionId),
|
|
34
|
+
);
|
|
35
|
+
const { id, name, version, backendExecutable, frontendFile } =
|
|
36
|
+
await readExtensionMetadata(unzippedExtensionPath);
|
|
37
|
+
const frontendContent = await readExtensionFrontend(
|
|
38
|
+
unzippedExtensionPath,
|
|
39
|
+
frontendFile,
|
|
40
|
+
id,
|
|
41
|
+
);
|
|
42
|
+
fs.chmodSync(extensionBackendPath(unzippedExtensionPath, backendExecutable), "755");
|
|
43
|
+
const port = await runExtension(id, backendExecutable, unzippedExtensionPath, {
|
|
44
|
+
projectFolderPath,
|
|
45
|
+
});
|
|
46
|
+
await addMicroserviceMetadatas(projectFolderPath, {
|
|
47
|
+
type: "back",
|
|
48
|
+
name,
|
|
49
|
+
port,
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
id,
|
|
53
|
+
name,
|
|
54
|
+
version,
|
|
55
|
+
frontendContent,
|
|
56
|
+
port,
|
|
57
|
+
};
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
statusCode: 200,
|
|
63
|
+
extensionsArray,
|
|
64
|
+
};
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error("Error running extensions:", error);
|
|
67
|
+
throw createError({
|
|
68
|
+
statusCode: 500,
|
|
69
|
+
statusMessage: error.message,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Node imports
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
3
|
|
|
5
4
|
// Third party imports
|
|
6
5
|
import { createError, defineEventHandler, readBody } from "h3";
|
|
@@ -11,20 +10,21 @@ import {
|
|
|
11
10
|
runBack,
|
|
12
11
|
} from "@geode/opengeodeweb-front/server/utils/microservices.js";
|
|
13
12
|
import {
|
|
14
|
-
|
|
13
|
+
extensionBackendPath,
|
|
15
14
|
extensionFolderPath,
|
|
16
|
-
extensionFrontendPath,
|
|
17
15
|
} from "@geode/opengeodeweb-front/server/utils/path.js";
|
|
16
|
+
import {
|
|
17
|
+
readExtensionFrontend,
|
|
18
|
+
readExtensionMetadata,
|
|
19
|
+
} from "@geode/opengeodeweb-front/server/utils/extension.js";
|
|
18
20
|
import { extensionsConf } from "@geode/opengeodeweb-front/server/utils/app_config.js";
|
|
19
21
|
import { unzipFile } from "@geode/opengeodeweb-front/server/utils/server.js";
|
|
20
22
|
|
|
21
23
|
export default defineEventHandler(async (event) => {
|
|
22
24
|
try {
|
|
23
25
|
console.log("NITRO: runExtensions", event);
|
|
24
|
-
const
|
|
25
|
-
const { projectFolderPath, projectName } = body;
|
|
26
|
+
const { projectFolderPath, projectName } = await readBody(event);
|
|
26
27
|
const extensionsConfig = extensionsConf(projectName);
|
|
27
|
-
|
|
28
28
|
const extensionsArray = await Promise.all(
|
|
29
29
|
Object.keys(extensionsConfig).map(async (extensionId) => {
|
|
30
30
|
const extensionPath = extensionsConfig[extensionId].path;
|
|
@@ -32,50 +32,14 @@ export default defineEventHandler(async (event) => {
|
|
|
32
32
|
extensionPath,
|
|
33
33
|
extensionFolderPath(projectFolderPath, extensionId),
|
|
34
34
|
);
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (!metadataContent) {
|
|
39
|
-
throw createError({
|
|
40
|
-
statusCode: 400,
|
|
41
|
-
statusMessage: "Invalid extension file: missing metadata.json",
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
const metadata = JSON.parse(metadataContent);
|
|
45
|
-
console.log("runExtensions", { metadata });
|
|
46
|
-
|
|
47
|
-
const { id, name, version, backendExecutable, frontendFile } = metadata;
|
|
48
|
-
console.log("runExtensions", { id, name, version, backendExecutable });
|
|
49
|
-
|
|
50
|
-
if (!frontendFile) {
|
|
51
|
-
throw createError({
|
|
52
|
-
statusCode: 400,
|
|
53
|
-
statusMessage: "Invalid extension file: missing frontend JavaScript",
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
if (!backendExecutable) {
|
|
57
|
-
throw createError({
|
|
58
|
-
statusCode: 400,
|
|
59
|
-
statusMessage: "Invalid extension file: missing backend executable",
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const frontendFilePath = await extensionFrontendPath(
|
|
35
|
+
const { id, name, version, backendExecutable, frontendFile } =
|
|
36
|
+
await readExtensionMetadata(unzippedExtensionPath);
|
|
37
|
+
const frontendContent = await readExtensionFrontend(
|
|
64
38
|
unzippedExtensionPath,
|
|
65
39
|
frontendFile,
|
|
66
|
-
path.resolve(),
|
|
67
40
|
id,
|
|
68
41
|
);
|
|
69
|
-
|
|
70
|
-
console.log("runExtensions", { frontendFilePath });
|
|
71
|
-
const frontendContent = await fs.promises.readFile(frontendFilePath, "utf8");
|
|
72
|
-
|
|
73
|
-
const backendExecutablePath = path.join(
|
|
74
|
-
unzippedExtensionPath,
|
|
75
|
-
executableName(backendExecutable),
|
|
76
|
-
);
|
|
77
|
-
console.log("runExtensions", { backendExecutablePath });
|
|
78
|
-
fs.chmodSync(backendExecutablePath, "755");
|
|
42
|
+
fs.chmodSync(extensionBackendPath(unzippedExtensionPath, backendExecutable), "755");
|
|
79
43
|
const port = await runBack(backendExecutable, unzippedExtensionPath, {
|
|
80
44
|
projectFolderPath,
|
|
81
45
|
});
|
package/server/utils/cloud.js
CHANGED
|
@@ -5,6 +5,8 @@ import { google } from "googleapis";
|
|
|
5
5
|
|
|
6
6
|
// Local imports
|
|
7
7
|
|
|
8
|
+
const LOCATIONS_DIR = "/etc/nginx/locations";
|
|
9
|
+
|
|
8
10
|
async function artifactImage(parent, authClient) {
|
|
9
11
|
const projectName = process.env.PROJECT;
|
|
10
12
|
const registry = google.artifactregistry({
|
|
@@ -92,4 +94,108 @@ function requestConfig(parent, image, email, projectName) {
|
|
|
92
94
|
};
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
|
|
97
|
+
function addSupervisorProgram(name, command, executableArgs) {
|
|
98
|
+
const conf = `
|
|
99
|
+
[program:${name}]
|
|
100
|
+
command=${command} ${executableArgs.join(" ")}
|
|
101
|
+
autostart=true
|
|
102
|
+
autorestart=true
|
|
103
|
+
stdout_logfile=/dev/stdout
|
|
104
|
+
stdout_logfile_maxbytes=0
|
|
105
|
+
stderr_logfile=/dev/stderr
|
|
106
|
+
stderr_logfile_maxbytes=0
|
|
107
|
+
`;
|
|
108
|
+
const confPath = path.join("/etc/supervisor/conf.d", `${name}.conf`);
|
|
109
|
+
fs.writeFileSync(confPath, conf);
|
|
110
|
+
execFileSync("supervisorctl", ["reread"]);
|
|
111
|
+
execFileSync("supervisorctl", ["update"]);
|
|
112
|
+
const stdout = execFileSync("supervisorctl", ["start", name]);
|
|
113
|
+
console.log("addSupervisorProgram", stdout);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function buildLocationBlock(routePath, port) {
|
|
117
|
+
if (!routePath.startsWith("/") || !routePath.endsWith("/")) {
|
|
118
|
+
throw new Error(`routePath must start and end with '/', got: ${routePath}`);
|
|
119
|
+
}
|
|
120
|
+
const methods = "GET, POST, PUT, DELETE, PATCH, OPTIONS";
|
|
121
|
+
const limitMethods = methods
|
|
122
|
+
.split(",")
|
|
123
|
+
.map((method) => method.trim())
|
|
124
|
+
.filter((method) => method !== "OPTIONS")
|
|
125
|
+
.join(" ");
|
|
126
|
+
|
|
127
|
+
return `# ====================== ${routePath} location ======================
|
|
128
|
+
location ~ "^${routePath}" {
|
|
129
|
+
if ($request_method = 'OPTIONS') {
|
|
130
|
+
add_header 'Access-Control-Allow-Origin' $allow_origin always;
|
|
131
|
+
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
|
132
|
+
add_header 'Access-Control-Allow-Methods' '${methods}' always;
|
|
133
|
+
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-CSRF-Token' always;
|
|
134
|
+
add_header 'Access-Control-Max-Age' 1728000 always;
|
|
135
|
+
add_header 'Content-Type' 'text/plain; charset=utf-8';
|
|
136
|
+
add_header 'Content-Length' 0;
|
|
137
|
+
return 204;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
limit_except ${limitMethods} { deny all; }
|
|
141
|
+
|
|
142
|
+
add_header 'Access-Control-Allow-Origin' $allow_origin always;
|
|
143
|
+
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
|
144
|
+
add_header 'Access-Control-Allow-Methods' '${methods}' always;
|
|
145
|
+
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-CSRF-Token' always;
|
|
146
|
+
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
|
|
147
|
+
add_header 'Vary' 'Origin' always;
|
|
148
|
+
|
|
149
|
+
rewrite "^${routePath}(.*)" /$1 break;
|
|
150
|
+
proxy_pass http://localhost:${port};
|
|
151
|
+
proxy_http_version 1.1;
|
|
152
|
+
proxy_set_header Host $host;
|
|
153
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
154
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
155
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
156
|
+
}
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function nginxConfigFile(name) {
|
|
161
|
+
return path.join(LOCATIONS_DIR, `${name}.conf`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function nginxReload() {
|
|
165
|
+
execFileSync("nginx", ["-t"]);
|
|
166
|
+
execFileSync("nginx", ["-s", "reload"]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function addNginxLocation(name, port) {
|
|
170
|
+
fs.mkdirSync(LOCATIONS_DIR, { recursive: true });
|
|
171
|
+
const filePath = nginxConfigFile(name);
|
|
172
|
+
if (fs.existsSync(filePath)) {
|
|
173
|
+
throw new Error(`Location '${name}' already exists at ${filePath}`);
|
|
174
|
+
}
|
|
175
|
+
fs.writeFileSync(filePath, buildLocationBlock(`/${name}/`, port));
|
|
176
|
+
try {
|
|
177
|
+
nginxReload();
|
|
178
|
+
} catch (error) {
|
|
179
|
+
fs.unlinkSync(filePath);
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
return filePath;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function removeNginxLocation(name) {
|
|
186
|
+
const filePath = filePathFor(name);
|
|
187
|
+
if (!fs.existsSync(filePath)) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
fs.unlinkSync(filePath);
|
|
191
|
+
nginxReload();
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export {
|
|
196
|
+
addNginxLocation,
|
|
197
|
+
addSupervisorProgram,
|
|
198
|
+
artifactImage,
|
|
199
|
+
removeNginxLocation,
|
|
200
|
+
requestConfig,
|
|
201
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Node imports
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
// Third party imports
|
|
6
|
+
import { createError } from "h3";
|
|
7
|
+
|
|
8
|
+
// Local imports
|
|
9
|
+
import { extensionFrontendPath } from "@geode/opengeodeweb-front/server/utils/path.js";
|
|
10
|
+
|
|
11
|
+
async function readExtensionMetadata(unzippedExtensionPath) {
|
|
12
|
+
const metadataPath = path.join(unzippedExtensionPath, "metadata.json");
|
|
13
|
+
const metadataContent = await fs.readFile(metadataPath, "utf8");
|
|
14
|
+
if (!metadataContent) {
|
|
15
|
+
throw createError({
|
|
16
|
+
statusCode: 400,
|
|
17
|
+
statusMessage: "Invalid extension file: missing metadata.json",
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const metadata = JSON.parse(metadataContent);
|
|
21
|
+
console.log("readExtensionMetadata", { metadata });
|
|
22
|
+
if (!metadata.frontendFile) {
|
|
23
|
+
throw createError({
|
|
24
|
+
statusCode: 400,
|
|
25
|
+
statusMessage: "Invalid extension file: missing frontend JavaScript",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (!metadata.backendExecutable) {
|
|
29
|
+
throw createError({
|
|
30
|
+
statusCode: 400,
|
|
31
|
+
statusMessage: "Invalid extension file: missing backend executable",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return metadata;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function readExtensionFrontend(unzippedExtensionPath, frontendFile, id) {
|
|
39
|
+
console.log("readExtensionFrontend", { id });
|
|
40
|
+
const frontendFilePath = await extensionFrontendPath(
|
|
41
|
+
unzippedExtensionPath,
|
|
42
|
+
frontendFile,
|
|
43
|
+
path.resolve(),
|
|
44
|
+
id,
|
|
45
|
+
);
|
|
46
|
+
console.log("readExtensionFrontend", { frontendFilePath });
|
|
47
|
+
return fs.readFile(frontendFilePath, "utf8");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { readExtensionFrontend, readExtensionMetadata };
|
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json" with { type: "json" };
|
|
8
8
|
|
|
9
9
|
// Local imports
|
|
10
|
+
import { addNginxLocation, addSupervisorProgram } from "./cloud.js";
|
|
10
11
|
import { getAvailablePort, waitForReady } from "./scripts.js";
|
|
11
12
|
import { microservicesMetadatasPath, projectMicroservices } from "./cleanup.js";
|
|
12
13
|
import { executablePath } from "./path.js";
|
|
@@ -57,33 +58,11 @@ function isPortInUseError(errorMessage) {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
async function runBack(execName, execPath, args = {}, attempts = 0) {
|
|
60
|
-
const { projectFolderPath } = args;
|
|
61
|
-
if (!projectFolderPath) {
|
|
62
|
-
throw new Error("projectFolderPath is required");
|
|
63
|
-
}
|
|
64
|
-
let { uploadFolderPath } = args;
|
|
65
|
-
if (!uploadFolderPath) {
|
|
66
|
-
uploadFolderPath = path.join(projectFolderPath, "uploads");
|
|
67
|
-
}
|
|
68
61
|
try {
|
|
69
62
|
const port = await getAvailablePort();
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"--project_folder_path",
|
|
74
|
-
projectFolderPath,
|
|
75
|
-
"--upload_folder_path",
|
|
76
|
-
uploadFolderPath,
|
|
77
|
-
"--allowed_origins",
|
|
78
|
-
"http://localhost:*",
|
|
79
|
-
"--timeout",
|
|
80
|
-
"0",
|
|
81
|
-
];
|
|
82
|
-
if (process.env.NODE_ENV === "development" || !process.env.NODE_ENV) {
|
|
83
|
-
backArgs.push("--debug");
|
|
84
|
-
}
|
|
85
|
-
console.log("runBack", execPath, execName, backArgs);
|
|
86
|
-
await runScript(execPath, execName, backArgs, "Serving Flask app");
|
|
63
|
+
const executableArgs = backArgs(args, port);
|
|
64
|
+
console.log("runBack", execPath, execName, executableArgs);
|
|
65
|
+
await runScript(execPath, execName, executableArgs, "Serving Flask app");
|
|
87
66
|
return port;
|
|
88
67
|
} catch (error) {
|
|
89
68
|
if (!isPortInUseError(error)) {
|
|
@@ -129,6 +108,52 @@ async function runViewer(execName, execPath, args = {}, attempts = 0) {
|
|
|
129
108
|
}
|
|
130
109
|
}
|
|
131
110
|
|
|
111
|
+
function backArgs(args, port) {
|
|
112
|
+
const { projectFolderPath } = args;
|
|
113
|
+
if (!projectFolderPath) {
|
|
114
|
+
throw new Error("projectFolderPath is required");
|
|
115
|
+
}
|
|
116
|
+
const uploadFolderPath = args.uploadFolderPath || path.join(projectFolderPath, "uploads");
|
|
117
|
+
const executableArgs = [
|
|
118
|
+
"--port",
|
|
119
|
+
String(port),
|
|
120
|
+
"--project_folder_path",
|
|
121
|
+
projectFolderPath,
|
|
122
|
+
"--upload_folder_path",
|
|
123
|
+
uploadFolderPath,
|
|
124
|
+
"--allowed_origins",
|
|
125
|
+
"http://localhost:*",
|
|
126
|
+
"--timeout",
|
|
127
|
+
"0",
|
|
128
|
+
];
|
|
129
|
+
if (process.env.NODE_ENV === "development" || !process.env.NODE_ENV) {
|
|
130
|
+
executableArgs.push("--debug");
|
|
131
|
+
}
|
|
132
|
+
return executableArgs;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function runExtension(extensionId, execName, execPath, args = {}, attempts = 0) {
|
|
136
|
+
try {
|
|
137
|
+
const port = await getAvailablePort();
|
|
138
|
+
const executableArgs = backArgs(args, port);
|
|
139
|
+
const command = executablePath(execPath, execName);
|
|
140
|
+
console.log("runExtension", execPath, execName, executableArgs);
|
|
141
|
+
addSupervisorProgram(extensionId, command, executableArgs);
|
|
142
|
+
addNginxLocation(extensionId, port);
|
|
143
|
+
return port;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (!isPortInUseError(error)) {
|
|
146
|
+
console.log("runBack error", error);
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
if (attempts <= MAX_PORT_RETRIES) {
|
|
150
|
+
console.log("Retrying runExtension on conflicting port", port);
|
|
151
|
+
const port = await runExtension(extensionId, execName, execPath, args, attempts + 1);
|
|
152
|
+
return port;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
132
157
|
function addMicroserviceMetadatas(projectFolderPath, serviceObj) {
|
|
133
158
|
const microservices = projectMicroservices(projectFolderPath);
|
|
134
159
|
if (serviceObj.type === "back") {
|
|
@@ -147,4 +172,4 @@ function addMicroserviceMetadatas(projectFolderPath, serviceObj) {
|
|
|
147
172
|
);
|
|
148
173
|
}
|
|
149
174
|
|
|
150
|
-
export { addMicroserviceMetadatas, runBack, runViewer };
|
|
175
|
+
export { addMicroserviceMetadatas, runBack, runExtension, runViewer };
|
package/server/utils/path.js
CHANGED
|
@@ -138,8 +138,18 @@ async function extensionFrontendPath(unzippedExtensionPath, frontendFile, rootPa
|
|
|
138
138
|
throw new Error(`Failed to find ${unzippedfrontendFilePath}`);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
function extensionBackendPath(unzippedExtensionPath, backendExecutableName) {
|
|
142
|
+
const backendExecutablePath = path.join(
|
|
143
|
+
unzippedExtensionPath,
|
|
144
|
+
executableName(backendExecutableName),
|
|
145
|
+
);
|
|
146
|
+
console.log("runExtensions", { backendExecutablePath });
|
|
147
|
+
return backendExecutablePath;
|
|
148
|
+
}
|
|
149
|
+
|
|
141
150
|
export {
|
|
142
151
|
createPath,
|
|
152
|
+
extensionBackendPath,
|
|
143
153
|
extensionFrontendPath,
|
|
144
154
|
extensionFolderPath,
|
|
145
155
|
executablePath,
|
|
File without changes
|