Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7a307c9d3 | |||
| 06d54db747 | |||
| 756c35aa05 | |||
| 2adb86c5ea |
@@ -3,6 +3,34 @@
|
||||
## Pending
|
||||
|
||||
|
||||
## 2026-05-26 - 6.3.1
|
||||
|
||||
- remove redundant card wrappers around Cloudly tables (ui)
|
||||
- Lets `dees-table` provide its own card shell in service, image, and task history views.
|
||||
- Moves the live deployment refresh action into the table header actions.
|
||||
|
||||
### Fixes
|
||||
|
||||
- remove redundant wrappers around Cloudly tables (ui)
|
||||
- Let dees-table provide its own card shell in service, image, and task history views.
|
||||
- Move the live deployments refresh action into the deployments table header actions.
|
||||
- Bump @types/node to ^25.9.1.
|
||||
|
||||
## 2026-05-26 - 6.3.0
|
||||
|
||||
- add hosted app lifecycle protocol support (hostedapp)
|
||||
- Implements generic Hosted App TypedRequest handlers for Cloudly-hosted App Store services.
|
||||
- Injects service-scoped runtime identity environment variables into Cloudly App Store installs.
|
||||
- Lets Cloudly report initial admin bootstrap credentials to its parent host when `SERVEZONE_ADMINACCOUNT` is not configured.
|
||||
|
||||
### Features
|
||||
|
||||
- add hosted app lifecycle protocol support (hostedapp)
|
||||
- Adds a hosted app manager with lifecycle, bootstrap, and managed upgrade TypedRequest handlers.
|
||||
- Injects hosted app runtime identity environment variables into App Store installs.
|
||||
- Allows initial admin bootstrap credentials to be requested from the parent hosted app runtime when SERVEZONE_ADMINACCOUNT is not configured.
|
||||
- Updates hosted app platform requirements and @serve.zone/interfaces for the lifecycle protocol.
|
||||
|
||||
## 2026-05-26 - 6.2.0
|
||||
|
||||
### Features
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@serve.zone/cloudly",
|
||||
"version": "6.2.0",
|
||||
"version": "6.3.1",
|
||||
"private": true,
|
||||
"description": "A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.",
|
||||
"type": "module",
|
||||
@@ -31,7 +31,7 @@
|
||||
"@git.zone/tstest": "^3.6.6",
|
||||
"@git.zone/tswatch": "^3.3.5",
|
||||
"@push.rocks/smartnetwork": "^4.7.1",
|
||||
"@types/node": "^25.9.0"
|
||||
"@types/node": "^25.9.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@api.global/typedrequest": "3.3.1",
|
||||
@@ -80,7 +80,7 @@
|
||||
"@push.rocks/webjwt": "^1.0.10",
|
||||
"@serve.zone/api": "^5.3.8",
|
||||
"@serve.zone/appstore": "^0.2.0",
|
||||
"@serve.zone/interfaces": "^6.0.1",
|
||||
"@serve.zone/interfaces": "^6.1.0",
|
||||
"@tsclass/tsclass": "^9.5.1"
|
||||
},
|
||||
"files": [
|
||||
|
||||
Generated
+27
-27
@@ -147,8 +147,8 @@ importers:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
'@serve.zone/interfaces':
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0
|
||||
'@tsclass/tsclass':
|
||||
specifier: ^9.5.1
|
||||
version: 9.5.1
|
||||
@@ -178,8 +178,8 @@ importers:
|
||||
specifier: ^4.7.1
|
||||
version: 4.7.1
|
||||
'@types/node':
|
||||
specifier: ^25.9.0
|
||||
version: 25.9.0
|
||||
specifier: ^25.9.1
|
||||
version: 25.9.1
|
||||
|
||||
packages:
|
||||
|
||||
@@ -1838,8 +1838,8 @@ packages:
|
||||
'@serve.zone/interfaces@5.10.0':
|
||||
resolution: {integrity: sha512-8ZnP1A43UZlYwfd2j+S0Yin//didacIX2Rou9MobRuSFFgi1RQOqQcIWqOINcDk80wBDuYkyMCwHygYxD5i+Ig==}
|
||||
|
||||
'@serve.zone/interfaces@6.0.1':
|
||||
resolution: {integrity: sha512-ZeLi0Bge8qRMoZMN5/xQ/8VRI4ep9ImitpZtNuLmeNHu0pGICcBGQE4g1aMmi+E3JynKOAphH4dnVmRULZV/RA==}
|
||||
'@serve.zone/interfaces@6.1.0':
|
||||
resolution: {integrity: sha512-nhxMmMfemBaGM1xxFpbNM8/zPM4Y59mVsgz9XBNGZr6n7kn81QsY+Xcn5HnLywztuGHqgEZRWGmI4MPzORRktw==}
|
||||
|
||||
'@shikijs/engine-oniguruma@3.23.0':
|
||||
resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==}
|
||||
@@ -2308,11 +2308,11 @@ packages:
|
||||
'@types/node@18.19.130':
|
||||
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
|
||||
|
||||
'@types/node@22.19.18':
|
||||
resolution: {integrity: sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ==}
|
||||
'@types/node@22.19.19':
|
||||
resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==}
|
||||
|
||||
'@types/node@25.9.0':
|
||||
resolution: {integrity: sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==}
|
||||
'@types/node@25.9.1':
|
||||
resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==}
|
||||
|
||||
'@types/randomatic@3.1.5':
|
||||
resolution: {integrity: sha512-VCwCTw6qh1pRRw+5rNTAwqPmf6A+hdrkdM7dBpZVmhl7g+em3ONXlYK/bWPVKqVGMWgP0d1bog8Vc/X6zRwRRQ==}
|
||||
@@ -5735,7 +5735,7 @@ snapshots:
|
||||
|
||||
'@happy-dom/global-registrator@20.9.0':
|
||||
dependencies:
|
||||
'@types/node': 25.9.0
|
||||
'@types/node': 25.9.1
|
||||
happy-dom: 20.9.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
@@ -5855,7 +5855,7 @@ snapshots:
|
||||
'@inquirer/figures': 1.0.15
|
||||
'@inquirer/type': 2.0.0
|
||||
'@types/mute-stream': 0.0.4
|
||||
'@types/node': 22.19.18
|
||||
'@types/node': 22.19.19
|
||||
'@types/wrap-ansi': 3.0.0
|
||||
ansi-escapes: 4.3.2
|
||||
cli-width: 4.1.0
|
||||
@@ -7364,7 +7364,7 @@ snapshots:
|
||||
|
||||
'@serve.zone/appstore@0.2.0':
|
||||
dependencies:
|
||||
'@serve.zone/interfaces': 6.0.1
|
||||
'@serve.zone/interfaces': 6.1.0
|
||||
|
||||
'@serve.zone/interfaces@5.10.0':
|
||||
dependencies:
|
||||
@@ -7372,7 +7372,7 @@ snapshots:
|
||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||
'@tsclass/tsclass': 9.5.1
|
||||
|
||||
'@serve.zone/interfaces@6.0.1':
|
||||
'@serve.zone/interfaces@6.1.0':
|
||||
dependencies:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||
@@ -7915,7 +7915,7 @@ snapshots:
|
||||
|
||||
'@types/clean-css@4.2.11':
|
||||
dependencies:
|
||||
'@types/node': 25.9.0
|
||||
'@types/node': 25.9.1
|
||||
source-map: 0.6.1
|
||||
|
||||
'@types/debug@4.1.13':
|
||||
@@ -7931,7 +7931,7 @@ snapshots:
|
||||
'@types/fs-extra@11.0.4':
|
||||
dependencies:
|
||||
'@types/jsonfile': 6.1.4
|
||||
'@types/node': 25.9.0
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
@@ -7947,12 +7947,12 @@ snapshots:
|
||||
|
||||
'@types/jsonfile@6.1.4':
|
||||
dependencies:
|
||||
'@types/node': 25.9.0
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@types/jsonwebtoken@9.0.10':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
'@types/node': 25.9.0
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@types/linkify-it@5.0.0': {}
|
||||
|
||||
@@ -7973,16 +7973,16 @@ snapshots:
|
||||
|
||||
'@types/mute-stream@0.0.4':
|
||||
dependencies:
|
||||
'@types/node': 25.9.0
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@types/node-fetch@2.6.13':
|
||||
dependencies:
|
||||
'@types/node': 25.9.0
|
||||
'@types/node': 25.9.1
|
||||
form-data: 4.0.5
|
||||
|
||||
'@types/node-forge@1.3.14':
|
||||
dependencies:
|
||||
'@types/node': 25.9.0
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@types/node@16.9.1': {}
|
||||
|
||||
@@ -7990,11 +7990,11 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
|
||||
'@types/node@22.19.18':
|
||||
'@types/node@22.19.19':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@25.9.0':
|
||||
'@types/node@25.9.1':
|
||||
dependencies:
|
||||
undici-types: 7.24.6
|
||||
|
||||
@@ -8012,7 +8012,7 @@ snapshots:
|
||||
|
||||
'@types/through2@2.0.41':
|
||||
dependencies:
|
||||
'@types/node': 25.9.0
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
|
||||
@@ -8040,11 +8040,11 @@ snapshots:
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 25.9.0
|
||||
'@types/node': 25.9.1
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
dependencies:
|
||||
'@types/node': 25.9.0
|
||||
'@types/node': 25.9.1
|
||||
optional: true
|
||||
|
||||
'@ungap/structured-clone@1.3.1': {}
|
||||
@@ -8780,7 +8780,7 @@ snapshots:
|
||||
|
||||
happy-dom@20.9.0:
|
||||
dependencies:
|
||||
'@types/node': 25.9.0
|
||||
'@types/node': 25.9.1
|
||||
'@types/whatwg-mimetype': 3.0.2
|
||||
'@types/ws': 8.18.1
|
||||
entities: 7.0.1
|
||||
|
||||
@@ -47,12 +47,6 @@
|
||||
"description": "Use external TLS termination through Onebox or dcrouter.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"key": "SERVEZONE_ADMINACCOUNT",
|
||||
"value": "",
|
||||
"description": "Initial admin account in username:password format. Only used when Cloudly has no human users yet.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"key": "MONGODB_URL",
|
||||
"value": "${MONGODB_URI}",
|
||||
@@ -118,7 +112,7 @@
|
||||
"mongodb": true,
|
||||
"s3": true
|
||||
},
|
||||
"minOneboxVersion": "1.24.2",
|
||||
"minOneboxVersion": "2.2.0",
|
||||
"backupBeforeUpgrade": true,
|
||||
"healthCheck": {
|
||||
"path": "/status",
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/cloudly',
|
||||
version: '6.2.0',
|
||||
version: '6.3.1',
|
||||
description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.'
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import { CloudlyPlatformManager } from './manager.platform/classes.platformmanag
|
||||
import { CloudlyBackupManager } from './manager.backup/classes.backupmanager.js';
|
||||
import { CloudlyBaseOsManager } from './manager.baseos/classes.baseosmanager.js';
|
||||
import { CloudlyAppStoreManager } from './manager.appstore/classes.appstoremanager.js';
|
||||
import { CloudlyHostedAppManager } from './manager.hostedapp/classes.hostedappmanager.js';
|
||||
import { CloudlyJumpManager } from './manager.jump/classes.jumpmanager.js';
|
||||
|
||||
/**
|
||||
@@ -82,6 +83,7 @@ export class Cloudly {
|
||||
public baremetalManager: CloudlyBaremetalManager;
|
||||
public baseOsManager: CloudlyBaseOsManager;
|
||||
public appStoreManager: CloudlyAppStoreManager;
|
||||
public hostedAppManager: CloudlyHostedAppManager;
|
||||
public jumpManager: CloudlyJumpManager;
|
||||
|
||||
private readyDeferred = new plugins.smartpromise.Deferred();
|
||||
@@ -119,6 +121,7 @@ export class Cloudly {
|
||||
this.backupManager = new CloudlyBackupManager(this);
|
||||
this.baseOsManager = new CloudlyBaseOsManager(this);
|
||||
this.secretManager = new CloudlySecretManager(this);
|
||||
this.hostedAppManager = new CloudlyHostedAppManager(this);
|
||||
this.appStoreManager = new CloudlyAppStoreManager(this);
|
||||
this.nodeManager = new CloudlyNodeManager(this);
|
||||
this.baremetalManager = new CloudlyBaremetalManager(this);
|
||||
@@ -151,6 +154,7 @@ export class Cloudly {
|
||||
await this.taskManager.init();
|
||||
await this.backupManager.start();
|
||||
await this.baseOsManager.start();
|
||||
await this.hostedAppManager.start();
|
||||
await this.appStoreManager.start();
|
||||
await this.registryManager.start();
|
||||
await this.domainManager.init();
|
||||
@@ -186,6 +190,7 @@ export class Cloudly {
|
||||
await this.backupManager.stop();
|
||||
await this.baseOsManager.stop();
|
||||
await this.registryManager.stop();
|
||||
await this.hostedAppManager.stop();
|
||||
await this.appStoreManager.stop();
|
||||
await this.externalRegistryManager.stop();
|
||||
}
|
||||
|
||||
@@ -240,6 +240,12 @@ export class CloudlyAppStoreManager {
|
||||
.slice(0, 25);
|
||||
}
|
||||
|
||||
public async startHostedAppUpgrade(serviceIdArg: string, targetVersionArg: string): Promise<IAppStoreUpgradeOperation> {
|
||||
const operation = await this.createUpgradeOperation(serviceIdArg, targetVersionArg);
|
||||
void this.performUpgrade(operation.id).catch(() => {});
|
||||
return operation;
|
||||
}
|
||||
|
||||
public async getAppStoreUpgradePreview(
|
||||
serviceIdArg: string,
|
||||
targetVersionArg?: string,
|
||||
@@ -545,7 +551,11 @@ export class CloudlyAppStoreManager {
|
||||
this.assertRuntimeCompatibility(config);
|
||||
this.assertSupportedPlatformRequirements(config);
|
||||
this.assertSupportedPublishedPorts(publishedPorts);
|
||||
const envVars = this.getAppStoreEnvVars(config, optionsArg.envVars || {});
|
||||
const hostedAppRuntime = this.cloudlyRef.hostedAppManager.createHostedAppRuntimeEnvVars(optionsArg.serviceName);
|
||||
const envVars = {
|
||||
...this.getAppStoreEnvVars(config, optionsArg.envVars || {}),
|
||||
...hostedAppRuntime.envVars,
|
||||
};
|
||||
if (this.requiresTemplateValue(envVars, 'SERVICE_DOMAIN') && !optionsArg.domain) {
|
||||
throw new Error('A domain is required because the app template uses ${SERVICE_DOMAIN}');
|
||||
}
|
||||
@@ -567,6 +577,7 @@ export class CloudlyAppStoreManager {
|
||||
appTemplateId: optionsArg.appId,
|
||||
appTemplateVersion: appStoreVersion,
|
||||
appStoreUpgradePolicy: 'manual',
|
||||
hostedAppLifecycle: hostedAppRuntime.lifecycle,
|
||||
environment: envVars,
|
||||
secretBundleId: secretBundle.id,
|
||||
additionalSecretBundleIds: [],
|
||||
|
||||
@@ -113,19 +113,28 @@ export class CloudlyAuthManager {
|
||||
}
|
||||
|
||||
const adminAccount = this.cloudlyRef.config.data.servezoneAdminaccount;
|
||||
if (!adminAccount) {
|
||||
throw new Error('SERVEZONE_ADMINACCOUNT is required for first-run Cloudly bootstrap');
|
||||
}
|
||||
let username: string;
|
||||
let password: string;
|
||||
let hostedBootstrapActionId: string | undefined;
|
||||
if (adminAccount) {
|
||||
const separatorIndex = adminAccount.indexOf(':');
|
||||
if (separatorIndex <= 0 || separatorIndex === adminAccount.length - 1) {
|
||||
throw new Error('SERVEZONE_ADMINACCOUNT must use username:password format');
|
||||
}
|
||||
|
||||
const separatorIndex = adminAccount.indexOf(':');
|
||||
if (separatorIndex <= 0 || separatorIndex === adminAccount.length - 1) {
|
||||
throw new Error('SERVEZONE_ADMINACCOUNT must use username:password format');
|
||||
}
|
||||
|
||||
const username = adminAccount.slice(0, separatorIndex).trim();
|
||||
const password = adminAccount.slice(separatorIndex + 1);
|
||||
if (!username || !password) {
|
||||
throw new Error('SERVEZONE_ADMINACCOUNT must include a non-empty username and password');
|
||||
username = adminAccount.slice(0, separatorIndex).trim();
|
||||
password = adminAccount.slice(separatorIndex + 1);
|
||||
if (!username || !password) {
|
||||
throw new Error('SERVEZONE_ADMINACCOUNT must include a non-empty username and password');
|
||||
}
|
||||
} else {
|
||||
const hostedBootstrap = await this.cloudlyRef.hostedAppManager.requestParentInitialAdminBootstrap();
|
||||
if (!hostedBootstrap) {
|
||||
throw new Error('SERVEZONE_ADMINACCOUNT is required for first-run Cloudly bootstrap unless hosted app lifecycle credentials are available');
|
||||
}
|
||||
username = hostedBootstrap.username;
|
||||
password = hostedBootstrap.password;
|
||||
hostedBootstrapActionId = hostedBootstrap.actionId;
|
||||
}
|
||||
|
||||
const user = new this.CUser({
|
||||
@@ -139,6 +148,14 @@ export class CloudlyAuthManager {
|
||||
});
|
||||
await user.save();
|
||||
logger.log('success', `created initial admin user ${username}`);
|
||||
if (hostedBootstrapActionId) {
|
||||
await this.cloudlyRef.hostedAppManager.completeParentBootstrapAction(
|
||||
hostedBootstrapActionId,
|
||||
'Cloudly created the initial admin user.',
|
||||
).catch((errorArg) => {
|
||||
logger.log('warn', `failed to complete hosted app bootstrap action: ${(errorArg as Error).message}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async stop() {}
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
import type { Cloudly } from '../classes.cloudly.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { Service } from '../manager.service/classes.service.js';
|
||||
|
||||
type IHostedAppLifecycleState = plugins.servezoneInterfaces.data.IHostedAppLifecycleState;
|
||||
type IHostedAppUpgradeState = plugins.servezoneInterfaces.data.IHostedAppUpgradeState;
|
||||
type IHostedAppRuntimeIdentity = plugins.servezoneInterfaces.data.IHostedAppRuntimeIdentity;
|
||||
|
||||
type TExtendedServiceData = plugins.servezoneInterfaces.data.IService['data'] & {
|
||||
hostedAppLifecycle?: IHostedAppLifecycleState;
|
||||
};
|
||||
|
||||
export class CloudlyHostedAppManager {
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
constructor(private cloudlyRef: Cloudly) {
|
||||
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||
this.registerHandlers();
|
||||
}
|
||||
|
||||
public async start() {}
|
||||
public async stop() {}
|
||||
|
||||
private getParentRuntimeIdentity(): IHostedAppRuntimeIdentity | null {
|
||||
const appInstanceId = process.env.SERVEZONE_APP_INSTANCE_ID;
|
||||
const appControlToken = process.env.SERVEZONE_APP_CONTROL_TOKEN;
|
||||
if (!appInstanceId || !appControlToken) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
appInstanceId,
|
||||
appControlToken,
|
||||
hostType: process.env.SERVEZONE_APP_HOST_TYPE || 'onebox',
|
||||
};
|
||||
}
|
||||
|
||||
private createParentRuntimeTypedRequest<TRequest extends plugins.typedrequestInterfaces.ITypedRequest>(methodArg: TRequest['method']): plugins.typedrequest.TypedRequest<TRequest> | null {
|
||||
const runtimeUrl = process.env.SERVEZONE_RUNTIME_URL;
|
||||
if (!runtimeUrl) {
|
||||
return null;
|
||||
}
|
||||
return new plugins.typedrequest.TypedRequest<TRequest>(
|
||||
`${runtimeUrl.replace(/\/+$/, '')}/typedrequest`,
|
||||
methodArg,
|
||||
);
|
||||
}
|
||||
|
||||
public async requestParentInitialAdminBootstrap(): Promise<{
|
||||
username: string;
|
||||
password: string;
|
||||
actionId: string;
|
||||
} | null> {
|
||||
const identity = this.getParentRuntimeIdentity();
|
||||
const request = this.createParentRuntimeTypedRequest<plugins.servezoneInterfaces.requests.hostedapp.IReq_HostedApp_RequestBootstrapAction>(
|
||||
'hostedAppRequestBootstrapAction',
|
||||
);
|
||||
if (!identity || !request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const username = 'admin';
|
||||
const password = plugins.smartunique.uniSimple('cloudlyadmin', 32);
|
||||
const response = await request.fire({
|
||||
identity,
|
||||
action: {
|
||||
type: 'credentials',
|
||||
label: 'Cloudly initial admin',
|
||||
url: `https://${this.cloudlyRef.config.data.publicUrl}`,
|
||||
username,
|
||||
password,
|
||||
message: 'Use these credentials to sign in to Cloudly, then change the admin password.',
|
||||
},
|
||||
});
|
||||
return {
|
||||
username,
|
||||
password,
|
||||
actionId: response.action.id,
|
||||
};
|
||||
}
|
||||
|
||||
public async completeParentBootstrapAction(actionIdArg?: string, messageArg?: string): Promise<void> {
|
||||
const identity = this.getParentRuntimeIdentity();
|
||||
const request = this.createParentRuntimeTypedRequest<plugins.servezoneInterfaces.requests.hostedapp.IReq_HostedApp_CompleteBootstrapAction>(
|
||||
'hostedAppCompleteBootstrapAction',
|
||||
);
|
||||
if (!identity || !request) {
|
||||
return;
|
||||
}
|
||||
await request.fire({
|
||||
identity,
|
||||
actionId: actionIdArg,
|
||||
message: messageArg,
|
||||
});
|
||||
}
|
||||
|
||||
public createHostedAppRuntimeEnvVars(serviceNameArg: string): {
|
||||
appInstanceId: string;
|
||||
appControlToken: string;
|
||||
envVars: Record<string, string>;
|
||||
lifecycle: IHostedAppLifecycleState;
|
||||
} {
|
||||
const appInstanceId = plugins.smartunique.uniSimple('hostedapp');
|
||||
const appControlToken = plugins.smartunique.uniSimple('hostedapptoken', 64);
|
||||
const runtimeUrl = `https://${this.cloudlyRef.config.data.publicUrl}`;
|
||||
return {
|
||||
appInstanceId,
|
||||
appControlToken,
|
||||
envVars: {
|
||||
SERVEZONE_RUNTIME_URL: runtimeUrl,
|
||||
SERVEZONE_APP_INSTANCE_ID: appInstanceId,
|
||||
SERVEZONE_APP_CONTROL_TOKEN: appControlToken,
|
||||
SERVEZONE_APP_HOST_TYPE: 'cloudly',
|
||||
},
|
||||
lifecycle: {
|
||||
appInstanceId,
|
||||
hostType: 'cloudly',
|
||||
appName: serviceNameArg,
|
||||
runtimeStatus: 'unknown',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async requireHostedAppIdentity(identityArg: IHostedAppRuntimeIdentity): Promise<Service> {
|
||||
const services = await this.cloudlyRef.serviceManager.CService.getInstances({});
|
||||
const service = services.find((serviceArg) => {
|
||||
const serviceData = serviceArg.data as TExtendedServiceData;
|
||||
return (
|
||||
serviceData.hostedAppLifecycle?.appInstanceId === identityArg?.appInstanceId ||
|
||||
serviceData.environment?.SERVEZONE_APP_INSTANCE_ID === identityArg?.appInstanceId
|
||||
);
|
||||
});
|
||||
if (!service) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Hosted app service not found');
|
||||
}
|
||||
const serviceData = service.data as TExtendedServiceData;
|
||||
if (serviceData.environment?.SERVEZONE_APP_CONTROL_TOKEN !== identityArg?.appControlToken) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Hosted app identity is invalid');
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
private async getUpgradeState(serviceArg: Service): Promise<IHostedAppUpgradeState> {
|
||||
const serviceData = serviceArg.data as TExtendedServiceData;
|
||||
const latestOperation = this.cloudlyRef.appStoreManager
|
||||
.getUpgradeOperations()
|
||||
.find((operationArg) => operationArg.serviceId === serviceArg.id);
|
||||
if (latestOperation) {
|
||||
return {
|
||||
status: latestOperation.status === 'running' ? 'running' : latestOperation.status,
|
||||
appTemplateId: latestOperation.appTemplateId,
|
||||
currentVersion: latestOperation.fromVersion,
|
||||
targetVersion: latestOperation.targetVersion,
|
||||
operationId: latestOperation.id,
|
||||
warnings: latestOperation.warnings,
|
||||
error: latestOperation.error,
|
||||
startedAt: latestOperation.startedAt,
|
||||
updatedAt: latestOperation.updatedAt,
|
||||
completedAt: latestOperation.completedAt,
|
||||
};
|
||||
}
|
||||
|
||||
if (!serviceData.appTemplateId || !serviceData.appTemplateVersion) {
|
||||
return { status: 'unknown' };
|
||||
}
|
||||
|
||||
const upgradeableServices = await this.cloudlyRef.appStoreManager.getUpgradeableAppStoreServices();
|
||||
const upgradeable = upgradeableServices.find((serviceArg2) => serviceArg2.serviceId === serviceArg.id);
|
||||
if (!upgradeable) {
|
||||
return {
|
||||
status: 'upToDate',
|
||||
appTemplateId: serviceData.appTemplateId,
|
||||
currentVersion: serviceData.appTemplateVersion,
|
||||
latestVersion: serviceData.appTemplateVersion,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'available',
|
||||
appTemplateId: upgradeable.appTemplateId,
|
||||
currentVersion: upgradeable.currentVersion,
|
||||
latestVersion: upgradeable.latestVersion,
|
||||
targetVersion: upgradeable.latestVersion,
|
||||
};
|
||||
}
|
||||
|
||||
private async getLifecycleState(serviceArg: Service): Promise<IHostedAppLifecycleState> {
|
||||
const serviceData = serviceArg.data as TExtendedServiceData;
|
||||
const appInstanceId = serviceData.hostedAppLifecycle?.appInstanceId || serviceData.environment?.SERVEZONE_APP_INSTANCE_ID;
|
||||
const state: IHostedAppLifecycleState = {
|
||||
...(serviceData.hostedAppLifecycle || ({} as IHostedAppLifecycleState)),
|
||||
appInstanceId: appInstanceId || '',
|
||||
hostType: 'cloudly',
|
||||
appName: serviceData.hostedAppLifecycle?.appName || serviceData.name,
|
||||
publicUrl: serviceData.hostedAppLifecycle?.publicUrl || (serviceData.domains?.[0]?.name ? `https://${serviceData.domains[0].name}` : undefined),
|
||||
upgradeState: await this.getUpgradeState(serviceArg),
|
||||
};
|
||||
serviceData.hostedAppLifecycle = state;
|
||||
serviceArg.data = serviceData;
|
||||
await serviceArg.save();
|
||||
return state;
|
||||
}
|
||||
|
||||
private async updateLifecycleState(serviceArg: Service, stateArg: IHostedAppLifecycleState): Promise<IHostedAppLifecycleState> {
|
||||
const serviceData = serviceArg.data as TExtendedServiceData;
|
||||
serviceData.hostedAppLifecycle = stateArg;
|
||||
serviceArg.data = serviceData;
|
||||
await serviceArg.save();
|
||||
return await this.getLifecycleState(serviceArg);
|
||||
}
|
||||
|
||||
private registerHandlers() {
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.hostedapp.IReq_HostedApp_ReportLifecycleState>(
|
||||
'hostedAppReportLifecycleState',
|
||||
async (dataArg) => {
|
||||
const service = await this.requireHostedAppIdentity(dataArg.identity);
|
||||
const existingState = await this.getLifecycleState(service);
|
||||
const state = await this.updateLifecycleState(service, {
|
||||
...existingState,
|
||||
...dataArg.report,
|
||||
appInstanceId: existingState.appInstanceId,
|
||||
hostType: 'cloudly',
|
||||
reportedAt: Date.now(),
|
||||
});
|
||||
return { state };
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.hostedapp.IReq_HostedApp_GetLifecycleState>(
|
||||
'hostedAppGetLifecycleState',
|
||||
async (dataArg) => {
|
||||
const service = await this.requireHostedAppIdentity(dataArg.identity);
|
||||
return { state: await this.getLifecycleState(service) };
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.hostedapp.IReq_HostedApp_RequestBootstrapAction>(
|
||||
'hostedAppRequestBootstrapAction',
|
||||
async (dataArg) => {
|
||||
const service = await this.requireHostedAppIdentity(dataArg.identity);
|
||||
const existingState = await this.getLifecycleState(service);
|
||||
const now = Date.now();
|
||||
const action = {
|
||||
...dataArg.action,
|
||||
id: dataArg.action.id || plugins.smartunique.shortId(12),
|
||||
status: 'ready' as const,
|
||||
label: dataArg.action.label || 'Initial setup',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
const state = await this.updateLifecycleState(service, {
|
||||
...existingState,
|
||||
runtimeStatus: 'setupRequired',
|
||||
bootstrapAction: action,
|
||||
reportedAt: now,
|
||||
});
|
||||
return { action, state };
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.hostedapp.IReq_HostedApp_CompleteBootstrapAction>(
|
||||
'hostedAppCompleteBootstrapAction',
|
||||
async (dataArg) => {
|
||||
const service = await this.requireHostedAppIdentity(dataArg.identity);
|
||||
const existingState = await this.getLifecycleState(service);
|
||||
const now = Date.now();
|
||||
const bootstrapAction = existingState.bootstrapAction
|
||||
? {
|
||||
...existingState.bootstrapAction,
|
||||
id: dataArg.actionId || existingState.bootstrapAction.id,
|
||||
status: 'completed' as const,
|
||||
message: dataArg.message || existingState.bootstrapAction.message,
|
||||
updatedAt: now,
|
||||
completedAt: now,
|
||||
}
|
||||
: undefined;
|
||||
const state = await this.updateLifecycleState(service, {
|
||||
...existingState,
|
||||
runtimeStatus: existingState.runtimeStatus === 'setupRequired' ? 'running' : existingState.runtimeStatus,
|
||||
bootstrapAction,
|
||||
reportedAt: now,
|
||||
});
|
||||
return { state };
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.hostedapp.IReq_HostedApp_StartManagedUpgrade>(
|
||||
'hostedAppStartManagedUpgrade',
|
||||
async (dataArg) => {
|
||||
const service = await this.requireHostedAppIdentity(dataArg.identity);
|
||||
const upgradeState = await this.getUpgradeState(service);
|
||||
const targetVersion = dataArg.targetVersion || upgradeState.targetVersion || upgradeState.latestVersion;
|
||||
if (!targetVersion) {
|
||||
throw new plugins.typedrequest.TypedResponseError('No managed upgrade target is available');
|
||||
}
|
||||
const operation = await this.cloudlyRef.appStoreManager.startHostedAppUpgrade(service.id, targetVersion);
|
||||
const nextUpgradeState: IHostedAppUpgradeState = {
|
||||
status: 'running',
|
||||
appTemplateId: operation.appTemplateId,
|
||||
currentVersion: operation.fromVersion,
|
||||
targetVersion: operation.targetVersion,
|
||||
operationId: operation.id,
|
||||
warnings: operation.warnings,
|
||||
startedAt: operation.startedAt,
|
||||
updatedAt: operation.updatedAt,
|
||||
};
|
||||
const existingState = await this.getLifecycleState(service);
|
||||
const state = await this.updateLifecycleState(service, {
|
||||
...existingState,
|
||||
upgradeState: nextUpgradeState,
|
||||
reportedAt: Date.now(),
|
||||
});
|
||||
return { upgradeState: nextUpgradeState, state };
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.hostedapp.IReq_HostedApp_GetManagedUpgradeStatus>(
|
||||
'hostedAppGetManagedUpgradeStatus',
|
||||
async (dataArg) => {
|
||||
const service = await this.requireHostedAppIdentity(dataArg.identity);
|
||||
return { upgradeState: await this.getUpgradeState(service) };
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -8,9 +8,10 @@ export { path, crypto, stream, fsPromises };
|
||||
|
||||
// @apiglobal scope
|
||||
import * as typedrequest from '@api.global/typedrequest';
|
||||
import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
|
||||
import * as typedsocket from '@api.global/typedsocket';
|
||||
|
||||
export { typedrequest, typedsocket };
|
||||
export { typedrequest, typedrequestInterfaces, typedsocket };
|
||||
|
||||
// @apiclient.xyz scope
|
||||
import * as cloudflare from '@apiclient.xyz/cloudflare';
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/cloudly',
|
||||
version: '6.2.0',
|
||||
version: '6.3.1',
|
||||
description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.'
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ export class CloudlyViewImages extends DeesElement {
|
||||
.detail-title { margin: 0; font-size: 26px; font-weight: 700; color: var(--ci-shade-7, #e4e4e7); }
|
||||
.detail-subtitle { margin-top: 6px; color: var(--ci-shade-4, #71717a); font-size: 14px; overflow-wrap: anywhere; }
|
||||
.back-button { border: 1px solid var(--ci-shade-2, #27272a); border-radius: 7px; padding: 9px 13px; font-size: 13px; cursor: pointer; background: var(--ci-shade-1, #09090b); color: var(--ci-shade-7, #e4e4e7); }
|
||||
.summary-card, .detail-card { background: var(--ci-shade-1, #09090b); border: 1px solid var(--ci-shade-2, #27272a); border-radius: 9px; padding: 16px; }
|
||||
.spaced-card { margin-top: 14px; }
|
||||
.detail-card { background: var(--ci-shade-1, #09090b); border: 1px solid var(--ci-shade-2, #27272a); border-radius: 9px; padding: 16px; }
|
||||
.spaced-table, .spaced-card { margin-top: 14px; }
|
||||
.details-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-top: 14px; }
|
||||
.section-title { font-size: 14px; font-weight: 700; color: var(--ci-shade-7, #e4e4e7); margin-bottom: 10px; }
|
||||
.kv-list { display: grid; gap: 8px; }
|
||||
@@ -172,43 +172,43 @@ export class CloudlyViewImages extends DeesElement {
|
||||
|
||||
<dees-statsgrid .tiles=${this.getImageStatsTiles(image)} .minTileWidth=${220} .gap=${12}></dees-statsgrid>
|
||||
|
||||
<div class="detail-card">
|
||||
<div class="section-title">Versions</div>
|
||||
<dees-table
|
||||
.heading1=${'Image Versions'}
|
||||
.heading2=${versions.length ? 'Stored image versions and registry metadata' : 'No versions recorded'}
|
||||
.data=${versions}
|
||||
.displayFunction=${(versionArg: plugins.interfaces.data.IImage['data']['versions'][number]) => ({
|
||||
Version: versionArg.versionString,
|
||||
Source: this.renderSourceBadge(versionArg.source),
|
||||
Size: this.formatBytes(versionArg.size),
|
||||
Digest: versionArg.digest || '-',
|
||||
Repository: versionArg.registryRepository || '-',
|
||||
Tag: versionArg.registryTag || '-',
|
||||
Storage: versionArg.storagePath || '-',
|
||||
Created: this.formatDate(versionArg.createdAt),
|
||||
})}
|
||||
></dees-table>
|
||||
|
||||
${servicesUsingImage.length ? html`
|
||||
<dees-table
|
||||
.heading1=${'Image Versions'}
|
||||
.heading2=${versions.length ? 'Stored image versions and registry metadata' : 'No versions recorded'}
|
||||
.data=${versions}
|
||||
.displayFunction=${(versionArg: plugins.interfaces.data.IImage['data']['versions'][number]) => ({
|
||||
Version: versionArg.versionString,
|
||||
Source: this.renderSourceBadge(versionArg.source),
|
||||
Size: this.formatBytes(versionArg.size),
|
||||
Digest: versionArg.digest || '-',
|
||||
Repository: versionArg.registryRepository || '-',
|
||||
Tag: versionArg.registryTag || '-',
|
||||
Storage: versionArg.storagePath || '-',
|
||||
Created: this.formatDate(versionArg.createdAt),
|
||||
class="spaced-table"
|
||||
.heading1=${'Service Usage'}
|
||||
.heading2=${'Services currently configured with this image ID'}
|
||||
.data=${servicesUsingImage}
|
||||
.displayFunction=${(serviceArg: plugins.interfaces.data.IService) => ({
|
||||
Name: serviceArg.data.name,
|
||||
Version: serviceArg.data.imageVersion || '-',
|
||||
Category: serviceArg.data.serviceCategory || 'workload',
|
||||
Strategy: serviceArg.data.deploymentStrategy || 'custom',
|
||||
Domains: serviceArg.data.domains?.map((domainArg) => domainArg.name).join(', ') || '-',
|
||||
Deployments: serviceArg.data.deploymentIds?.length || 0,
|
||||
})}
|
||||
></dees-table>
|
||||
</div>
|
||||
|
||||
<div class="detail-card spaced-card">
|
||||
<div class="section-title">Services Using This Image</div>
|
||||
${servicesUsingImage.length ? html`
|
||||
<dees-table
|
||||
.heading1=${'Service Usage'}
|
||||
.heading2=${'Services currently configured with this image ID'}
|
||||
.data=${servicesUsingImage}
|
||||
.displayFunction=${(serviceArg: plugins.interfaces.data.IService) => ({
|
||||
Name: serviceArg.data.name,
|
||||
Version: serviceArg.data.imageVersion || '-',
|
||||
Category: serviceArg.data.serviceCategory || 'workload',
|
||||
Strategy: serviceArg.data.deploymentStrategy || 'custom',
|
||||
Domains: serviceArg.data.domains?.map((domainArg) => domainArg.name).join(', ') || '-',
|
||||
Deployments: serviceArg.data.deploymentIds?.length || 0,
|
||||
})}
|
||||
></dees-table>
|
||||
` : html`<div class="empty-state">No services currently reference this image.</div>`}
|
||||
</div>
|
||||
` : html`
|
||||
<div class="detail-card spaced-card">
|
||||
<div class="section-title">Services Using This Image</div>
|
||||
<div class="empty-state">No services currently reference this image.</div>
|
||||
</div>
|
||||
`}
|
||||
|
||||
<div class="details-grid">
|
||||
<div class="detail-card">
|
||||
|
||||
@@ -368,66 +368,65 @@ export class CloudlyViewServices extends DeesElement {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-card">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px;">
|
||||
<div>
|
||||
<div class="section-title">Deployments</div>
|
||||
<div class="detail-subtitle">Container-level runtime actions happen here.</div>
|
||||
</div>
|
||||
<button class="back-button" @click=${() => this.loadDeploymentsForService(service)}>Refresh</button>
|
||||
</div>
|
||||
${this.deploymentsLoading ? html`<div class="detail-subtitle">Loading deployments...</div>` : html`
|
||||
<dees-table
|
||||
.heading1=${'Live Deployments'}
|
||||
.heading2=${this.serviceDeployments.length ? 'Docker Swarm tasks reported by connected Coreflows' : 'No live deployments reported'}
|
||||
.data=${this.serviceDeployments}
|
||||
.displayFunction=${(deploymentArg: any) => ({
|
||||
Status: this.renderStatusBadge(deploymentArg.status),
|
||||
Node: deploymentArg.nodeName || deploymentArg.nodeId || '-',
|
||||
Slot: deploymentArg.slot || '-',
|
||||
Version: deploymentArg.version || service.data.imageVersion,
|
||||
Container: deploymentArg.containerId ? deploymentArg.containerId.slice(0, 12) : '-',
|
||||
CPU: deploymentArg.resourceUsage ? `${deploymentArg.resourceUsage.cpuUsagePercent.toFixed(1)}%` : '-',
|
||||
Memory: deploymentArg.resourceUsage ? `${deploymentArg.resourceUsage.memoryUsedMB} MB` : '-',
|
||||
Updated: deploymentArg.updatedAt ? new Date(deploymentArg.updatedAt).toLocaleString() : '-',
|
||||
})}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'Details',
|
||||
iconName: 'lucide:Eye',
|
||||
type: ['contextmenu', 'inRow', 'doubleClick'],
|
||||
actionFunc: async (actionDataArg: any) => {
|
||||
await this.showDeploymentDetailsModal(actionDataArg.item);
|
||||
},
|
||||
${this.deploymentsLoading ? html`<div class="detail-subtitle">Loading deployments...</div>` : html`
|
||||
<dees-table
|
||||
.heading1=${'Live Deployments'}
|
||||
.heading2=${this.serviceDeployments.length ? 'Docker Swarm tasks reported by connected Coreflows' : 'No live deployments reported'}
|
||||
.data=${this.serviceDeployments}
|
||||
.displayFunction=${(deploymentArg: any) => ({
|
||||
Status: this.renderStatusBadge(deploymentArg.status),
|
||||
Node: deploymentArg.nodeName || deploymentArg.nodeId || '-',
|
||||
Slot: deploymentArg.slot || '-',
|
||||
Version: deploymentArg.version || service.data.imageVersion,
|
||||
Container: deploymentArg.containerId ? deploymentArg.containerId.slice(0, 12) : '-',
|
||||
CPU: deploymentArg.resourceUsage ? `${deploymentArg.resourceUsage.cpuUsagePercent.toFixed(1)}%` : '-',
|
||||
Memory: deploymentArg.resourceUsage ? `${deploymentArg.resourceUsage.memoryUsedMB} MB` : '-',
|
||||
Updated: deploymentArg.updatedAt ? new Date(deploymentArg.updatedAt).toLocaleString() : '-',
|
||||
})}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'Refresh',
|
||||
iconName: 'refresh-cw',
|
||||
type: ['header'],
|
||||
actionFunc: async () => {
|
||||
await this.loadDeploymentsForService(service);
|
||||
},
|
||||
{
|
||||
name: 'Open IDE',
|
||||
iconName: 'terminal',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg: any) => {
|
||||
await this.openDeploymentWorkspace(actionDataArg.item);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Details',
|
||||
iconName: 'lucide:Eye',
|
||||
type: ['contextmenu', 'inRow', 'doubleClick'],
|
||||
actionFunc: async (actionDataArg: any) => {
|
||||
await this.showDeploymentDetailsModal(actionDataArg.item);
|
||||
},
|
||||
{
|
||||
name: 'Restart',
|
||||
iconName: 'refresh-cw',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg: any) => {
|
||||
await this.restartDeployment(actionDataArg.item);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Open IDE',
|
||||
iconName: 'terminal',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg: any) => {
|
||||
await this.openDeploymentWorkspace(actionDataArg.item);
|
||||
},
|
||||
{
|
||||
name: 'Kill Container',
|
||||
iconName: 'skull',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg: any) => {
|
||||
await this.confirmKillDeployment(actionDataArg.item);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Restart',
|
||||
iconName: 'refresh-cw',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg: any) => {
|
||||
await this.restartDeployment(actionDataArg.item);
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`}
|
||||
</div>
|
||||
},
|
||||
{
|
||||
name: 'Kill Container',
|
||||
iconName: 'skull',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg: any) => {
|
||||
await this.confirmKillDeployment(actionDataArg.item);
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`}
|
||||
|
||||
<div class="details-grid">
|
||||
<div class="detail-card">
|
||||
|
||||
@@ -266,32 +266,30 @@ export class CloudlyViewTasks extends DeesElement {
|
||||
|
||||
<cloudly-sectionheading>Execution History</cloudly-sectionheading>
|
||||
|
||||
<dees-panel .title=${'Recent Executions'} .subtitle=${'History of task runs and their outcomes'} .variant=${'outline'}>
|
||||
<dees-table
|
||||
.heading1=${'Task Executions'}
|
||||
.heading2=${'History of task runs and their outcomes'}
|
||||
.data=${this.data.taskExecutions || []}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ITaskExecution) => {
|
||||
return {
|
||||
Task: itemArg.data.taskName,
|
||||
Status: html`<span class="status-badge status-${itemArg.data.status}">${itemArg.data.status}</span>`,
|
||||
'Started At': formatDate(itemArg.data.startedAt),
|
||||
Duration: itemArg.data.duration ? formatDuration(itemArg.data.duration) : '-',
|
||||
'Triggered By': itemArg.data.triggeredBy,
|
||||
Logs: itemArg.data.logs?.length || 0,
|
||||
} as any;
|
||||
}}
|
||||
.actionFunction=${async (itemArg: plugins.interfaces.data.ITaskExecution) => {
|
||||
const actions: any[] = [
|
||||
{ name: 'View Details', iconName: 'lucide:Eye', type: ['inRow'], actionFunc: async () => { this.selectedExecution = itemArg; } }
|
||||
];
|
||||
if (itemArg.data.status === 'running') {
|
||||
actions.push({ name: 'Cancel', iconName: 'lucide:SquareX', type: ['inRow'], actionFunc: async () => { await appstate.dataState.dispatchAction(appstate.taskActions.cancelTask, { executionId: itemArg.id }); await this.loadExecutionsWithFilter(); } });
|
||||
}
|
||||
return actions;
|
||||
}}
|
||||
></dees-table>
|
||||
</dees-panel>
|
||||
<dees-table
|
||||
.heading1=${'Task Executions'}
|
||||
.heading2=${'History of task runs and their outcomes'}
|
||||
.data=${this.data.taskExecutions || []}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ITaskExecution) => {
|
||||
return {
|
||||
Task: itemArg.data.taskName,
|
||||
Status: html`<span class="status-badge status-${itemArg.data.status}">${itemArg.data.status}</span>`,
|
||||
'Started At': formatDate(itemArg.data.startedAt),
|
||||
Duration: itemArg.data.duration ? formatDuration(itemArg.data.duration) : '-',
|
||||
'Triggered By': itemArg.data.triggeredBy,
|
||||
Logs: itemArg.data.logs?.length || 0,
|
||||
} as any;
|
||||
}}
|
||||
.actionFunction=${async (itemArg: plugins.interfaces.data.ITaskExecution) => {
|
||||
const actions: any[] = [
|
||||
{ name: 'View Details', iconName: 'lucide:Eye', type: ['inRow'], actionFunc: async () => { this.selectedExecution = itemArg; } }
|
||||
];
|
||||
if (itemArg.data.status === 'running') {
|
||||
actions.push({ name: 'Cancel', iconName: 'lucide:SquareX', type: ['inRow'], actionFunc: async () => { await appstate.dataState.dispatchAction(appstate.taskActions.cancelTask, { executionId: itemArg.id }); await this.loadExecutionsWithFilter(); } });
|
||||
}
|
||||
return actions;
|
||||
}}
|
||||
></dees-table>
|
||||
|
||||
${this.selectedExecution ? html`
|
||||
<cloudly-sectionheading>Execution Details</cloudly-sectionheading>
|
||||
|
||||
Reference in New Issue
Block a user