diff --git a/src/frontend/src/lib/api/mission-control.api.ts b/src/frontend/src/lib/api/mission-control.api.ts index eb61a248be..47171ab7a0 100644 --- a/src/frontend/src/lib/api/mission-control.api.ts +++ b/src/frontend/src/lib/api/mission-control.api.ts @@ -140,6 +140,21 @@ export const setSatelliteMetadata = async ({ return set_satellite_metadata(satelliteId, metadata); }; +export const setUfoMetadata = async ({ + missionControlId, + ufoId, + metadata, + identity +}: { + missionControlId: MissionControlId; + ufoId: UfoId; + metadata: Metadata; + identity: NullishIdentity; +}): Promise => { + const { set_ufo_metadata } = await getMissionControlActor({ missionControlId, identity }); + return set_ufo_metadata(ufoId, metadata); +}; + export const setOrbitersController = async ({ missionControlId, orbiterIds, diff --git a/src/frontend/src/lib/components/satellites/overview/SatelliteEditDetails.svelte b/src/frontend/src/lib/components/modules/segments/SegmentWithMetadataEditDetails.svelte similarity index 66% rename from src/frontend/src/lib/components/satellites/overview/SatelliteEditDetails.svelte rename to src/frontend/src/lib/components/modules/segments/SegmentWithMetadataEditDetails.svelte index 60d70838fb..b4bc28573a 100644 --- a/src/frontend/src/lib/components/satellites/overview/SatelliteEditDetails.svelte +++ b/src/frontend/src/lib/components/modules/segments/SegmentWithMetadataEditDetails.svelte @@ -6,12 +6,12 @@ import { isBusy } from '$lib/derived/app/busy.derived'; import { authIdentity } from '$lib/derived/auth.derived'; import { missionControlId } from '$lib/derived/console/account.mission-control.derived'; - import { setSatelliteMetadata } from '$lib/services/metadata.services'; + import type { SetMetadataParams, SetMetadataResult } from '$lib/services/metadata.services'; import { busy } from '$lib/stores/app/busy.store'; import { i18n } from '$lib/stores/app/i18n.store'; import { toasts } from '$lib/stores/app/toasts.store'; import type { MetadataUiTags } from '$lib/types/metadata'; - import type { Satellite } from '$lib/types/satellite'; + import type { SegmentWithMetadata } from '$lib/types/segment'; import { metadataUiEnvironment, metadataUiName, @@ -19,28 +19,29 @@ } from '$lib/utils/metadata-ui.utils'; interface Props { - satellite: Satellite; + segment: SegmentWithMetadata; + updateMetadata: (params: SetMetadataParams) => Promise; } - let { satellite }: Props = $props(); + let { segment, updateMetadata }: Props = $props(); // svelte-ignore state_referenced_locally - let satName = $state(metadataUiName(satellite)); + let segmentName = $state(metadataUiName(segment)); // svelte-ignore state_referenced_locally - let satEnv = $state(metadataUiEnvironment(satellite)); + let segmentEnv = $state(metadataUiEnvironment(segment)); // svelte-ignore state_referenced_locally - let satTagsInput = $state(metadataUiTags(satellite)?.join(',') ?? ''); - let satTags = $derived( - satTagsInput + let segmentTagsInput = $state(metadataUiTags(segment)?.join(',') ?? ''); + let segmentTags = $derived( + segmentTagsInput .split(/[\n,]+/) .map((input) => input.toLowerCase().trim()) .filter(notEmptyString) ); - let visible: boolean = $state(false); + let visible = $state(false); - let validConfirm = $derived(nonNullish(satName) && satName !== ''); + let validConfirm = $derived(nonNullish(segmentName) && segmentName !== ''); const handleSubmit = async ($event: SubmitEvent) => { $event.preventDefault(); @@ -48,21 +49,20 @@ if (!validConfirm) { // Submit is disabled if not valid toasts.error({ - text: $i18n.errors.satellite_name_missing + text: $i18n.errors.segment_name_missing }); return; } busy.start(); - const { success } = await setSatelliteMetadata({ + const { success } = await updateMetadata({ missionControlId: $missionControlId, identity: $authIdentity, - satellite, metadata: { - name: satName, - environment: satEnv, - tags: satTags + name: segmentName, + environment: segmentEnv, + tags: segmentTags } }); @@ -80,33 +80,33 @@ }; - +
- + {#snippet label()} - {$i18n.satellites.satellite_name} + {$i18n.core.name} {/snippet} - + {#snippet label()} {$i18n.core.environment} {/snippet} - @@ -114,12 +114,12 @@ - + {#snippet label()} {$i18n.core.tags} {/snippet} - diff --git a/src/frontend/src/lib/components/satellites/overview/SatelliteOverviewActions.svelte b/src/frontend/src/lib/components/satellites/overview/SatelliteOverviewActions.svelte index a1624131de..77532acb7d 100644 --- a/src/frontend/src/lib/components/satellites/overview/SatelliteOverviewActions.svelte +++ b/src/frontend/src/lib/components/satellites/overview/SatelliteOverviewActions.svelte @@ -1,9 +1,14 @@ @@ -23,7 +34,7 @@ {/snippet} {#snippet moreActions()} - + -
-
- {$i18n.satellites.overview} +
+ {$i18n.satellites.overview} -
-
- +
+
+ - + - -
+ +
-
- - {#snippet label()} - {$i18n.ufo.id} - {/snippet} - - +
+ + {#snippet label()} + {$i18n.ufo.id} + {/snippet} + + - -
+
- -
TODO
+ +
{$i18n.monitoring.runtime} diff --git a/src/frontend/src/lib/components/ufos/overview/UfoOverviewActions.svelte b/src/frontend/src/lib/components/ufos/overview/UfoOverviewActions.svelte new file mode 100644 index 0000000000..b836e45c98 --- /dev/null +++ b/src/frontend/src/lib/components/ufos/overview/UfoOverviewActions.svelte @@ -0,0 +1,32 @@ + + + + {#snippet moreActions()} + + {/snippet} + diff --git a/src/frontend/src/lib/i18n/en.json b/src/frontend/src/lib/i18n/en.json index 4f24c083b2..64c1925d7f 100644 --- a/src/frontend/src/lib/i18n/en.json +++ b/src/frontend/src/lib/i18n/en.json @@ -124,7 +124,9 @@ "lets_go": "Let's go!", "name": "Name", "environment": "Environment", - "tags": "Tags" + "tags": "Tags", + "edit_details": "Edit details", + "tags_placeholder": "Comma-separated or one per line. Leave empty for no tags." }, "canisters": { "top_up": "Top-up", @@ -336,9 +338,7 @@ "application": "Application", "application_description": "Data storage, user management, serverless functions", "application_hint": "Your satellite will be initialized with settings optimized for an application.", - "tags_placeholder": "Comma-separated or one per line. Leave empty for no tags.", "enter_name": "Enter a name for your Satellite", - "edit_details": "Edit details", "create_satellite_price": "Starting a new Satellite requires {0}.", "loading_satellites": "Loading your Satellites", "overview": "Overview", @@ -369,7 +369,8 @@ "ready": "Your Mission Control is ready!", "attaching": "Sharing existing modules with Mission Control...", "warn_attaching": "Failed to share the following modules with Mission Control:", - "warn_satellite_metadata_update": "Satellite not found in Mission Control. This is unexpected." + "warn_satellite_metadata_update": "Satellite not found in Mission Control. This is unexpected.", + "warn_ufo_metadata_update": "UFO not found in Mission Control. This is unexpected." }, "wallet": { "title": "Wallet", @@ -617,13 +618,14 @@ "cli_missing_params": "Missing URL parameters. Either the redirection URL or principal is not provided.", "cli_missing_selection": "No Mission Control or Satellite(s) selected.", "cli_unexpected_error": "Unexpected error(s) while adding your admin access key.", - "satellite_name_missing": "A name for the Satellite must be provided.", + "segment_name_missing": "A name must be provided.", "satellite_kind": "Please choose what you are building to continue.", "satellite_unexpected_error": "Unexpected error(s) while creating the Satellite.", "satellite_no_found": "Nothing here. Return to your launchpad to find your Satellites.", "satellite_metadata_update": "Unexpected error(s) while trying to set the metadata of your Satellite.", "satellite_missing_name": "A name must be provided.", "satellites_not_loaded": "The Satellites data are not yet loaded.", + "ufo_metadata_update": "Unexpected error(s) while trying to set the metadata of your UFO.", "create_ufo_name_missing": "A name for the UFO must be provided.", "create_ufo_unexpected_error": "Unexpected error(s) while creating the UFO.", "ufo_not_found": "Nothing here. Return to your launchpad to find your UFOs.", @@ -712,6 +714,7 @@ "invalid_email": "Please enter a valid email address.", "invalid_destination": "Please enter a valid destination address.", "invalid_metadata": "The metadata provided is invalid.", + "update_metadata_error": "Unexpected error(s) while updating the metadata in the Console.", "empty_amount": "Please enter an amount for the transfer.", "cycles_transfer_not_supported": "Transferring cycles from Mission Control is not yet supported. Get in touch!", "convert_icp_to_cycles_not_supported": "Converting ICP to Cycles with the Mission Control is not yet supported. Get in touch!", diff --git a/src/frontend/src/lib/i18n/zh-cn.json b/src/frontend/src/lib/i18n/zh-cn.json index dc87e98193..0242568979 100644 --- a/src/frontend/src/lib/i18n/zh-cn.json +++ b/src/frontend/src/lib/i18n/zh-cn.json @@ -125,7 +125,9 @@ "lets_go": "出发吧!", "name": "名称", "environment": "运行环境", - "tags": "标签" + "tags": "标签", + "edit_details": "编辑详情", + "tags_placeholder": "逗号分隔或每行一个,留空则不添加标签。" }, "canisters": { "top_up": "充值", @@ -337,9 +339,7 @@ "application": "应用程序", "application_description": "数据存储、用户管理、无服务器函数", "application_hint": "您的 Satellite 将使用针对应用程序优化的设置进行初始化。", - "tags_placeholder": "逗号分隔或每行一个,留空则不添加标签。", "enter_name": "输入卫星名称", - "edit_details": "编辑详情", "create_satellite_price": "启动新的 Satellite 需要 {0}。", "loading_satellites": "正在加载所有卫星", "overview": "概览", @@ -370,7 +370,8 @@ "ready": "您的 Mission Control 已就绪!", "attaching": "正在与任务控制中心共享现有模块...", "warn_attaching": "无法与任务控制中心共享以下模块:", - "warn_satellite_metadata_update": "在任务控制中心未找到 Satellite。这是意外情况。" + "warn_satellite_metadata_update": "在任务控制中心未找到 Satellite。这是意外情况。", + "warn_ufo_metadata_update": "在任务控制中未找到 UFO,这是意外情况。" }, "wallet": { "title": "钱包", @@ -618,13 +619,14 @@ "cli_missing_params": "缺少URL参数,未提供重定向URL或主体标识。", "cli_missing_selection": "未选择控制中心或卫星", "cli_unexpected_error": "添加管理员密钥时发生意外错误。", - "satellite_name_missing": "必须提供卫星名称。", + "segment_name_missing": "必须提供名称。", "satellite_kind": "请选择构建类型以继续。", "satellite_unexpected_error": "创建卫星时发生意外错误。", "satellite_no_found": "未找到卫星,请返回启动台。", "satellite_metadata_update": "更新卫星元数据时发生意外错误。", "satellite_missing_name": "必须提供名称。", "satellites_not_loaded": "卫星数据未加载。", + "ufo_metadata_update": "尝试设置 UFO 元数据时发生意外错误。", "create_ufo_name_missing": "必须提供 UFO 的名称。", "create_ufo_unexpected_error": "创建 UFO 时发生意外错误。", "ufo_not_found": "这里什么都没有。返回发射台查找您的 UFO。", @@ -714,6 +716,7 @@ "invalid_email": "请输入有效的邮箱地址。", "invalid_destination": "请输入有效的目标地址。", "invalid_metadata": "提供的元数据无效。", + "update_metadata_error": "在控制台更新元数据时发生意外错误。", "empty_amount": "请输入转账金额。", "cycles_transfer_not_supported": "暂不支持从 Mission Control 转移 Cycles。请联系我们!", "convert_icp_to_cycles_not_supported": "尚不支持通过 Mission Control 将 ICP 兑换为 Cycles。请联系我们!", diff --git a/src/frontend/src/lib/services/factory/factory.create.services.ts b/src/frontend/src/lib/services/factory/factory.create.services.ts index b1defd5700..77ec436a0a 100644 --- a/src/frontend/src/lib/services/factory/factory.create.services.ts +++ b/src/frontend/src/lib/services/factory/factory.create.services.ts @@ -319,7 +319,7 @@ export const createSatelliteWizard = async ({ }): Promise => { if (isEmptyString(satelliteName)) { toasts.error({ - text: get(i18n).errors.satellite_name_missing + text: get(i18n).errors.segment_name_missing }); return { success: 'error' }; } diff --git a/src/frontend/src/lib/services/metadata.services.ts b/src/frontend/src/lib/services/metadata.services.ts index 27a8e2bd06..ce6f62f59b 100644 --- a/src/frontend/src/lib/services/metadata.services.ts +++ b/src/frontend/src/lib/services/metadata.services.ts @@ -1,7 +1,10 @@ import type { MissionControlDid } from '$declarations'; import type { SegmentKey } from '$declarations/console/console.did'; import { setSegmentMetadata } from '$lib/api/console.api'; -import { setSatelliteMetadata as setSatelliteMetadataApi } from '$lib/api/mission-control.api'; +import { + setSatelliteMetadata as setSatelliteMetadataApi, + setUfoMetadata as setUfoMetadataApi +} from '$lib/api/mission-control.api'; import { METADATA_KEY_ENVIRONMENT, METADATA_KEY_NAME, @@ -9,34 +12,100 @@ import { } from '$lib/constants/metadata.constants'; import { segments } from '$lib/derived/console/segments.derived'; import { mctrlSatellites } from '$lib/derived/mission-control/mission-control-satellites.derived'; +import { mctrlUfos } from '$lib/derived/mission-control/mission-control-ufos.derived'; import { MetadataSerializer, MetadataUiSchema } from '$lib/schemas/metadata.schema'; import { i18n } from '$lib/stores/app/i18n.store'; import { toasts } from '$lib/stores/app/toasts.store'; import { segmentsUncertifiedStore } from '$lib/stores/console/segments.store'; import { satellitesUncertifiedStore } from '$lib/stores/mission-control/satellites.store'; +import { ufosUncertifiedStore } from '$lib/stores/mission-control/ufos.store'; import type { NullishIdentity } from '$lib/types/itentity'; import type { Metadata, MetadataUi } from '$lib/types/metadata'; import type { MissionControlId } from '$lib/types/mission-control'; import type { SatelliteId } from '$lib/types/satellite'; +import type { UfoId } from '$lib/types/ufo'; import { isNullish, nonNullish, notEmptyString } from '@dfinity/utils'; import type { Nullish } from '@dfinity/zod-schemas'; import type { Identity } from '@icp-sdk/core/agent'; +import type { Principal } from '@icp-sdk/core/principal'; import { get } from 'svelte/store'; import * as z from 'zod'; -interface SetSatelliteMetadataParams { +export interface SetMetadataParams { missionControlId: Nullish; - satellite: MissionControlDid.Satellite; metadata: MetadataUi; identity: NullishIdentity; } +export interface SetMetadataResult { + success: boolean; +} + +interface SetSatelliteMetadataParams extends SetMetadataParams { + satellite: MissionControlDid.Satellite; +} + +interface SetUfoMetadataParams extends SetMetadataParams { + ufo: MissionControlDid.Ufo; +} + export const setSatelliteMetadata = async ({ missionControlId, satellite: { satellite_id: satelliteId, metadata: currentMetadata }, metadata, identity -}: SetSatelliteMetadataParams): Promise<{ success: boolean }> => { +}: SetSatelliteMetadataParams): Promise => { + // TODO: indentity check service + if (isNullish(identity) || isNullish(identity?.getPrincipal())) { + toasts.error({ text: get(i18n).core.not_logged_in }); + return { success: false }; + } + + const { error, success, data } = MetadataUiSchema.safeParse(metadata); + + if (!success) { + toasts.error({ + text: get(i18n).errors.invalid_metadata, + detail: z.prettifyError(error) + }); + return { success: false }; + } + + const updateMetadata = prepareMetadata({ data, currentMetadata }); + + const payload = { + metadata: updateMetadata, + identity + }; + + const results = await Promise.all([ + setMetadataWithConsole({ + ...payload, + segmentId: satelliteId, + segmentKindText: 'Satellite' + }), + ...(nonNullish(missionControlId) + ? [ + setSatelliteMetadataWithMissionControl({ + missionControlId, + satelliteId, + ...payload + }) + ] + : []) + ]); + + const hasError = results.find(({ result }) => result === 'error') !== undefined; + + return { success: !hasError }; +}; + +export const setUfoMetadata = async ({ + missionControlId, + ufo: { ufo_id: ufoId, metadata: currentMetadata }, + metadata, + identity +}: SetUfoMetadataParams): Promise => { // TODO: indentity check service if (isNullish(identity) || isNullish(identity?.getPrincipal())) { toasts.error({ text: get(i18n).core.not_logged_in }); @@ -56,17 +125,21 @@ export const setSatelliteMetadata = async ({ const updateMetadata = prepareMetadata({ data, currentMetadata }); const payload = { - satelliteId, metadata: updateMetadata, identity }; const results = await Promise.all([ - setMetadataWithConsole(payload), + setMetadataWithConsole({ + ...payload, + segmentId: ufoId, + segmentKindText: 'Ufo' + }), ...(nonNullish(missionControlId) ? [ - setMetadataWithMissionControl({ + setUfoMetadataWithMissionControl({ missionControlId, + ufoId, ...payload }) ] @@ -108,22 +181,24 @@ const prepareMetadata = ({ }; const setMetadataWithConsole = async ({ - satelliteId, + segmentId, + segmentKindText, metadata, identity }: { - satelliteId: SatelliteId; + segmentId: Principal; + segmentKindText: 'Satellite' | 'Ufo'; metadata: Metadata; identity: Identity; }): Promise<{ result: 'skip' | 'success' | 'error' }> => { const currentState = get(segments); - const isKeySatellite = (segmentKey: SegmentKey): boolean => - segmentKey.segment_id.toText() === satelliteId.toText() && - 'Satellite' in segmentKey.segment_kind && + const isKeySegment = (segmentKey: SegmentKey): boolean => + segmentKey.segment_id.toText() === segmentId.toText() && + segmentKindText in segmentKey.segment_kind && segmentKey.user.toText() === identity.getPrincipal().toText(); - const segment = currentState?.find(([segmentKey]) => isKeySatellite(segmentKey)); + const segment = currentState?.find(([segmentKey]) => isKeySegment(segmentKey)); // No segment found in the Console. Maybe it's a "legacy" segment that is "only" known by the Mission Control. if (isNullish(segment)) { @@ -131,29 +206,31 @@ const setMetadataWithConsole = async ({ } try { + const segmentKind = segmentKindText === 'Ufo' ? { Ufo: null } : { Satellite: null }; + const updatedSegment = await setSegmentMetadata({ - segmentId: satelliteId, - segmentKind: { Satellite: null }, + segmentId, + segmentKind, metadata, identity }); const updateKey: SegmentKey = { - segment_id: satelliteId, - segment_kind: { Satellite: null }, + segment_id: segmentId, + segment_kind: segmentKind, user: identity.getPrincipal() }; const updateState = get(segments); segmentsUncertifiedStore.set([ - ...(updateState ?? []).filter(([segmentKey]) => !isKeySatellite(segmentKey)), + ...(updateState ?? []).filter(([segmentKey]) => !isKeySegment(segmentKey)), [updateKey, updatedSegment] ]); return { result: 'success' }; } catch (err: unknown) { toasts.error({ - text: get(i18n).errors.satellite_metadata_update, + text: get(i18n).errors.update_metadata_error, detail: err }); @@ -161,7 +238,7 @@ const setMetadataWithConsole = async ({ } }; -const setMetadataWithMissionControl = async ({ +const setSatelliteMetadataWithMissionControl = async ({ missionControlId, metadata, satelliteId, @@ -208,3 +285,47 @@ const setMetadataWithMissionControl = async ({ return { result: 'error' }; } }; + +const setUfoMetadataWithMissionControl = async ({ + missionControlId, + metadata, + ufoId, + ...rest +}: { + missionControlId: MissionControlId; + ufoId: UfoId; + metadata: Metadata; + identity: Identity; +}): Promise<{ result: 'success' | 'warn' | 'error' }> => { + const currentState = get(mctrlUfos); + const ufo = currentState?.find(({ ufo_id }) => ufo_id.toText() === ufoId.toText()); + + if (isNullish(ufo)) { + toasts.warn(get(i18n).mission_control.warn_ufo_metadata_update); + return { result: 'warn' }; + } + + try { + const updatedUfo = await setUfoMetadataApi({ + missionControlId, + ufoId, + metadata, + ...rest + }); + + const updateState = get(mctrlUfos); + ufosUncertifiedStore.set([ + ...(updateState ?? []).filter(({ ufo_id }) => updatedUfo.ufo_id.toText() !== ufo_id.toText()), + updatedUfo + ]); + + return { result: 'success' }; + } catch (err: unknown) { + toasts.error({ + text: get(i18n).errors.ufo_metadata_update, + detail: err + }); + + return { result: 'error' }; + } +}; diff --git a/src/frontend/src/lib/types/i18n.d.ts b/src/frontend/src/lib/types/i18n.d.ts index d7cd638939..5c1e2b8c43 100644 --- a/src/frontend/src/lib/types/i18n.d.ts +++ b/src/frontend/src/lib/types/i18n.d.ts @@ -128,6 +128,8 @@ interface I18nCore { name: string; environment: string; tags: string; + edit_details: string; + tags_placeholder: string; } interface I18nCanisters { @@ -344,9 +346,7 @@ interface I18nSatellites { application: string; application_description: string; application_hint: string; - tags_placeholder: string; enter_name: string; - edit_details: string; create_satellite_price: string; loading_satellites: string; overview: string; @@ -379,6 +379,7 @@ interface I18nMission_control { attaching: string; warn_attaching: string; warn_satellite_metadata_update: string; + warn_ufo_metadata_update: string; } interface I18nWallet { @@ -635,13 +636,14 @@ interface I18nErrors { cli_missing_params: string; cli_missing_selection: string; cli_unexpected_error: string; - satellite_name_missing: string; + segment_name_missing: string; satellite_kind: string; satellite_unexpected_error: string; satellite_no_found: string; satellite_metadata_update: string; satellite_missing_name: string; satellites_not_loaded: string; + ufo_metadata_update: string; create_ufo_name_missing: string; create_ufo_unexpected_error: string; ufo_not_found: string; @@ -730,6 +732,7 @@ interface I18nErrors { invalid_email: string; invalid_destination: string; invalid_metadata: string; + update_metadata_error: string; empty_amount: string; cycles_transfer_not_supported: string; convert_icp_to_cycles_not_supported: string;