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
await compilation.deployToFolder(tempDir, {
environment: command.cloudService ? new Environment(command.cloudService) : undefined
environment: command.cloudService ? new Environment("", command.cloudService) : undefined
});
// Upload to S3

View File

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

View File

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

View File

@@ -1,25 +1,21 @@
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 type EnvironmentDataFormat = {
enabled: boolean;
id: string;
name: string;
description: string;
masterKey: string;
appId: string;
endpoint: string;
};
export class ExternalCloudService {
async list(): Promise<EnvironmentDataFormat[]> {
export class GlobalCloudService implements ICloudServiceProvider {
async list(_project: ProjectModel): Promise<EnvironmentDataFormat[]> {
const local = await JSONStorage.get('externalBrokers');
return local.brokers || [];
}
async create(options: CreateEnvironmentRequest): Promise<CreateEnvironment> {
async create(_project: ProjectModel, options: CreateEnvironmentRequest): Promise<CreateEnvironment> {
const id = `${options.url}-${options.appId}`;
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 brokers: EnvironmentDataFormat[] = local.brokers || [];
@@ -63,7 +59,7 @@ export class ExternalCloudService {
return true;
}
async delete(id: string): Promise<boolean> {
async delete(_project: ProjectModel, id: string): Promise<boolean> {
const local = await JSONStorage.get('externalBrokers');
const brokers: EnvironmentDataFormat[] = local.brokers || [];
@@ -79,25 +75,3 @@ export class ExternalCloudService {
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 { IModel } from '@noodl-utils/model';
@@ -30,11 +29,11 @@ export interface ICloudBackendService {
get isLoading(): boolean;
get items(): Environment[];
fetch(): Promise<Environment[]>;
fetch(project: ProjectModel): Promise<Environment[]>;
fromProject(project: ProjectModel): Promise<Environment> | undefined;
create(options: CreateEnvironmentRequest): Promise<CreateEnvironment>;
update(options: UpdateEnvironmentRequest): Promise<boolean>;
delete(id: string): Promise<boolean>;
create(project: ProjectModel, options: CreateEnvironmentRequest): Promise<CreateEnvironment>;
update(project: ProjectModel, options: UpdateEnvironmentRequest): Promise<boolean>;
delete(project: ProjectModel, id: string): Promise<boolean>;
}
export enum CloudServiceEvent {
@@ -55,3 +54,54 @@ export interface ICloudService extends IModel<CloudServiceEvent, CloudServiceEve
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 { ProjectModel } from '@noodl-models/projectmodel';
import SchemaModel from '@noodl-models/schemamodel';
class FormCollection {
@@ -193,7 +194,7 @@ export default class CloudFormation {
}) {
// Create new cloud services if needed
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
// // Make sure we have a unique name for the cloud services
// const orgName = options.cloudServices.name;

View File

@@ -41,7 +41,7 @@ export default class SchemaHandler {
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
let environment = collection.find((b) => {
return b.url === activeBroker.endpoint && b.appId === activeBroker.appId;

View File

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

View File

@@ -5,6 +5,7 @@ import { CloudService, Environment } from '@noodl-models/CloudServices';
import { ToastType } from '../../../ToastLayer/components/ToastCard';
import { CloudServiceCard } from '../CloudServiceCard';
import { useCloudServiceContext } from '../CloudServicePanel.context';
import { ProjectModel } from '@noodl-models/projectmodel';
export interface CloudServiceCardItemProps {
environment: Environment;
@@ -20,7 +21,7 @@ export function CloudServiceCardItem({ environment, deleteEnvironment }: CloudSe
async function onDelete() {
await deleteEnvironment();
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 {
type: ToastType.Success,
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 { useCloudServiceContext } from '../CloudServicePanel.context';
import { ProjectModel } from '@noodl-models/projectmodel';
function isValidParseUrl(url: string) {
if (!url) return false;
@@ -63,7 +64,7 @@ export function CloudServiceCreateModal({ isVisible, onClose }: CloudServiceCrea
async function onCreate() {
await runActivity('Creating Cloud Service...', async () => {
await cloudService.backend.create({
await cloudService.backend.create(ProjectModel.instance, {
name,
description,
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 { 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 { ToastLayer } from '../../../ToastLayer';
import { ProjectModel } from '@noodl-models/projectmodel';
export interface CloudServiceModalProps {
isVisible: boolean;
@@ -60,7 +61,7 @@ function AsSelfHosted({
}
CloudService.instance.backend
.update({
.update(ProjectModel.instance, {
id: environment.id,
name,
description,
@@ -70,7 +71,7 @@ function AsSelfHosted({
})
.then(() => {
ToastLayer.showSuccess(`Updated Cloud Service`);
CloudService.instance.backend.fetch();
CloudService.instance.backend.fetch(ProjectModel.instance);
})
.catch(() => {
ToastLayer.showError(`Failed to update Cloud Service`);

View File

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