3 Commits

Author SHA1 Message Date
Eric Tuvesson
41711c3934 Merge branch 'main' into feature/store-cloudservices-in-project-folder 2024-06-17 10:35:36 +02:00
Eric Tuvesson
5d8b2d5bba Merge branch 'main' into feature/store-cloudservices-in-project-folder 2024-06-03 10:29:29 +02:00
Eric Tuvesson
851dbc98d3 feat: Save Cloud Service in the project folder instead 2024-05-22 10:01:46 +02:00
13 changed files with 205 additions and 74 deletions

View File

@@ -48,7 +48,7 @@ export async function execute(command: Command, event: MessageEvent) {
// Deploy to temp folder // Deploy to temp folder
await compilation.deployToFolder(tempDir, { await compilation.deployToFolder(tempDir, {
environment: command.cloudService ? new Environment(command.cloudService) : undefined environment: command.cloudService ? new Environment("", command.cloudService) : undefined
}); });
// Upload to S3 // Upload to S3

View File

@@ -1,9 +1,9 @@
import { Environment, ExternalCloudService } from '@noodl-models/CloudServices/ExternalCloudService';
import { import {
CloudServiceEvent, CloudServiceEvent,
CloudServiceEvents, CloudServiceEvents,
CreateEnvironment, CreateEnvironment,
CreateEnvironmentRequest, CreateEnvironmentRequest,
Environment,
ICloudBackendService, ICloudBackendService,
ICloudService, ICloudService,
UpdateEnvironmentRequest UpdateEnvironmentRequest
@@ -12,6 +12,9 @@ import { ProjectModel } from '@noodl-models/projectmodel';
import { getCloudServices } from '@noodl-models/projectmodel.editor'; import { getCloudServices } from '@noodl-models/projectmodel.editor';
import { Model } from '@noodl-utils/model'; import { Model } from '@noodl-utils/model';
import { GlobalCloudService } from './providers/GlobalCloudService';
import { ProjectCloudService } from './providers/ProjectCloudService';
export type CloudQueueItem = { export type CloudQueueItem = {
frontendId: string; frontendId: string;
environmentId: string; environmentId: string;
@@ -20,7 +23,9 @@ export type CloudQueueItem = {
class CloudBackendService implements ICloudBackendService { class CloudBackendService implements ICloudBackendService {
private _isLoading = false; private _isLoading = false;
private _collection?: Environment[]; private _collection?: Environment[];
private _localExternal = new ExternalCloudService();
private _globalProvider = new GlobalCloudService();
private _projectProvider = new ProjectCloudService();
get isLoading(): boolean { get isLoading(): boolean {
return this._isLoading; return this._isLoading;
@@ -32,12 +37,14 @@ class CloudBackendService implements ICloudBackendService {
constructor(private readonly service: CloudService) {} constructor(private readonly service: CloudService) {}
async fetch(): Promise<Environment[]> { async fetch(project: ProjectModel): Promise<Environment[]> {
this._isLoading = true; this._isLoading = true;
try { try {
// Fetch environments from local machine const projectResults = await this._projectProvider.list(project);
const localEnvironments = await this._localExternal.list(); this._collection = projectResults.map((x) => new Environment("project", x));
this._collection = localEnvironments.map((x) => new Environment(x));
const globalResults = await this._globalProvider.list(project);
this._collection = this._collection.concat(globalResults.map((x) => new Environment("global", x)));
} finally { } finally {
this._isLoading = false; this._isLoading = false;
this.service.notifyListeners(CloudServiceEvent.BackendUpdated); this.service.notifyListeners(CloudServiceEvent.BackendUpdated);
@@ -48,7 +55,7 @@ class CloudBackendService implements ICloudBackendService {
async fromProject(project: ProjectModel): Promise<Environment> { async fromProject(project: ProjectModel): Promise<Environment> {
const activeCloudServices = getCloudServices(project); const activeCloudServices = getCloudServices(project);
if (!this._collection) { if (!this._collection) {
await this.fetch(); await this.fetch(project);
} }
return this.items.find((b) => { return this.items.find((b) => {
@@ -56,16 +63,16 @@ class CloudBackendService implements ICloudBackendService {
}); });
} }
async create(options: CreateEnvironmentRequest): Promise<CreateEnvironment> { async create(project: ProjectModel, options: CreateEnvironmentRequest): Promise<CreateEnvironment> {
return await this._localExternal.create(options); return await this._projectProvider.create(project, options);
} }
async update(options: UpdateEnvironmentRequest): Promise<boolean> { async update(project: ProjectModel, options: UpdateEnvironmentRequest): Promise<boolean> {
return await this._localExternal.update(options); return await this._projectProvider.update(project, options);
} }
async delete(id: string): Promise<boolean> { async delete(project: ProjectModel, id: string): Promise<boolean> {
return await this._localExternal.delete(id); return await this._projectProvider.delete(project, id);
} }
} }
@@ -95,15 +102,11 @@ export class CloudService extends Model<CloudServiceEvent, CloudServiceEvents> i
*/ */
public async prefetch() { public async prefetch() {
this.reset(); this.reset();
await this.fetch(); await this.backend.fetch(ProjectModel.instance);
}
public async fetch() {
await this.backend.fetch();
} }
public async getActiveEnvironment(project: ProjectModel): Promise<Environment> { public async getActiveEnvironment(project: ProjectModel): Promise<Environment> {
await this.backend.fetch(); await this.backend.fetch(project);
return this.backend.fromProject(project); return this.backend.fromProject(project);
} }
} }

View File

@@ -1,3 +1,2 @@
export * from './CloudService'; export * from './CloudService';
export * from './type'; export * from './type';
export * from './ExternalCloudService';

View File

@@ -1,25 +1,21 @@
import { JSONStorage } from '@noodl/platform'; import { JSONStorage } from '@noodl/platform';
import { CreateEnvironment, CreateEnvironmentRequest, UpdateEnvironmentRequest } from '@noodl-models/CloudServices'; import {
CreateEnvironment,
CreateEnvironmentRequest,
EnvironmentDataFormat,
ICloudServiceProvider,
UpdateEnvironmentRequest
} from '@noodl-models/CloudServices';
import { ProjectModel } from '@noodl-models/projectmodel';
/** The data format is separated from our internal model. */ export class GlobalCloudService implements ICloudServiceProvider {
export type EnvironmentDataFormat = { async list(_project: ProjectModel): Promise<EnvironmentDataFormat[]> {
enabled: boolean;
id: string;
name: string;
description: string;
masterKey: string;
appId: string;
endpoint: string;
};
export class ExternalCloudService {
async list(): Promise<EnvironmentDataFormat[]> {
const local = await JSONStorage.get('externalBrokers'); const local = await JSONStorage.get('externalBrokers');
return local.brokers || []; return local.brokers || [];
} }
async create(options: CreateEnvironmentRequest): Promise<CreateEnvironment> { async create(_project: ProjectModel, options: CreateEnvironmentRequest): Promise<CreateEnvironment> {
const id = `${options.url}-${options.appId}`; const id = `${options.url}-${options.appId}`;
const newBroker: EnvironmentDataFormat = { const newBroker: EnvironmentDataFormat = {
@@ -44,7 +40,7 @@ export class ExternalCloudService {
}; };
} }
async update(options: UpdateEnvironmentRequest): Promise<boolean> { async update(_project: ProjectModel, options: UpdateEnvironmentRequest): Promise<boolean> {
const local = await JSONStorage.get('externalBrokers'); const local = await JSONStorage.get('externalBrokers');
const brokers: EnvironmentDataFormat[] = local.brokers || []; const brokers: EnvironmentDataFormat[] = local.brokers || [];
@@ -63,7 +59,7 @@ export class ExternalCloudService {
return true; return true;
} }
async delete(id: string): Promise<boolean> { async delete(_project: ProjectModel, id: string): Promise<boolean> {
const local = await JSONStorage.get('externalBrokers'); const local = await JSONStorage.get('externalBrokers');
const brokers: EnvironmentDataFormat[] = local.brokers || []; const brokers: EnvironmentDataFormat[] = local.brokers || [];
@@ -79,25 +75,3 @@ export class ExternalCloudService {
return true; return true;
} }
} }
export class Environment {
id: string;
name: string;
description: string;
createdAt: string;
masterKeyUpdatedAt: string;
masterKey: string;
appId: string;
url: string;
constructor(item: EnvironmentDataFormat) {
this.id = item.id;
this.name = item.name;
this.description = item.description;
this.createdAt = '';
this.masterKeyUpdatedAt = '';
this.masterKey = item.masterKey;
this.appId = item.appId;
this.url = item.endpoint;
}
}

View File

@@ -0,0 +1,101 @@
import { filesystem } from '@noodl/platform';
import {
CreateEnvironment,
CreateEnvironmentRequest,
EnvironmentDataFormat,
ICloudServiceProvider,
UpdateEnvironmentRequest
} from '@noodl-models/CloudServices';
import { ProjectModel } from '@noodl-models/projectmodel';
/**
* Store the Cloud Service relative to the project folder.
*/
export class ProjectCloudService implements ICloudServiceProvider {
private async load(project: ProjectModel) {
const dirpath = filesystem.resolve(project._retainedProjectDirectory, ".noodl");
const filepath = filesystem.resolve(dirpath, "cloudservices.json");
if (!filesystem.exists(filepath)) {
return []
}
// TODO: Validate file content
return filesystem.readJson(filepath);
}
private async save(project: ProjectModel, items: EnvironmentDataFormat[]) {
const dirpath = filesystem.resolve(project._retainedProjectDirectory, ".noodl");
const filepath = filesystem.resolve(dirpath, "cloudservices.json");
if (!filesystem.exists(filepath)) {
await filesystem.makeDirectory(dirpath);
}
await filesystem.writeJson(filepath, items);
}
async list(project: ProjectModel): Promise<EnvironmentDataFormat[]> {
if (!project || !project._retainedProjectDirectory) {
return []
}
return await this.load(project);
}
async create(project: ProjectModel, options: CreateEnvironmentRequest): Promise<CreateEnvironment> {
const id = `${options.url}-${options.appId}`;
const newBroker: EnvironmentDataFormat = {
enabled: true,
id,
name: options.name,
description: options.description,
masterKey: options.masterKey,
appId: options.appId,
endpoint: options.url
};
const current = await this.load(project);
await this.save(project, [...current, newBroker]);
return {
id: newBroker.id,
appId: newBroker.appId,
url: newBroker.endpoint,
masterKey: newBroker.masterKey
};
}
async update(project: ProjectModel, options: UpdateEnvironmentRequest): Promise<boolean> {
const current = await this.load(project);
// Find and update
const broker = current.find((x) => x.id === options.id);
if (!broker) return false;
if (typeof options.name !== undefined) broker.name = options.name;
if (typeof options.description !== undefined) broker.description = options.description;
if (typeof options.appId !== undefined) broker.appId = options.appId;
if (typeof options.masterKey !== undefined) broker.masterKey = options.masterKey;
if (typeof options.url !== undefined) broker.endpoint = options.url;
await this.save(project, current);
return true;
}
async delete(project: ProjectModel, id: string): Promise<boolean> {
const current = await this.load(project);
// Find the environment
const found = current.find((b) => b.id === id);
if (found) {
// Delete the environment
current.splice(current.indexOf(found), 1);
}
// Save the list
await this.save(project, current);
return true;
}
}

View File

@@ -1,4 +1,3 @@
import { Environment } from '@noodl-models/CloudServices';
import { ProjectModel } from '@noodl-models/projectmodel'; import { ProjectModel } from '@noodl-models/projectmodel';
import { IModel } from '@noodl-utils/model'; import { IModel } from '@noodl-utils/model';
@@ -30,11 +29,11 @@ export interface ICloudBackendService {
get isLoading(): boolean; get isLoading(): boolean;
get items(): Environment[]; get items(): Environment[];
fetch(): Promise<Environment[]>; fetch(project: ProjectModel): Promise<Environment[]>;
fromProject(project: ProjectModel): Promise<Environment> | undefined; fromProject(project: ProjectModel): Promise<Environment> | undefined;
create(options: CreateEnvironmentRequest): Promise<CreateEnvironment>; create(project: ProjectModel, options: CreateEnvironmentRequest): Promise<CreateEnvironment>;
update(options: UpdateEnvironmentRequest): Promise<boolean>; update(project: ProjectModel, options: UpdateEnvironmentRequest): Promise<boolean>;
delete(id: string): Promise<boolean>; delete(project: ProjectModel, id: string): Promise<boolean>;
} }
export enum CloudServiceEvent { export enum CloudServiceEvent {
@@ -55,3 +54,54 @@ export interface ICloudService extends IModel<CloudServiceEvent, CloudServiceEve
get backend(): ICloudBackendService; get backend(): ICloudBackendService;
} }
/** The data format is separated from our internal model. */
export type EnvironmentDataFormat = {
enabled: boolean;
id: string;
name: string;
description: string;
masterKey: string;
appId: string;
endpoint: string;
};
export class Environment {
id: string;
name: string;
description: string;
createdAt: string;
masterKeyUpdatedAt: string;
masterKey: string;
appId: string;
url: string;
provider: string;
get typeDisplayName() {
switch (this.provider) {
case "project": return "Self hosted";
default: return "Global Self hosted"
}
}
constructor(provider: string, item: EnvironmentDataFormat) {
this.provider = provider;
this.id = item.id;
this.name = item.name;
this.description = item.description;
this.createdAt = '';
this.masterKeyUpdatedAt = '';
this.masterKey = item.masterKey;
this.appId = item.appId;
this.url = item.endpoint;
}
}
export interface ICloudServiceProvider {
list(project: ProjectModel | undefined): Promise<EnvironmentDataFormat[]>;
create(project: ProjectModel | undefined, options: CreateEnvironmentRequest): Promise<CreateEnvironment>;
update(project: ProjectModel | undefined, options: UpdateEnvironmentRequest): Promise<boolean>;
delete(project: ProjectModel | undefined, id: string): Promise<boolean>;
}

View File

@@ -1,4 +1,5 @@
import { CloudService } from '@noodl-models/CloudServices'; import { CloudService } from '@noodl-models/CloudServices';
import { ProjectModel } from '@noodl-models/projectmodel';
import SchemaModel from '@noodl-models/schemamodel'; import SchemaModel from '@noodl-models/schemamodel';
class FormCollection { class FormCollection {
@@ -193,7 +194,7 @@ export default class CloudFormation {
}) { }) {
// Create new cloud services if needed // Create new cloud services if needed
if (options.cloudServices.id === undefined) { if (options.cloudServices.id === undefined) {
CloudService.instance.backend.fetch().then((collection) => { CloudService.instance.backend.fetch(ProjectModel.instance).then((collection) => {
// TODO(OS): Cloud formation Cloud Service // TODO(OS): Cloud formation Cloud Service
// // Make sure we have a unique name for the cloud services // // Make sure we have a unique name for the cloud services
// const orgName = options.cloudServices.name; // const orgName = options.cloudServices.name;

View File

@@ -41,7 +41,7 @@ export default class SchemaHandler {
return; // No project broker return; // No project broker
} }
CloudService.instance.backend.fetch().then((collection) => { CloudService.instance.backend.fetch(ProjectModel.instance).then((collection) => {
// Find by the Url / Endpoint and app id // Find by the Url / Endpoint and app id
let environment = collection.find((b) => { let environment = collection.find((b) => {
return b.url === activeBroker.endpoint && b.appId === activeBroker.appId; return b.url === activeBroker.endpoint && b.appId === activeBroker.appId;

View File

@@ -87,7 +87,7 @@ export function CloudServiceCard({
<div className={css['MetaBar']}> <div className={css['MetaBar']}>
<div className={classNames([css['TypeDisplay'], isEditorEnvironment && css['is-editor-environment']])}> <div className={classNames([css['TypeDisplay'], isEditorEnvironment && css['is-editor-environment']])}>
<Icon icon={IconName.CloudCheck} size={IconSize.Small} UNSAFE_style={{ marginRight: 4 }} /> <Icon icon={IconName.CloudCheck} size={IconSize.Small} UNSAFE_style={{ marginRight: 4 }} />
{'Self hosted '} {environment.typeDisplayName + ' '}
{errorMessage && <span className={css['ArchivedDisplay']}>({errorMessage})</span>} {errorMessage && <span className={css['ArchivedDisplay']}>({errorMessage})</span>}
{isEditorEnvironment && <span className={css['UsedInEditorDisplay']}>(Used in editor)</span>} {isEditorEnvironment && <span className={css['UsedInEditorDisplay']}>(Used in editor)</span>}
</div> </div>

View File

@@ -5,6 +5,7 @@ import { CloudService, Environment } from '@noodl-models/CloudServices';
import { ToastType } from '../../../ToastLayer/components/ToastCard'; import { ToastType } from '../../../ToastLayer/components/ToastCard';
import { CloudServiceCard } from '../CloudServiceCard'; import { CloudServiceCard } from '../CloudServiceCard';
import { useCloudServiceContext } from '../CloudServicePanel.context'; import { useCloudServiceContext } from '../CloudServicePanel.context';
import { ProjectModel } from '@noodl-models/projectmodel';
export interface CloudServiceCardItemProps { export interface CloudServiceCardItemProps {
environment: Environment; environment: Environment;
@@ -20,7 +21,7 @@ export function CloudServiceCardItem({ environment, deleteEnvironment }: CloudSe
async function onDelete() { async function onDelete() {
await deleteEnvironment(); await deleteEnvironment();
await runActivity('Deleting cloud service...', async () => { await runActivity('Deleting cloud service...', async () => {
const response: boolean = await CloudService.instance.backend.delete(environment.id); const response: boolean = await CloudService.instance.backend.delete(ProjectModel.instance, environment.id);
return { return {
type: ToastType.Success, type: ToastType.Success,
message: 'Cloud service deleted' message: 'Cloud service deleted'

View File

@@ -12,6 +12,7 @@ import { Text, TextType } from '@noodl-core-ui/components/typography/Text';
import { ToastType } from '../../../ToastLayer/components/ToastCard'; import { ToastType } from '../../../ToastLayer/components/ToastCard';
import { useCloudServiceContext } from '../CloudServicePanel.context'; import { useCloudServiceContext } from '../CloudServicePanel.context';
import { ProjectModel } from '@noodl-models/projectmodel';
function isValidParseUrl(url: string) { function isValidParseUrl(url: string) {
if (!url) return false; if (!url) return false;
@@ -63,7 +64,7 @@ export function CloudServiceCreateModal({ isVisible, onClose }: CloudServiceCrea
async function onCreate() { async function onCreate() {
await runActivity('Creating Cloud Service...', async () => { await runActivity('Creating Cloud Service...', async () => {
await cloudService.backend.create({ await cloudService.backend.create(ProjectModel.instance, {
name, name,
description, description,
masterKey: masterKey ? masterKey : undefined, masterKey: masterKey ? masterKey : undefined,

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useState } from 'react';
import { FeedbackType } from '@noodl-constants/FeedbackType'; import { FeedbackType } from '@noodl-constants/FeedbackType';
import { CloudService, Environment } from '@noodl-models/CloudServices'; import { CloudService, Environment } from '@noodl-models/CloudServices';
@@ -12,6 +12,7 @@ import { HStack, VStack } from '@noodl-core-ui/components/layout/Stack';
import { Text } from '@noodl-core-ui/components/typography/Text'; import { Text } from '@noodl-core-ui/components/typography/Text';
import { ToastLayer } from '../../../ToastLayer'; import { ToastLayer } from '../../../ToastLayer';
import { ProjectModel } from '@noodl-models/projectmodel';
export interface CloudServiceModalProps { export interface CloudServiceModalProps {
isVisible: boolean; isVisible: boolean;
@@ -60,7 +61,7 @@ function AsSelfHosted({
} }
CloudService.instance.backend CloudService.instance.backend
.update({ .update(ProjectModel.instance, {
id: environment.id, id: environment.id,
name, name,
description, description,
@@ -70,7 +71,7 @@ function AsSelfHosted({
}) })
.then(() => { .then(() => {
ToastLayer.showSuccess(`Updated Cloud Service`); ToastLayer.showSuccess(`Updated Cloud Service`);
CloudService.instance.backend.fetch(); CloudService.instance.backend.fetch(ProjectModel.instance);
}) })
.catch(() => { .catch(() => {
ToastLayer.showError(`Failed to update Cloud Service`); ToastLayer.showError(`Failed to update Cloud Service`);

View File

@@ -35,7 +35,7 @@ export function CloudServiceContextProvider({ children }) {
const { hasActivity, runActivity } = useActivityQueue({ const { hasActivity, runActivity } = useActivityQueue({
onSuccess: async () => { onSuccess: async () => {
// Always fetch all the backends after something have changed // Always fetch all the backends after something have changed
await cloudService.backend.fetch(); await cloudService.backend.fetch(ProjectModel.instance);
} }
}); });