mirror of
https://github.com/fluxscape/fluxscape.git
synced 2026-01-11 14:52:54 +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';
|
||||
|
||||
/** The data format is separated from our internal model. */
|
||||
type EnvironmentDataFormat = {
|
||||
export type EnvironmentDataFormat = {
|
||||
enabled: boolean;
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -43,6 +43,7 @@ import { BaseWindow } from '../../views/windows/BaseWindow';
|
||||
import { whatsnewRender } from '../../whats-new';
|
||||
import { IRouteProps } from '../AppRoute';
|
||||
import { useSetupSettings } from './useSetupSettings';
|
||||
import { PluginContextProvider } from '@noodl-contexts/PluginContext';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const ImportOverwritePopupTemplate = require('../../templates/importoverwritepopup.html');
|
||||
@@ -223,24 +224,26 @@ export function EditorPage({ route }: EditorPageProps) {
|
||||
return (
|
||||
<NodeGraphContextProvider>
|
||||
<ProjectDesignTokenContextProvider>
|
||||
<BaseWindow>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<>
|
||||
<FrameDivider
|
||||
first={<SidePanel />}
|
||||
second={<ErrorBoundary>{Boolean(Document) && <Document />}</ErrorBoundary>}
|
||||
sizeMin={200}
|
||||
size={frameDividerSize}
|
||||
horizontal
|
||||
onSizeChanged={setFrameDividerSize}
|
||||
/>
|
||||
<PluginContextProvider>
|
||||
<BaseWindow>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<>
|
||||
<FrameDivider
|
||||
first={<SidePanel />}
|
||||
second={<ErrorBoundary>{Boolean(Document) && <Document />}</ErrorBoundary>}
|
||||
sizeMin={200}
|
||||
size={frameDividerSize}
|
||||
horizontal
|
||||
onSizeChanged={setFrameDividerSize}
|
||||
/>
|
||||
|
||||
{Boolean(lesson) && <Frame instance={lesson} isContentSize isFitWidth />}
|
||||
</>
|
||||
)}
|
||||
</BaseWindow>
|
||||
{Boolean(lesson) && <Frame instance={lesson} isContentSize isFitWidth />}
|
||||
</>
|
||||
)}
|
||||
</BaseWindow>
|
||||
</PluginContextProvider>
|
||||
</ProjectDesignTokenContextProvider>
|
||||
</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 { BaseDialog } from '@noodl-core-ui/components/layout/BaseDialog';
|
||||
@@ -25,7 +26,12 @@ function DeployPopupChild() {
|
||||
>
|
||||
<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 />}
|
||||
</div>
|
||||
@@ -54,3 +60,8 @@ export function DeployPopup(props: DeployPopupProps) {
|
||||
</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