diff --git a/src/frontend/src/lib/components/modals/Modals.svelte b/src/frontend/src/lib/components/modals/Modals.svelte index 05cadab43..a95fbdf04 100644 --- a/src/frontend/src/lib/components/modals/Modals.svelte +++ b/src/frontend/src/lib/components/modals/Modals.svelte @@ -2,6 +2,7 @@ import { nonNullish } from '@dfinity/utils'; import AuthConfigModal from '$lib/components/modals/auth/AuthConfigModal.svelte'; import UserDetailsModal from '$lib/components/modals/auth/UserDetailsModal.svelte'; + import AutomationKeysConfigModal from '$lib/components/modals/automation/AutomationKeysConfigModal.svelte'; import CreateAutomationModal from '$lib/components/modals/automation/CreateAutomationModal.svelte'; import ApplyChangeModal from '$lib/components/modals/changes/ApplyChangeModal.svelte'; import RejectChangeModal from '$lib/components/modals/changes/RejectChangeModal.svelte'; @@ -168,3 +169,7 @@ {#if modal?.type === 'create_automation' && nonNullish(modal.detail)} {/if} + +{#if modal?.type === 'edit_automation_keys_config' && nonNullish(modal.detail)} + +{/if} diff --git a/src/frontend/src/lib/components/modals/automation/AutomationKeysConfigModal.svelte b/src/frontend/src/lib/components/modals/automation/AutomationKeysConfigModal.svelte new file mode 100644 index 000000000..1ddbd8c06 --- /dev/null +++ b/src/frontend/src/lib/components/modals/automation/AutomationKeysConfigModal.svelte @@ -0,0 +1,82 @@ + + + + {#if step === 'ready'} +
+

{$i18n.core.configuration_applied}

+ +
+ {:else if step === 'in_progress'} + +

{$i18n.core.updating_configuration}

+
+ {:else} + + {/if} +
+ + diff --git a/src/frontend/src/lib/components/satellites/auth/AuthSettingsLoader.svelte b/src/frontend/src/lib/components/satellites/auth/AuthSettingsLoader.svelte index d3cf1cd8b..12a210d18 100644 --- a/src/frontend/src/lib/components/satellites/auth/AuthSettingsLoader.svelte +++ b/src/frontend/src/lib/components/satellites/auth/AuthSettingsLoader.svelte @@ -4,7 +4,6 @@ import SpinnerParagraph from '$lib/components/ui/SpinnerParagraph.svelte'; import Warning from '$lib/components/ui/Warning.svelte'; import { authIdentity } from '$lib/derived/auth.derived'; - import { satelliteAuthConfig } from '$lib/derived/satellite/satellite-configs.derived'; import { getRuleUser } from '$lib/services/satellite/collection/collection.services'; import { loadSatelliteConfig } from '$lib/services/satellite/satellite-config.services'; import { i18n } from '$lib/stores/app/i18n.store'; @@ -37,8 +36,6 @@ }); }; - $inspect($satelliteAuthConfig); - const load = async () => { await Promise.all([loadConfig(), loadRule()]); }; diff --git a/src/frontend/src/lib/components/satellites/automation/AutomationSettings.svelte b/src/frontend/src/lib/components/satellites/automation/AutomationSettings.svelte new file mode 100644 index 000000000..d7160439d --- /dev/null +++ b/src/frontend/src/lib/components/satellites/automation/AutomationSettings.svelte @@ -0,0 +1,18 @@ + + + + {#snippet content(config: SatelliteDid.OpenIdAutomationProviderConfig)} + + {/snippet} + diff --git a/src/frontend/src/lib/components/satellites/automation/guards/GitHubConfigGuard.svelte b/src/frontend/src/lib/components/satellites/automation/guards/GitHubConfigGuard.svelte new file mode 100644 index 000000000..a56c5f828 --- /dev/null +++ b/src/frontend/src/lib/components/satellites/automation/guards/GitHubConfigGuard.svelte @@ -0,0 +1,32 @@ + + +{#if $githubConfig === undefined} + {$i18n.core.loading_config} +{:else if $githubConfig === null} +
+ + + +
+{:else} +
+ {@render content($githubConfig)} +
+{/if} diff --git a/src/frontend/src/lib/components/satellites/automation/settings/AutomationKeysConfigForm.svelte b/src/frontend/src/lib/components/satellites/automation/settings/AutomationKeysConfigForm.svelte new file mode 100644 index 000000000..b12079aa8 --- /dev/null +++ b/src/frontend/src/lib/components/satellites/automation/settings/AutomationKeysConfigForm.svelte @@ -0,0 +1,124 @@ + + +

{$i18n.automation.keys}

+ +

+ {$i18n.automation.edit_automation_keys} +

+ +
+
+
+ + {#snippet label()} + {$i18n.controllers.scope} + {/snippet} + + + +
+ +
+ + {#snippet label()} + {$i18n.authentication.session_duration}{#if customMaxTimeToLive} + ({$i18n.authentication.in_nanoseconds}){/if} + {/snippet} + + {#if customMaxTimeToLive} + + {:else} + + {/if} + +
+
+ + +
diff --git a/src/frontend/src/lib/components/satellites/automation/settings/AutomationKeysSettings.svelte b/src/frontend/src/lib/components/satellites/automation/settings/AutomationKeysSettings.svelte new file mode 100644 index 000000000..18bb1b9f6 --- /dev/null +++ b/src/frontend/src/lib/components/satellites/automation/settings/AutomationKeysSettings.svelte @@ -0,0 +1,114 @@ + + +
+ {$i18n.automation.keys} + +
+
+
+ + {#snippet label()} + {$i18n.controllers.scope} + {/snippet} + +

{scope === 'submit' ? $i18n.controllers.submit : $i18n.controllers.write}

+
+
+
+ +
+
+ + {#snippet label()} + {$i18n.automation.access_duration} + {/snippet} + +

+ {#if maxTimeToLive === BigInt(TWO_MINUTES_NS)} + {$i18n.core.two_minutes} + {:else if maxTimeToLive === BigInt(FIVE_MINUTES_NS)} + {$i18n.core.five_minutes} + {:else if maxTimeToLive === BigInt(TEN_MINUTES_NS)} + {$i18n.core.ten_minutes} + {:else if maxTimeToLive === BigInt(FIFTEEN_MINUTES_NS)} + {$i18n.core.fifteen_minutes} + {:else if maxTimeToLive === BigInt(THIRTY_MINUTES_NS)} + {$i18n.core.thirty_minutes} + {:else if maxTimeToLive === BigInt(FORTY_FIVE_MINUTES_NS)} + {$i18n.core.forty_five_minutes} + {:else if maxTimeToLive === BigInt(AN_HOUR_NS)} + {$i18n.core.an_hour} + {:else} + {secondsToDuration(maxTimeToLive ?? 0n)} + {/if} +

+
+
+
+
+
+ + diff --git a/src/frontend/src/lib/constants/automation.constants.ts b/src/frontend/src/lib/constants/automation.constants.ts new file mode 100644 index 000000000..c596ac02d --- /dev/null +++ b/src/frontend/src/lib/constants/automation.constants.ts @@ -0,0 +1,3 @@ +import { TEN_MINUTES_NS } from '$lib/constants/duration.constants'; + +export const AUTOMATION_DEFAULT_MAX_SESSION_TIME_TO_LIVE = TEN_MINUTES_NS; diff --git a/src/frontend/src/lib/constants/duration.constants.ts b/src/frontend/src/lib/constants/duration.constants.ts index 2b2103243..4bc205458 100644 --- a/src/frontend/src/lib/constants/duration.constants.ts +++ b/src/frontend/src/lib/constants/duration.constants.ts @@ -2,8 +2,13 @@ const MINUTE_NANOSECONDS = 60n * 1_000_000_000n; const HOUR_NANOSECONDS = 60n * MINUTE_NANOSECONDS; const DAY_NANOSECONDS = 24n * HOUR_NANOSECONDS; -// 1 day in nanoseconds -export const AN_HOUR_NS = HOUR_NANOSECONDS; +export const TWO_MINUTES_NS = 2n * MINUTE_NANOSECONDS; +export const FIVE_MINUTES_NS = 5n * MINUTE_NANOSECONDS; +export const TEN_MINUTES_NS = 10n * MINUTE_NANOSECONDS; +export const FIFTEEN_MINUTES_NS = 15n * MINUTE_NANOSECONDS; +export const THIRTY_MINUTES_NS = 30n * MINUTE_NANOSECONDS; +export const FORTY_FIVE_MINUTES_NS = 45n * MINUTE_NANOSECONDS; +export const AN_HOUR_NS = HOUR_NANOSECONDS; // One hour max for automation export const TWO_HOURS_NS = 2n * AN_HOUR_NS; export const FOUR_HOURS_NS = 4n * AN_HOUR_NS; export const EIGHT_HOURS_NS = 8n * AN_HOUR_NS; @@ -11,4 +16,4 @@ export const HALF_DAY_NS = 12n * AN_HOUR_NS; export const ONE_DAY_NS = DAY_NANOSECONDS; export const A_WEEK_NS = 7n * ONE_DAY_NS; export const TWO_WEEKS_NS = 2n * A_WEEK_NS; -export const A_MONTH_NS = 30n * ONE_DAY_NS; // 30 days. Max. +export const A_MONTH_NS = 30n * ONE_DAY_NS; // 30 days. max for authentication diff --git a/src/frontend/src/lib/derived/satellite/github.derived.ts b/src/frontend/src/lib/derived/satellite/github.derived.ts new file mode 100644 index 000000000..e6529c76f --- /dev/null +++ b/src/frontend/src/lib/derived/satellite/github.derived.ts @@ -0,0 +1,14 @@ +import { satelliteAutomationConfig } from '$lib/derived/satellite/satellite-configs.derived'; +import { fromNullable, isNullish } from '@dfinity/utils'; +import { derived } from 'svelte/store'; + +export const githubConfig = derived([satelliteAutomationConfig], ([$satelliteAutomationConfig]) => { + // Undefined not loaded or null as set as such + if (isNullish($satelliteAutomationConfig)) { + return $satelliteAutomationConfig; + } + + const openid = fromNullable($satelliteAutomationConfig.openid); + const github = openid?.providers.find(([key]) => 'GitHub' in key); + return github?.[1] ?? null; +}); diff --git a/src/frontend/src/lib/i18n/en.json b/src/frontend/src/lib/i18n/en.json index ebc0e0fb4..b3f305d59 100644 --- a/src/frontend/src/lib/i18n/en.json +++ b/src/frontend/src/lib/i18n/en.json @@ -98,6 +98,12 @@ "staging": "Staging", "test": "Test", "unspecified": "Unspecified", + "two_minutes": "Two minutes", + "five_minutes": "Five minutes", + "ten_minutes": "Ten minutes", + "fifteen_minutes": "Fifteen minutes", + "thirty_minutes": "Thirty minutes", + "forty_five_minutes": "Forty-five minutes", "an_hour": "An hour", "two_hours": "Two hours", "four_hours": "Four hours", @@ -753,7 +759,8 @@ "build_repo_key_invalid_github_url": "Only GitHub repositories are supported. Please enter the GitHub URL of your repository.", "build_repo_key_owner_not_found": "Repository owner not found in URL. Please enter a complete GitHub repository URL.", "build_repo_key_repo_not_found": "Repository name not found in URL. Please enter a complete GitHub repository URL.", - "create_automation_config": "An error occurred while saving your automation configuration." + "save_automation_config": "An error occurred while saving your automation configuration.", + "automation_config_undefined": "The automation configuration is undefined, which is unexpected. Try reloading the page." }, "document": { "owner": "Owner", @@ -1119,6 +1126,9 @@ "view_workflow": "View workflow run on GitHub", "view_commit": "View commit on GitHub", "view_branch": "View branch run on GitHub", - "view_contributor": "View contributor on GitHub" + "view_contributor": "View contributor on GitHub", + "keys": "Automation Keys", + "access_duration": "Access Duration", + "edit_automation_keys": "Configure how your workflows access your Satellite and how long their keys remain valid." } } diff --git a/src/frontend/src/lib/i18n/zh-cn.json b/src/frontend/src/lib/i18n/zh-cn.json index 698f3271e..03591e063 100644 --- a/src/frontend/src/lib/i18n/zh-cn.json +++ b/src/frontend/src/lib/i18n/zh-cn.json @@ -99,6 +99,12 @@ "staging": "预发环境", "test": "测试环境", "unspecified": "未定义状态", + "two_minutes": "两分钟", + "five_minutes": "五分钟", + "ten_minutes": "十分钟", + "fifteen_minutes": "十五分钟", + "thirty_minutes": "三十分钟", + "forty_five_minutes": "四十五分钟", "an_hour": "一小时", "two_hours": "两小时", "four_hours": "四小时", @@ -755,7 +761,8 @@ "build_repo_key_invalid_github_url": "仅支持 GitHub 仓库。请输入您的 GitHub 仓库 URL。", "build_repo_key_owner_not_found": "在 URL 中未找到仓库所有者。请输入完整的 GitHub 仓库 URL。", "build_repo_key_repo_not_found": "在 URL 中未找到仓库名称。请输入完整的 GitHub 仓库 URL。", - "create_automation_config": "保存自动化配置时发生错误。" + "save_automation_config": "保存自动化配置时发生错误。", + "automation_config_undefined": "自动化配置未定义,这是意外情况。请尝试重新加载页面。" }, "document": { "owner": "所有者", @@ -1121,6 +1128,9 @@ "view_workflow": "在 GitHub 上查看工作流运行", "view_commit": "在 GitHub 上查看提交", "view_branch": "在 GitHub 上查看分支", - "view_contributor": "在 GitHub 上查看贡献者" + "view_contributor": "在 GitHub 上查看贡献者", + "keys": "自动化密钥", + "access_duration": "访问时长", + "edit_automation_keys": "配置工作流访问 Satellite 的方式及其密钥的有效期。" } } diff --git a/src/frontend/src/lib/services/satellite/automation/automation.config.edit.services.ts b/src/frontend/src/lib/services/satellite/automation/automation.config.edit.services.ts new file mode 100644 index 000000000..bba0acf13 --- /dev/null +++ b/src/frontend/src/lib/services/satellite/automation/automation.config.edit.services.ts @@ -0,0 +1,116 @@ +import type { SatelliteDid } from '$declarations'; +import { setAutomationConfig } from '$lib/api/satellites.api'; +import { AUTOMATION_DEFAULT_MAX_SESSION_TIME_TO_LIVE } from '$lib/constants/automation.constants'; +import { loadSatelliteConfig } from '$lib/services/satellite/satellite-config.services'; +import { i18n } from '$lib/stores/app/i18n.store'; +import { toasts } from '$lib/stores/app/toasts.store'; +import type { AddAccessKeyScope } from '$lib/types/access-keys'; +import type { OptionIdentity } from '$lib/types/itentity'; +import type { Satellite } from '$lib/types/satellite'; +import { isNullish, toNullable } from '@dfinity/utils'; +import type { Identity } from '@icp-sdk/core/agent'; +import { get } from 'svelte/store'; + +export interface UpdateAutomationConfigResult { + success: 'ok' | 'error'; + err?: unknown; +} + +interface UpdateResult { + result: 'success' | 'error'; + err?: unknown; +} + +export const updateAutomationKeysConfig = async ({ + automationConfig, + providerConfig, + maxTimeToLive, + scope, + identity, + satellite +}: { + scope: Omit | undefined; + maxTimeToLive: bigint | undefined; + automationConfig: SatelliteDid.AutomationConfig; + providerConfig: SatelliteDid.OpenIdAutomationProviderConfig; + satellite: Satellite; + identity: OptionIdentity; +}): Promise => { + const labels = get(i18n); + + if (isNullish(identity) || isNullish(identity?.getPrincipal())) { + toasts.error({ text: labels.core.not_logged_in }); + return { success: 'error' }; + } + + const updateProviderConfig: SatelliteDid.OpenIdAutomationProviderConfig = { + ...providerConfig, + controller: + scope === 'write' && maxTimeToLive === AUTOMATION_DEFAULT_MAX_SESSION_TIME_TO_LIVE + ? [] + : toNullable({ + max_time_to_live: + maxTimeToLive === AUTOMATION_DEFAULT_MAX_SESSION_TIME_TO_LIVE + ? [] + : toNullable(maxTimeToLive), + scope: scope === 'write' ? [] : toNullable({ Submit: null }) + }) + }; + + const updateAutomationConfig: SatelliteDid.SetAutomationConfig = { + ...automationConfig, + openid: toNullable({ + observatory_id: [], + providers: [[{ GitHub: null }, updateProviderConfig]] + }) + }; + + const result = await updateConfig({ identity, satellite, config: updateAutomationConfig }); + + if (result.result === 'error') { + return { + success: 'error', + err: result.err + }; + } + + if (result.result === 'success') { + // Reload Satellite configuration + await loadSatelliteConfig({ + identity, + satelliteId: satellite.satellite_id, + reload: true + }); + } + + return { success: 'ok' }; +}; + +const updateConfig = async ({ + satellite: { satellite_id: satelliteId }, + config, + identity +}: { + satellite: Satellite; + config: SatelliteDid.SetAutomationConfig; + identity: Identity; +}): Promise => { + try { + await setAutomationConfig({ + satelliteId, + config, + identity + }); + } catch (err: unknown) { + const labels = get(i18n); + + toasts.error({ + text: labels.errors.save_automation_config, + detail: err + }); + + return { result: 'error', err }; + } + + return { result: 'success' }; +}; diff --git a/src/frontend/src/lib/services/satellite/automation/automation.config.services.ts b/src/frontend/src/lib/services/satellite/automation/automation.config.services.ts index d904c7f89..2983bd9dd 100644 --- a/src/frontend/src/lib/services/satellite/automation/automation.config.services.ts +++ b/src/frontend/src/lib/services/satellite/automation/automation.config.services.ts @@ -140,7 +140,7 @@ const setAutomationConfig = async ({ const labels = get(i18n); toasts.error({ - text: labels.errors.create_automation_config, + text: labels.errors.save_automation_config, detail: err }); diff --git a/src/frontend/src/lib/types/i18n.d.ts b/src/frontend/src/lib/types/i18n.d.ts index 98b3a8724..fe4addb46 100644 --- a/src/frontend/src/lib/types/i18n.d.ts +++ b/src/frontend/src/lib/types/i18n.d.ts @@ -101,6 +101,12 @@ interface I18nCore { staging: string; test: string; unspecified: string; + two_minutes: string; + five_minutes: string; + ten_minutes: string; + fifteen_minutes: string; + thirty_minutes: string; + forty_five_minutes: string; an_hour: string; two_hours: string; four_hours: string; @@ -771,7 +777,8 @@ interface I18nErrors { build_repo_key_invalid_github_url: string; build_repo_key_owner_not_found: string; build_repo_key_repo_not_found: string; - create_automation_config: string; + save_automation_config: string; + automation_config_undefined: string; } interface I18nDocument { @@ -1158,6 +1165,9 @@ interface I18nAutomation { view_commit: string; view_branch: string; view_contributor: string; + keys: string; + access_duration: string; + edit_automation_keys: string; } interface I18n { diff --git a/src/frontend/src/lib/types/modal.ts b/src/frontend/src/lib/types/modal.ts index 7c44578c3..2a30031fa 100644 --- a/src/frontend/src/lib/types/modal.ts +++ b/src/frontend/src/lib/types/modal.ts @@ -131,6 +131,11 @@ export interface JunoModalWalletDetail { export type JunoModalConvertIcpToCyclesDetails = Pick; +export interface JunoModalAutomationConfigDetail extends JunoModalWithSatellite { + automationConfig: SatelliteDid.AutomationConfig; + providerConfig: SatelliteDid.OpenIdAutomationProviderConfig; +} + export type JunoModalDetail = | JunoModalUpgradeSatelliteDetail | JunoModalUpgradeDetail @@ -150,7 +155,8 @@ export type JunoModalDetail = | JunoModalChangeDetail | JunoModalCdnUpgradeDetail | JunoModalEditAuthConfigDetail - | JunoModalWalletDetail; + | JunoModalWalletDetail + | JunoModalAutomationConfigDetail; export interface JunoModal { type: @@ -185,6 +191,7 @@ export interface JunoModal { | 'upgrade_satellite_with_cdn' | 'convert_icp_to_cycles' | 'reconcile_out_of_sync_segments' - | 'create_automation'; + | 'create_automation' + | 'edit_automation_keys_config'; detail?: T; } diff --git a/src/frontend/src/routes/(split)/deployments/+page.svelte b/src/frontend/src/routes/(split)/deployments/+page.svelte index 12627587f..e8aa01b9a 100644 --- a/src/frontend/src/routes/(split)/deployments/+page.svelte +++ b/src/frontend/src/routes/(split)/deployments/+page.svelte @@ -5,6 +5,7 @@ import Loaders from '$lib/components/app/loaders/Loaders.svelte'; import IdentityGuard from '$lib/components/auth/guards/IdentityGuard.svelte'; import Automation from '$lib/components/satellites/automation/Automation.svelte'; + import AutomationSettings from '$lib/components/satellites/automation/AutomationSettings.svelte'; import SatelliteGuard from '$lib/components/satellites/guards/SatelliteGuard.svelte'; import NoTabs from '$lib/components/ui/NoTabs.svelte'; import Tabs from '$lib/components/ui/Tabs.svelte'; @@ -21,6 +22,10 @@ { id: Symbol('1'), labelKey: 'automation.title' + }, + { + id: Symbol('2'), + labelKey: 'core.settings' } ]; @@ -43,6 +48,8 @@ {#if $store.tabId === $store.tabs[0].id} + {:else if $store.tabId === $store.tabs[1].id} + {/if} {/snippet}