@cognite/dune 0.3.7 ā 0.4.1
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/_templates/app/new/root/app.json.ejs.t +3 -0
- package/_templates/app/new/root/package.json.ejs.t +3 -0
- package/_templates/app/new/src/App.tsx.ejs.t +70 -1
- package/_templates/app/new/src/main.tsx.ejs.t +6 -0
- package/bin/cli.js +21 -17
- package/bin/deploy-command.js +3 -14
- package/bin/deploy-interactive-command.js +41 -42
- package/bin/utils/fusion-url.js +28 -0
- package/bin/utils/fusion-url.test.js +50 -0
- package/dist/auth/index.d.ts +14 -1
- package/dist/auth/index.js +3 -1
- package/dist/{chunk-53VTKDSC.js ā chunk-XWZCFZUH.js} +50 -0
- package/dist/cli/cli.js +63 -0
- package/dist/deploy/index.d.ts +41 -1
- package/dist/deploy/index.js +154 -22
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/dist/vite/index.js +10 -1
- package/package.json +23 -3
- package/src/auth/app-sdk-auth-provider.test.tsx +126 -0
- package/src/auth/app-sdk-auth-provider.tsx +69 -0
- package/src/auth/index.ts +1 -0
- package/src/deploy/apphosting-deployer.test.ts +177 -0
- package/src/deploy/apphosting-deployer.ts +166 -0
- package/src/deploy/deploy.ts +25 -20
- package/src/deploy/index.ts +1 -0
- package/src/deploy/types.ts +2 -0
- package/src/vite/fusion-open-plugin.ts +10 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { type CogniteClient, HttpError } from '@cognite/sdk';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { AppHostingDeployer } from './apphosting-deployer';
|
|
5
|
+
|
|
6
|
+
vi.mock('node:fs', () => ({
|
|
7
|
+
default: {
|
|
8
|
+
readFileSync: vi.fn(() => Buffer.from('fake-zip-content')),
|
|
9
|
+
},
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('node:path', () => ({
|
|
13
|
+
default: {
|
|
14
|
+
basename: vi.fn(() => 'app.zip'),
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const BASE_URL = 'https://cognite.test';
|
|
19
|
+
const PROJECT = 'test-project';
|
|
20
|
+
const TOKEN = 'test-token';
|
|
21
|
+
|
|
22
|
+
function makeDeployer() {
|
|
23
|
+
const mockClient = {
|
|
24
|
+
project: PROJECT,
|
|
25
|
+
authenticate: vi.fn(() => Promise.resolve(TOKEN)),
|
|
26
|
+
getBaseUrl: vi.fn(() => BASE_URL),
|
|
27
|
+
post: vi.fn(),
|
|
28
|
+
};
|
|
29
|
+
const deployer = new AppHostingDeployer(
|
|
30
|
+
mockClient as Partial<CogniteClient> as CogniteClient,
|
|
31
|
+
);
|
|
32
|
+
return { deployer, mockClient };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mockFetchResponse(ok: boolean, status: number, body = '') {
|
|
36
|
+
return vi.fn(() =>
|
|
37
|
+
Promise.resolve({
|
|
38
|
+
ok,
|
|
39
|
+
status,
|
|
40
|
+
text: vi.fn(() => Promise.resolve(body)),
|
|
41
|
+
} as Partial<Response> as Response)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeHttpError(status: number, message = 'Error') {
|
|
46
|
+
return new HttpError(status, { error: { code: status, message } }, {});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe(AppHostingDeployer.name, () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
vi.clearAllMocks();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
vi.unstubAllGlobals();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('ensureApp', () => {
|
|
59
|
+
it('resolves when app already exists (409)', async () => {
|
|
60
|
+
const { deployer, mockClient } = makeDeployer();
|
|
61
|
+
mockClient.post.mockRejectedValueOnce(makeHttpError(409, 'Conflict'));
|
|
62
|
+
|
|
63
|
+
await expect(
|
|
64
|
+
deployer.ensureApp('my-app', 'My App', 'A description')
|
|
65
|
+
).resolves.toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('throws on unexpected API error', async () => {
|
|
69
|
+
const { deployer, mockClient } = makeDeployer();
|
|
70
|
+
mockClient.post.mockRejectedValueOnce(makeHttpError(500, 'Internal Server Error'));
|
|
71
|
+
|
|
72
|
+
await expect(
|
|
73
|
+
deployer.ensureApp('my-app', 'My App', 'A description')
|
|
74
|
+
).rejects.toThrow('Request failed | status code: 500');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('sends POST to the apps endpoint with correct path and data', async () => {
|
|
78
|
+
const { deployer, mockClient } = makeDeployer();
|
|
79
|
+
mockClient.post.mockResolvedValueOnce({ data: {}, status: 201, headers: {} });
|
|
80
|
+
|
|
81
|
+
await deployer.ensureApp('my-app', 'My App', 'A description');
|
|
82
|
+
|
|
83
|
+
expect(mockClient.post).toHaveBeenCalledWith(
|
|
84
|
+
`/api/v1/projects/${PROJECT}/apphosting/apps`,
|
|
85
|
+
expect.objectContaining({
|
|
86
|
+
data: { items: [{ externalId: 'my-app', name: 'My App', description: 'A description' }] },
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('uploadVersion', () => {
|
|
93
|
+
it('throws on upload failure', async () => {
|
|
94
|
+
const { deployer } = makeDeployer();
|
|
95
|
+
vi.stubGlobal('fetch', mockFetchResponse(false, 413, 'Payload Too Large'));
|
|
96
|
+
|
|
97
|
+
await expect(
|
|
98
|
+
deployer.uploadVersion('my-app', '1.0.0', '/tmp/app.zip')
|
|
99
|
+
).rejects.toThrow('Upload failed: 413');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('sends POST to the versions endpoint with auth header and form data', async () => {
|
|
103
|
+
const { deployer, mockClient } = makeDeployer();
|
|
104
|
+
const fetchMock = mockFetchResponse(true, 200);
|
|
105
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
106
|
+
|
|
107
|
+
await deployer.uploadVersion('my-app', '1.0.0', '/tmp/app.zip');
|
|
108
|
+
|
|
109
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
110
|
+
`${BASE_URL}/api/v1/projects/${PROJECT}/apphosting/apps/my-app/versions`,
|
|
111
|
+
expect.objectContaining({
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: expect.objectContaining({ Authorization: `Bearer ${TOKEN}` }),
|
|
114
|
+
body: expect.any(FormData),
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('publishAndActivate', () => {
|
|
121
|
+
it('throws on publish failure', async () => {
|
|
122
|
+
const { deployer, mockClient } = makeDeployer();
|
|
123
|
+
mockClient.post.mockRejectedValueOnce(makeHttpError(400, 'Bad Request'));
|
|
124
|
+
|
|
125
|
+
await expect(
|
|
126
|
+
deployer.publishAndActivate('my-app', '1.0.0')
|
|
127
|
+
).rejects.toThrow('Request failed | status code: 400');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('sends POST to the versions/update endpoint', async () => {
|
|
131
|
+
const { deployer, mockClient } = makeDeployer();
|
|
132
|
+
mockClient.post.mockResolvedValueOnce({ data: {}, status: 200, headers: {} });
|
|
133
|
+
|
|
134
|
+
await deployer.publishAndActivate('my-app', '1.0.0');
|
|
135
|
+
|
|
136
|
+
expect(mockClient.post).toHaveBeenCalledWith(
|
|
137
|
+
`/api/v1/projects/${PROJECT}/apphosting/apps/my-app/versions/update`,
|
|
138
|
+
expect.any(Object)
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('deploy', () => {
|
|
144
|
+
it('calls ensureApp and uploadVersion but not publishAndActivate when published is false', async () => {
|
|
145
|
+
const { deployer, mockClient } = makeDeployer();
|
|
146
|
+
mockClient.post.mockResolvedValue({ data: {}, status: 200, headers: {} });
|
|
147
|
+
vi.stubGlobal('fetch', mockFetchResponse(true, 200));
|
|
148
|
+
|
|
149
|
+
await deployer.deploy('my-app', 'My App', 'A description', '1.0.0', '/tmp/app.zip', false);
|
|
150
|
+
|
|
151
|
+
// ensureApp ā 1 client.post, uploadVersion ā 1 fetch, publishAndActivate ā not called
|
|
152
|
+
expect(mockClient.post).toHaveBeenCalledTimes(1);
|
|
153
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('calls publishAndActivate as third step when published is true', async () => {
|
|
157
|
+
const { deployer, mockClient } = makeDeployer();
|
|
158
|
+
mockClient.post.mockResolvedValue({ data: {}, status: 200, headers: {} });
|
|
159
|
+
vi.stubGlobal('fetch', mockFetchResponse(true, 200));
|
|
160
|
+
|
|
161
|
+
await deployer.deploy('my-app', 'My App', 'A description', '1.0.0', '/tmp/app.zip', true);
|
|
162
|
+
|
|
163
|
+
// ensureApp + publishAndActivate ā 2 client.post, uploadVersion ā 1 fetch
|
|
164
|
+
expect(mockClient.post).toHaveBeenCalledTimes(2);
|
|
165
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('wraps errors with "Deployment failed:" prefix', async () => {
|
|
169
|
+
const { deployer, mockClient } = makeDeployer();
|
|
170
|
+
mockClient.post.mockRejectedValueOnce(makeHttpError(500, 'Server Error'));
|
|
171
|
+
|
|
172
|
+
await expect(
|
|
173
|
+
deployer.deploy('my-app', 'My App', 'A description', '1.0.0', '/tmp/app.zip')
|
|
174
|
+
).rejects.toThrow('Deployment failed:');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Hosting API Deployment
|
|
3
|
+
*
|
|
4
|
+
* Handles deployment of packaged applications via the new App Hosting API
|
|
5
|
+
* instead of CDF Files API + dataset.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { type CogniteClient } from '@cognite/sdk';
|
|
12
|
+
|
|
13
|
+
export class AppHostingDeployer {
|
|
14
|
+
private client: CogniteClient;
|
|
15
|
+
|
|
16
|
+
constructor(client: CogniteClient) {
|
|
17
|
+
this.client = client;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private get appsBasePath(): string {
|
|
21
|
+
return `/api/v1/projects/${encodeURIComponent(this.client.project)}/apphosting/apps`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Ensure the app exists in the App Hosting service. Creates it if missing.
|
|
26
|
+
*/
|
|
27
|
+
async ensureApp(externalId: string, name: string, description: string): Promise<void> {
|
|
28
|
+
console.log('š Ensuring app exists...');
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
await this.client.post(this.appsBasePath, {
|
|
32
|
+
data: { items: [{ externalId, name, description }] },
|
|
33
|
+
});
|
|
34
|
+
console.log(`ā
App '${externalId}' created`);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error instanceof Error && 'status' in error && error.status === 409) {
|
|
37
|
+
console.log(`ā
App '${externalId}' already exists`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Upload a new version of the app as a zip file.
|
|
46
|
+
*
|
|
47
|
+
* `entryPath` must be a relative path (no leading slash). The App
|
|
48
|
+
* Hosting backend rejects absolute paths as a security measure against
|
|
49
|
+
* archive traversal, see `ZipCentralDirectory.normalizeEntryPath` in
|
|
50
|
+
* infrastructure/services/app-hosting.
|
|
51
|
+
*/
|
|
52
|
+
async uploadVersion(
|
|
53
|
+
appExternalId: string,
|
|
54
|
+
version: string,
|
|
55
|
+
zipPath: string,
|
|
56
|
+
entryPath = 'index.html',
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
console.log(`š¤ Uploading version ${version}...`);
|
|
59
|
+
|
|
60
|
+
const fileBuffer = fs.readFileSync(zipPath);
|
|
61
|
+
const fileName = path.basename(zipPath);
|
|
62
|
+
|
|
63
|
+
const formData = new FormData();
|
|
64
|
+
formData.append('file', new Blob([fileBuffer]), fileName);
|
|
65
|
+
formData.append('version', version);
|
|
66
|
+
formData.append('entryPath', entryPath);
|
|
67
|
+
|
|
68
|
+
const encodedAppExternalId = encodeURIComponent(appExternalId);
|
|
69
|
+
const uploadPath = `${this.appsBasePath}/${encodedAppExternalId}/versions`;
|
|
70
|
+
|
|
71
|
+
// client.post always sends application/json; use fetch directly so FormData
|
|
72
|
+
// sets the correct multipart/form-data Content-Type with boundary
|
|
73
|
+
const token = await this.client.authenticate();
|
|
74
|
+
const url = `${this.client.getBaseUrl()}${uploadPath}`;
|
|
75
|
+
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const timeout = setTimeout(() => controller.abort(), 5 * 60 * 1000);
|
|
78
|
+
|
|
79
|
+
let response: Response;
|
|
80
|
+
try {
|
|
81
|
+
response = await fetch(url, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
84
|
+
body: formData,
|
|
85
|
+
signal: controller.signal,
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
89
|
+
throw new Error('Upload timed out after 5 minutes');
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
} finally {
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const body = await response.text();
|
|
98
|
+
let message = body;
|
|
99
|
+
try {
|
|
100
|
+
const json = JSON.parse(body) as { message?: string; error?: string };
|
|
101
|
+
message = json.message ?? json.error ?? body;
|
|
102
|
+
} catch {
|
|
103
|
+
// not JSON, use raw text
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`Upload failed: ${response.status} ā ${message}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(`ā
Version ${version} uploaded`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Publish the version and set it as the ACTIVE alias.
|
|
113
|
+
*/
|
|
114
|
+
async publishAndActivate(appExternalId: string, version: string): Promise<void> {
|
|
115
|
+
console.log(`š Publishing and activating version ${version}...`);
|
|
116
|
+
|
|
117
|
+
const encodedAppExternalId = encodeURIComponent(appExternalId);
|
|
118
|
+
const url = `${this.appsBasePath}/${encodedAppExternalId}/versions/update`;
|
|
119
|
+
|
|
120
|
+
await this.client.post(url, {
|
|
121
|
+
data: {
|
|
122
|
+
items: [
|
|
123
|
+
{
|
|
124
|
+
version,
|
|
125
|
+
update: {
|
|
126
|
+
lifecycleState: { set: 'PUBLISHED' },
|
|
127
|
+
alias: { set: 'ACTIVE' },
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
console.log(`ā
Version ${version} is now PUBLISHED and ACTIVE`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Full deployment: ensure app exists, upload version, and optionally publish and activate.
|
|
139
|
+
*
|
|
140
|
+
* When `published` is false the version is uploaded in DRAFT state with no alias ā
|
|
141
|
+
* it is stored but not served to end users. Pass `published: true` to transition the
|
|
142
|
+
* version to PUBLISHED and set it as the ACTIVE alias so it starts receiving traffic.
|
|
143
|
+
*/
|
|
144
|
+
async deploy(
|
|
145
|
+
appExternalId: string,
|
|
146
|
+
name: string,
|
|
147
|
+
description: string,
|
|
148
|
+
versionTag: string,
|
|
149
|
+
zipPath: string,
|
|
150
|
+
published = false,
|
|
151
|
+
): Promise<void> {
|
|
152
|
+
console.log('\nš Deploying application via App Hosting API...\n');
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
await this.ensureApp(appExternalId, name, description);
|
|
156
|
+
await this.uploadVersion(appExternalId, versionTag, zipPath);
|
|
157
|
+
if (published) {
|
|
158
|
+
await this.publishAndActivate(appExternalId, versionTag);
|
|
159
|
+
}
|
|
160
|
+
console.log('\nā
Deployment successful!');
|
|
161
|
+
} catch (error: unknown) {
|
|
162
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
163
|
+
throw Object.assign(new Error(`Deployment failed: ${message}`), { cause: error });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
package/src/deploy/deploy.ts
CHANGED
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
|
|
3
|
+
import { AppHostingDeployer } from './apphosting-deployer';
|
|
3
4
|
import { CdfApplicationDeployer } from './application-deployer';
|
|
4
5
|
import { ApplicationPackager } from './application-packager';
|
|
5
6
|
import { getSdk } from './get-sdk';
|
|
6
7
|
import type { App, Deployment } from './types';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
async function deployViaAppHosting(deployment: Deployment, app: App, folder: string, zipFilename: string) {
|
|
10
|
+
const { externalId, name, description, versionTag } = app;
|
|
10
11
|
const sdk = await getSdk(deployment, folder);
|
|
12
|
+
const deployer = new AppHostingDeployer(sdk);
|
|
13
|
+
await deployer.deploy(externalId, name, description, versionTag, zipFilename, deployment.published);
|
|
14
|
+
}
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const zipFilename = await packager.createZip('app.zip', true);
|
|
16
|
-
|
|
17
|
-
// Step 3: Deploy to CDF
|
|
16
|
+
async function deployViaCdf(deployment: Deployment, app: App, folder: string, zipFilename: string) {
|
|
17
|
+
const { externalId, name, description, versionTag } = app;
|
|
18
|
+
const sdk = await getSdk(deployment, folder);
|
|
18
19
|
const deployer = new CdfApplicationDeployer(sdk);
|
|
19
|
-
await deployer.deploy(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
zipFilename,
|
|
25
|
-
deployment.published
|
|
26
|
-
);
|
|
20
|
+
await deployer.deploy(externalId, name, description, versionTag, zipFilename, deployment.published);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const deploy = async (deployment: Deployment, app: App, folder: string) => {
|
|
24
|
+
const zipFilename = await new ApplicationPackager(`${folder}/dist`).createZip('app.zip', true);
|
|
27
25
|
|
|
28
|
-
// Step 4: Clean up zip file
|
|
29
26
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
if (app.infra === 'appsApi') {
|
|
28
|
+
await deployViaAppHosting(deployment, app, folder, zipFilename);
|
|
29
|
+
} else {
|
|
30
|
+
await deployViaCdf(deployment, app, folder, zipFilename);
|
|
31
|
+
}
|
|
32
|
+
} finally {
|
|
33
|
+
try {
|
|
34
|
+
fs.unlinkSync(zipFilename);
|
|
35
|
+
} catch {
|
|
36
|
+
// Ignore cleanup errors
|
|
37
|
+
}
|
|
33
38
|
}
|
|
34
39
|
};
|
package/src/deploy/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Deploy exports for CI/CD and programmatic deployment
|
|
2
2
|
export { deploy } from './deploy';
|
|
3
|
+
export { AppHostingDeployer } from './apphosting-deployer';
|
|
3
4
|
export { CdfApplicationDeployer } from './application-deployer';
|
|
4
5
|
export { ApplicationPackager } from './application-packager';
|
|
5
6
|
export { getSdk } from './get-sdk';
|
package/src/deploy/types.ts
CHANGED
|
@@ -37,7 +37,16 @@ export const fusionOpenPlugin = () => {
|
|
|
37
37
|
const parsedBaseUrl = baseUrl?.split('//')[1];
|
|
38
38
|
|
|
39
39
|
if (org && project && baseUrl) {
|
|
40
|
-
|
|
40
|
+
let fusionUrl: string;
|
|
41
|
+
if (appJson.infra === 'appsApi') {
|
|
42
|
+
if (!appJson.externalId) {
|
|
43
|
+
console.warn(' ā app.json is missing externalId ā cannot determine Fusion URL');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
fusionUrl = `https://${org}.fusion.cognite.com/${project}/dune-app-host/development/${appJson.externalId}/${port}?cluster=${parsedBaseUrl}`;
|
|
47
|
+
} else {
|
|
48
|
+
fusionUrl = `https://${org}.fusion.cognite.com/${project}/streamlit-apps/dune/development/${port}?cluster=${parsedBaseUrl}&workspace=industrial-tools`;
|
|
49
|
+
}
|
|
41
50
|
console.log(` ā Fusion: ${fusionUrl}`);
|
|
42
51
|
openUrl(fusionUrl);
|
|
43
52
|
return;
|