mirror of
https://github.com/fluxscape/fluxscape.git
synced 2026-01-11 23:02:55 +01:00
feat: Add initial Fluxscape deploy popup (#6)
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
import React, { createContext, useContext, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { Slot } from '@noodl-core-ui/types/global';
|
||||||
|
|
||||||
|
import { commandEventHandler } from './iframe';
|
||||||
|
|
||||||
|
export interface PluginContext {}
|
||||||
|
|
||||||
|
const PluginContext = createContext<PluginContext>({});
|
||||||
|
|
||||||
|
export interface PluginContextProps {
|
||||||
|
children: Slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PluginContextProvider({ children }: PluginContextProps) {
|
||||||
|
// Listen for all the iframe commands
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('message', commandEventHandler, false);
|
||||||
|
return function () {
|
||||||
|
window.removeEventListener('message', commandEventHandler, false);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <PluginContext.Provider value={{}}>{children}</PluginContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePluginContext() {
|
||||||
|
const context = useContext(PluginContext);
|
||||||
|
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('usePluginContext must be a child of PluginContextProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { ToastLayer } from '../../../views/ToastLayer';
|
||||||
|
|
||||||
|
export type Command = {
|
||||||
|
kind: 'notify';
|
||||||
|
messageType: 'info' | 'success' | 'error';
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function execute(command: Command, _event: MessageEvent) {
|
||||||
|
switch (command.messageType || 'info') {
|
||||||
|
case 'info': {
|
||||||
|
ToastLayer.showInteraction(String(command.message));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'success': {
|
||||||
|
ToastLayer.showSuccess(String(command.message));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'error': {
|
||||||
|
ToastLayer.showError(String(command.message));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import s3 from 's3';
|
||||||
|
import { filesystem, platform } from '@noodl/platform';
|
||||||
|
|
||||||
|
import { Environment, EnvironmentDataFormat } from '@noodl-models/CloudServices';
|
||||||
|
import { ProjectModel } from '@noodl-models/projectmodel';
|
||||||
|
import { createEditorCompilation } from '@noodl-utils/compilation/compilation.editor';
|
||||||
|
import { guid } from '@noodl-utils/utils';
|
||||||
|
|
||||||
|
import { ToastLayer } from '../../../views/ToastLayer';
|
||||||
|
|
||||||
|
export type Command = {
|
||||||
|
kind: 'upload-aws-s3';
|
||||||
|
taskId: string;
|
||||||
|
cloudService: EnvironmentDataFormat;
|
||||||
|
s3: {
|
||||||
|
accessKeyId: string;
|
||||||
|
secretAccessKey: string;
|
||||||
|
sessionToken: string;
|
||||||
|
region: string;
|
||||||
|
bucket: string;
|
||||||
|
prefix: string;
|
||||||
|
}
|
||||||
|
messages: {
|
||||||
|
progress: string;
|
||||||
|
success: string;
|
||||||
|
failure: string;
|
||||||
|
}
|
||||||
|
// Initial version to a more extendable system that can be used with these
|
||||||
|
// kind of events.
|
||||||
|
events: {
|
||||||
|
complete: {
|
||||||
|
kind: "fetch",
|
||||||
|
resource: string,
|
||||||
|
options: RequestInit
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function execute(command: Command, event: MessageEvent) {
|
||||||
|
const project = ProjectModel.instance;
|
||||||
|
const taskId = command.taskId || guid();
|
||||||
|
|
||||||
|
// Setup the build steps
|
||||||
|
const compilation = createEditorCompilation(project).addProjectBuildScripts();
|
||||||
|
|
||||||
|
// Create a temp folder
|
||||||
|
const tempDir = platform.getTempPath() + '/upload-' + taskId;
|
||||||
|
|
||||||
|
// Deploy to temp folder
|
||||||
|
await compilation.deployToFolder(tempDir, {
|
||||||
|
environment: command.cloudService ? new Environment(command.cloudService) : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
// Upload to S3
|
||||||
|
try {
|
||||||
|
await uploadDirectory(
|
||||||
|
{
|
||||||
|
localDir: tempDir,
|
||||||
|
accessKeyId: command.s3.accessKeyId,
|
||||||
|
secretAccessKey: command.s3.secretAccessKey,
|
||||||
|
sessionToken: command.s3.sessionToken,
|
||||||
|
region: command.s3.region,
|
||||||
|
bucket: command.s3.bucket,
|
||||||
|
prefix: command.s3.prefix
|
||||||
|
},
|
||||||
|
({ progress, total }) => {
|
||||||
|
ToastLayer.showProgress(command.messages.progress, (progress / total) * 100, taskId);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ToastLayer.showSuccess(command.messages.success);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
ToastLayer.showError(command.messages.failure);
|
||||||
|
} finally {
|
||||||
|
ToastLayer.hideProgress(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the temp folder
|
||||||
|
filesystem.removeDirRecursive(tempDir);
|
||||||
|
|
||||||
|
// NOTE: Would be nice to clean up the events in here, so "complete" is like
|
||||||
|
// "finally". And then also have "success" and "failure".
|
||||||
|
|
||||||
|
// Notify that the process is done
|
||||||
|
event.source.postMessage({
|
||||||
|
kind: "upload-aws-s3",
|
||||||
|
id: taskId,
|
||||||
|
status: "complete",
|
||||||
|
}, { targetOrigin: event.origin })
|
||||||
|
|
||||||
|
// Execute complete event
|
||||||
|
if (Array.isArray(command?.events?.complete)) {
|
||||||
|
for (const event of command.events.complete) {
|
||||||
|
if (event.kind === "fetch") {
|
||||||
|
await fetch(event.resource, event.options);
|
||||||
|
} else {
|
||||||
|
console.error("invalid event type", event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadDirectory(
|
||||||
|
options: {
|
||||||
|
localDir: string;
|
||||||
|
accessKeyId: string;
|
||||||
|
secretAccessKey: string;
|
||||||
|
sessionToken: string;
|
||||||
|
region: string;
|
||||||
|
bucket: string;
|
||||||
|
prefix: string;
|
||||||
|
},
|
||||||
|
progress: (args: { progress: number; total: number }) => void
|
||||||
|
) {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
// https://github.com/noodlapp/node-s3-client?tab=readme-ov-file#create-a-client
|
||||||
|
const client = s3.createClient({
|
||||||
|
maxAsyncS3: 20, // this is the default
|
||||||
|
s3RetryCount: 3, // this is the default
|
||||||
|
s3RetryDelay: 1000, // this is the default
|
||||||
|
multipartUploadThreshold: 20971520, // this is the default (20 MB)
|
||||||
|
multipartUploadSize: 15728640, // this is the default (15 MB)
|
||||||
|
s3Options: {
|
||||||
|
accessKeyId: options.accessKeyId,
|
||||||
|
secretAccessKey: options.secretAccessKey,
|
||||||
|
sessionToken: options.sessionToken,
|
||||||
|
region: options.region
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function getFileCacheControl(fullPath: string) {
|
||||||
|
switch (fullPath.split('.').at(-1)) {
|
||||||
|
case 'html':
|
||||||
|
return 'no-cache';
|
||||||
|
default: {
|
||||||
|
const TIME_1 = 31536000; // 365 days
|
||||||
|
const TIME_2 = 10800; // 3 hours for files with hash
|
||||||
|
const maxAge = /-[a-f0-9]{16}\./.test(fullPath) ? TIME_1 : TIME_2;
|
||||||
|
return `public, max-age=${maxAge}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploader = client.uploadDir({
|
||||||
|
localDir: options.localDir,
|
||||||
|
deleteRemoved: true,
|
||||||
|
s3Params: {
|
||||||
|
Bucket: options.bucket,
|
||||||
|
Prefix: options.prefix
|
||||||
|
},
|
||||||
|
getS3Params(fullPath, _s3Object, callback) {
|
||||||
|
callback(null, {
|
||||||
|
CacheControl: getFileCacheControl(fullPath)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
uploader.on('error', function (error) {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
uploader.on('progress', function () {
|
||||||
|
progress({ progress: uploader.progressAmount, total: uploader.progressTotal });
|
||||||
|
});
|
||||||
|
uploader.on('end', function () {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import * as Notify from './commands/notify';
|
||||||
|
import * as UploadAwsS3 from './commands/upload-aws-s3';
|
||||||
|
|
||||||
|
type IFrameCommand = Notify.Command | UploadAwsS3.Command;
|
||||||
|
|
||||||
|
const commands: Record<IFrameCommand['kind'], (command: IFrameCommand, event: MessageEvent) => Promise<void>> = {
|
||||||
|
notify: Notify.execute,
|
||||||
|
'upload-aws-s3': UploadAwsS3.execute
|
||||||
|
};
|
||||||
|
|
||||||
|
export function commandEventHandler(event: MessageEvent) {
|
||||||
|
try {
|
||||||
|
const command = event.data;
|
||||||
|
const handler = commands[command.kind];
|
||||||
|
handler && handler(command, event);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './PluginContext';
|
||||||
@@ -3,7 +3,7 @@ import { JSONStorage } from '@noodl/platform';
|
|||||||
import { CreateEnvironment, CreateEnvironmentRequest, UpdateEnvironmentRequest } from '@noodl-models/CloudServices';
|
import { CreateEnvironment, CreateEnvironmentRequest, UpdateEnvironmentRequest } from '@noodl-models/CloudServices';
|
||||||
|
|
||||||
/** The data format is separated from our internal model. */
|
/** The data format is separated from our internal model. */
|
||||||
type EnvironmentDataFormat = {
|
export type EnvironmentDataFormat = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import { BaseWindow } from '../../views/windows/BaseWindow';
|
|||||||
import { whatsnewRender } from '../../whats-new';
|
import { whatsnewRender } from '../../whats-new';
|
||||||
import { IRouteProps } from '../AppRoute';
|
import { IRouteProps } from '../AppRoute';
|
||||||
import { useSetupSettings } from './useSetupSettings';
|
import { useSetupSettings } from './useSetupSettings';
|
||||||
|
import { PluginContextProvider } from '@noodl-contexts/PluginContext';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const ImportOverwritePopupTemplate = require('../../templates/importoverwritepopup.html');
|
const ImportOverwritePopupTemplate = require('../../templates/importoverwritepopup.html');
|
||||||
@@ -223,24 +224,26 @@ export function EditorPage({ route }: EditorPageProps) {
|
|||||||
return (
|
return (
|
||||||
<NodeGraphContextProvider>
|
<NodeGraphContextProvider>
|
||||||
<ProjectDesignTokenContextProvider>
|
<ProjectDesignTokenContextProvider>
|
||||||
<BaseWindow>
|
<PluginContextProvider>
|
||||||
{isLoading ? (
|
<BaseWindow>
|
||||||
<ActivityIndicator />
|
{isLoading ? (
|
||||||
) : (
|
<ActivityIndicator />
|
||||||
<>
|
) : (
|
||||||
<FrameDivider
|
<>
|
||||||
first={<SidePanel />}
|
<FrameDivider
|
||||||
second={<ErrorBoundary>{Boolean(Document) && <Document />}</ErrorBoundary>}
|
first={<SidePanel />}
|
||||||
sizeMin={200}
|
second={<ErrorBoundary>{Boolean(Document) && <Document />}</ErrorBoundary>}
|
||||||
size={frameDividerSize}
|
sizeMin={200}
|
||||||
horizontal
|
size={frameDividerSize}
|
||||||
onSizeChanged={setFrameDividerSize}
|
horizontal
|
||||||
/>
|
onSizeChanged={setFrameDividerSize}
|
||||||
|
/>
|
||||||
|
|
||||||
{Boolean(lesson) && <Frame instance={lesson} isContentSize isFitWidth />}
|
{Boolean(lesson) && <Frame instance={lesson} isContentSize isFitWidth />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</BaseWindow>
|
</BaseWindow>
|
||||||
|
</PluginContextProvider>
|
||||||
</ProjectDesignTokenContextProvider>
|
</ProjectDesignTokenContextProvider>
|
||||||
</NodeGraphContextProvider>
|
</NodeGraphContextProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { RefObject } from 'react';
|
import { usePluginContext } from '@noodl-contexts/PluginContext';
|
||||||
|
import React, { RefObject, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { ActivityIndicator } from '@noodl-core-ui/components/common/ActivityIndicator';
|
import { ActivityIndicator } from '@noodl-core-ui/components/common/ActivityIndicator';
|
||||||
import { BaseDialog } from '@noodl-core-ui/components/layout/BaseDialog';
|
import { BaseDialog } from '@noodl-core-ui/components/layout/BaseDialog';
|
||||||
@@ -25,7 +26,12 @@ function DeployPopupChild() {
|
|||||||
>
|
>
|
||||||
<PopupSection title="Deploy options" />
|
<PopupSection title="Deploy options" />
|
||||||
|
|
||||||
<Tabs tabs={[{ label: 'Self Hosting', content: <DeployToFolderTab />, testId: 'self-hosting-tab-button' }]} />
|
<Tabs
|
||||||
|
tabs={[
|
||||||
|
{ label: 'Fluxscape', content: <FluxscapeDeployTab />, testId: 'fluxscape-tab-button' },
|
||||||
|
{ label: 'Self Hosting', content: <DeployToFolderTab />, testId: 'self-hosting-tab-button' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
{hasActivity && <ActivityIndicator isOverlay />}
|
{hasActivity && <ActivityIndicator isOverlay />}
|
||||||
</div>
|
</div>
|
||||||
@@ -54,3 +60,8 @@ export function DeployPopup(props: DeployPopupProps) {
|
|||||||
</DeployContextProvider>
|
</DeployContextProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FluxscapeDeployTab() {
|
||||||
|
// Preview URL: 'http://192.168.0.33:8574/'
|
||||||
|
return <iframe src="https://portal.fluxscape.io" style={{ width: "100%", height: "50vh", borderStyle: "none" }} />;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user