2162 Add asset watcher in twenty-sdk dev mode (#17513)
- assets are pushed in .twenty/output - assets are uploaded in FileFolder.Assets - not handled yet by the sync-manifest endpoint
This commit is contained in:
@@ -10,7 +10,6 @@ import { type FileFolder } from 'twenty-shared/types';
|
||||
import { validateManifest } from '@/cli/utilities/build/manifest/manifest-validate';
|
||||
import type { Location } from 'esbuild';
|
||||
import { type DevUiStateManager } from '@/cli/utilities/dev/dev-ui-state-manager';
|
||||
import { relative } from 'path';
|
||||
import { type EventName } from 'chokidar/handler.js';
|
||||
|
||||
export type DevModeOrchestratorOptions = {
|
||||
@@ -42,8 +41,8 @@ export class DevModeOrchestrator {
|
||||
private syncTimer: NodeJS.Timeout | null = null;
|
||||
private isSyncing = false;
|
||||
private uiStateManager: DevUiStateManager;
|
||||
private serverChecked = false;
|
||||
private serverCheckedLogged = false;
|
||||
private serverReady = false;
|
||||
private serverErrorLogged = false;
|
||||
|
||||
private handleManifestBuilt: (
|
||||
result: ManifestBuildResult,
|
||||
@@ -57,41 +56,59 @@ export class DevModeOrchestrator {
|
||||
}
|
||||
|
||||
private async checkServer(): Promise<void> {
|
||||
this.serverChecked = await this.apiService.validateAuth();
|
||||
const validateAuth = await this.apiService.validateAuth();
|
||||
|
||||
if (!this.serverChecked && !this.serverCheckedLogged) {
|
||||
this.uiStateManager.addEvent({
|
||||
message:
|
||||
'Please check your server is up and your credentials are correct: "yarn auth:login"',
|
||||
status: 'error',
|
||||
});
|
||||
this.uiStateManager.updateManifestState({
|
||||
manifestStatus: 'error',
|
||||
});
|
||||
this.serverCheckedLogged = true;
|
||||
if (!validateAuth.serverUp) {
|
||||
if (!this.serverErrorLogged) {
|
||||
this.uiStateManager.addEvent({
|
||||
message: 'Cannot reach server',
|
||||
status: 'error',
|
||||
});
|
||||
this.uiStateManager.updateManifestState({
|
||||
manifestStatus: 'error',
|
||||
error: 'Cannot connect to Twenty server. Is it running?',
|
||||
});
|
||||
this.serverErrorLogged = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!validateAuth.authValid) {
|
||||
if (!this.serverErrorLogged) {
|
||||
this.uiStateManager.addEvent({
|
||||
message: 'Authentication failed',
|
||||
status: 'error',
|
||||
});
|
||||
this.uiStateManager.updateManifestState({
|
||||
manifestStatus: 'error',
|
||||
error:
|
||||
'Cannot authenticate. Check your credentials are correct with "yarn auth:login"',
|
||||
});
|
||||
this.serverErrorLogged = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.serverErrorLogged = false;
|
||||
this.serverReady = true;
|
||||
}
|
||||
|
||||
async handleChangeDetected(sourcePath: string, event: EventName) {
|
||||
if (!this.serverChecked) {
|
||||
if (!this.serverReady) {
|
||||
await this.checkServer();
|
||||
}
|
||||
|
||||
if (!this.serverChecked) {
|
||||
if (!this.serverReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedSourcePath = this.normalizeFilePath(sourcePath);
|
||||
|
||||
this.uiStateManager.addEvent({
|
||||
message: `Change detected: ${normalizedSourcePath}`,
|
||||
message: `Change detected: ${sourcePath}`,
|
||||
status: 'info',
|
||||
});
|
||||
|
||||
if (event === 'unlink') {
|
||||
this.uiStateManager.removeEntity(normalizedSourcePath);
|
||||
this.uiStateManager.removeEntity(sourcePath);
|
||||
} else {
|
||||
this.uiStateManager.updateFileStatus(normalizedSourcePath, 'building');
|
||||
this.uiStateManager.updateFileStatus(sourcePath, 'building');
|
||||
}
|
||||
|
||||
this.scheduleSync();
|
||||
@@ -142,10 +159,6 @@ export class DevModeOrchestrator {
|
||||
this.scheduleSync();
|
||||
}
|
||||
|
||||
private normalizeFilePath(filePath: string): string {
|
||||
return relative(this.appPath, filePath);
|
||||
}
|
||||
|
||||
private uploadFile(
|
||||
builtPath: string,
|
||||
sourcePath: string,
|
||||
@@ -295,6 +308,7 @@ export class DevModeOrchestrator {
|
||||
manifest: result.manifest,
|
||||
builtFileInfos: this.builtFileInfos,
|
||||
});
|
||||
|
||||
this.uiStateManager.addEvent({
|
||||
message: 'Manifest checksums set',
|
||||
status: 'info',
|
||||
@@ -311,9 +325,11 @@ export class DevModeOrchestrator {
|
||||
message: 'Syncing manifest',
|
||||
status: 'info',
|
||||
});
|
||||
|
||||
this.uiStateManager.updateManifestState({
|
||||
manifestStatus: 'syncing',
|
||||
});
|
||||
|
||||
const syncResult = await this.apiService.syncApplication(manifest);
|
||||
|
||||
this.uiStateManager.updateAllFilesStatus('success');
|
||||
|
||||
@@ -10,6 +10,13 @@ import { type EntityFilePaths } from '@/cli/utilities/build/manifest/manifest-bu
|
||||
|
||||
const MAX_EVENT_NUMBER = 200;
|
||||
|
||||
const FILE_STATUS_TRANSITION_MATRIX: Record<FileStatus, FileStatus[]> = {
|
||||
pending: ['building', 'uploading', 'success'],
|
||||
building: ['pending', 'uploading', 'success'],
|
||||
uploading: ['pending', 'success'],
|
||||
success: ['pending', 'building', 'uploading'],
|
||||
};
|
||||
|
||||
export class DevUiStateManager {
|
||||
private state: DevUiState;
|
||||
private eventIdCounter = 0;
|
||||
@@ -75,14 +82,17 @@ export class DevUiStateManager {
|
||||
updateManifestState({
|
||||
manifestStatus,
|
||||
appName,
|
||||
error,
|
||||
}: {
|
||||
manifestStatus?: ManifestStatus;
|
||||
appName?: string;
|
||||
error?: string;
|
||||
}): void {
|
||||
this.state = {
|
||||
...this.state,
|
||||
...(manifestStatus ? { manifestStatus } : {}),
|
||||
...(appName ? { appName } : {}),
|
||||
...(error ? { error } : {}),
|
||||
};
|
||||
|
||||
this.notify();
|
||||
@@ -102,6 +112,8 @@ export class DevUiStateManager {
|
||||
return SyncableEntity.FrontComponent;
|
||||
case 'roles':
|
||||
return SyncableEntity.Role;
|
||||
case 'assets':
|
||||
return SyncableEntity.PublicAsset;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
@@ -164,11 +176,27 @@ export class DevUiStateManager {
|
||||
updateFileStatus(filePath: string, status: FileStatus): void {
|
||||
const entities = new Map(this.state.entities);
|
||||
|
||||
entities.set(filePath, {
|
||||
name: filePath,
|
||||
path: filePath,
|
||||
status: status,
|
||||
});
|
||||
const entity = entities.get(filePath);
|
||||
|
||||
if (
|
||||
entity?.status &&
|
||||
!FILE_STATUS_TRANSITION_MATRIX[entity.status].find(
|
||||
(nextStatus) => nextStatus === status,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
entities.set(
|
||||
filePath,
|
||||
entity
|
||||
? { ...entity, status }
|
||||
: {
|
||||
name: filePath,
|
||||
path: filePath,
|
||||
status,
|
||||
},
|
||||
);
|
||||
|
||||
this.state = { ...this.state, entities };
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export type DevUiState = {
|
||||
appUniversalIdentifier: string | null;
|
||||
frontendUrl?: string | null;
|
||||
manifestStatus: ManifestStatus;
|
||||
error?: string | null;
|
||||
entities: Map<string, EntityInfo>;
|
||||
events: UiEvent[];
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ const ENTITY_LABELS: Record<SyncableEntity, string> = {
|
||||
[SyncableEntity.Function]: 'Functions',
|
||||
[SyncableEntity.FrontComponent]: 'Front Components',
|
||||
[SyncableEntity.Role]: 'Roles',
|
||||
[SyncableEntity.PublicAsset]: 'Public Assets',
|
||||
};
|
||||
|
||||
const ENTITY_ORDER = Object.keys(ENTITY_LABELS) as SyncableEntity[];
|
||||
@@ -176,10 +177,10 @@ export const renderDevUI = async (
|
||||
|
||||
const MANIFEST_STATUS_CONFIG = {
|
||||
synced: { color: 'green', icon: '✓', text: 'Synced' },
|
||||
building: { color: 'yellow', icon: null, text: 'Building...' },
|
||||
syncing: { color: 'yellow', icon: null, text: 'Syncing...' },
|
||||
error: { color: 'red', icon: 'x', text: 'Error' },
|
||||
idle: { color: 'gray', icon: 'o', text: 'Idle' },
|
||||
building: { color: 'yellow', icon: 'spinner', text: 'Building...' },
|
||||
syncing: { color: 'yellow', icon: 'spinner', text: 'Syncing...' },
|
||||
error: { color: 'red', icon: null, text: 'Error' },
|
||||
idle: { color: 'gray', icon: null, text: 'Idle' },
|
||||
} as const;
|
||||
|
||||
const UnifiedStatusIndicator = ({
|
||||
@@ -189,11 +190,13 @@ export const renderDevUI = async (
|
||||
}): React.ReactElement => {
|
||||
const spinnerFrame = useSpinner(SPINNER_FRAMES, 80);
|
||||
const config = MANIFEST_STATUS_CONFIG[snapshot.manifestStatus];
|
||||
const icon = config.icon ?? spinnerFrame;
|
||||
const icon = config.icon === 'spinner' ? spinnerFrame : config.icon;
|
||||
|
||||
return (
|
||||
<Text color={config.color}>
|
||||
{icon} {config.text}
|
||||
{icon ?? ''}
|
||||
{config.text}
|
||||
{snapshot.error && `: ${snapshot.error}`}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user