From 924bf0d3f227737ffe5bdf14a7a7a4ee037f3782 Mon Sep 17 00:00:00 2001 From: yacchin1205 <968739+yacchin1205@users.noreply.github.com> Date: Tue, 21 Oct 2025 22:18:38 +0900 Subject: [PATCH 1/2] Add toggle for copying default storage with paths.yaml support --- .../-components/build-console/component.ts | 10 + .../-components/build-console/styles.scss | 20 ++ .../-components/build-console/template.hbs | 28 ++- .../-components/project-editor/component.ts | 198 ++++++++++++++++++ .../-components/project-editor/template.hbs | 32 +++ translations/en-us.yml | 7 +- translations/ja.yml | 7 +- 7 files changed, 299 insertions(+), 3 deletions(-) diff --git a/app/guid-node/binderhub/-components/build-console/component.ts b/app/guid-node/binderhub/-components/build-console/component.ts index 79c477651..bef27ebf0 100644 --- a/app/guid-node/binderhub/-components/build-console/component.ts +++ b/app/guid-node/binderhub/-components/build-console/component.ts @@ -32,6 +32,16 @@ export default class BuildConsole extends Component { notAuthorized: boolean = false; + copyDefaultStorage?: boolean; + + pathsYamlIsCustom?: boolean; + + userHasWritePermission?: boolean; + + toggleCopyDefaultStorage?: (event: Event) => void; + + openPathsYamlResetConfirm?: () => void; + @requiredAction renewToken!: (binderhubUrl: string) => void; @requiredAction requestBuild!: ( diff --git a/app/guid-node/binderhub/-components/build-console/styles.scss b/app/guid-node/binderhub/-components/build-console/styles.scss index 791961aa4..9bff22da1 100644 --- a/app/guid-node/binderhub/-components/build-console/styles.scss +++ b/app/guid-node/binderhub/-components/build-console/styles.scss @@ -53,3 +53,23 @@ .build_launch_button { margin-left: auto; } + +.paths_toggle { + margin: 1.5em 0; +} + +.paths_toggle__label { + display: flex; + align-items: center; + gap: 0.5em; + margin-bottom: 0; +} + +.paths_toggle__label--disabled { + color: #999; + cursor: not-allowed; +} + +.paths_toggle__hint { + margin: 0.5em 0; +} diff --git a/app/guid-node/binderhub/-components/build-console/template.hbs b/app/guid-node/binderhub/-components/build-console/template.hbs index 18f2b1e1c..01b3a3f1c 100644 --- a/app/guid-node/binderhub/-components/build-console/template.hbs +++ b/app/guid-node/binderhub/-components/build-console/template.hbs @@ -1,7 +1,33 @@

{{t 'binderhub.deployment.header'}}

-{{t 'binderhub.deployment.description'}} +
+ + {{#if this.pathsYamlIsCustom}} +

+ {{t 'binderhub.deployment.copy_default_storage_disabled_hint'}} +

+ {{#if this.userHasWritePermission}} + + {{/if}} + {{/if}} +
this.loadCurrentFile(file, files)); await Promise.all(tasks); + await this.loadPathsYaml(files); } async saveCurrentFile( @@ -1624,6 +1711,50 @@ export default class ProjectEditor extends Component { ); } + @action + toggleCopyDefaultStorage(this: ProjectEditor, event: Event) { + const node = this.get('node'); + if (!node || !node.userHasWritePermission || this.pathsYamlIsCustom) { + return; + } + const target = event.target as HTMLInputElement; + const previous = this.pathsYamlState; + const newState = target.checked ? PathsYamlState.DEFAULT : PathsYamlState.NO_COPY; + this.set('pathsYamlState', newState); + later(async () => { + try { + await this.persistPathsYamlPreference(); + this.pageCleanser(); + } catch (exception) { + this.set('pathsYamlState', previous); + this.onError(exception, this.intl.t('binderhub.error.modify_files_error')); + } + }, 0); + } + + @action + openPathsYamlResetConfirm(this: ProjectEditor) { + this.set('showPathsYamlResetConfirm', true); + } + + @action + cancelPathsYamlReset(this: ProjectEditor) { + this.set('showPathsYamlResetConfirm', false); + } + + @action + resetPathsYaml(this: ProjectEditor) { + this.set('showPathsYamlResetConfirm', false); + later(async () => { + try { + await this.performPathsYamlReset(); + this.pageCleanser(); + } catch (exception) { + this.onError(exception, this.intl.t('binderhub.error.modify_files_error')); + } + }, 0); + } + @action editDockerfileContent(this: ProjectEditor, event: { target: HTMLInputElement }) { this.set('editingDockerfileContent', event.target.value); @@ -1728,4 +1859,71 @@ export default class ProjectEditor extends Component { modifyCustomBaseImageModelForLocale(model: CustomBaseImageModel) { return this.modifyImageForLocale(imageFromCustomBaseImageModel(model)); } + + private getHashedMinimalPathsYaml(): string { + const content = MINIMAL_PATHS_YAML_CONTENT; + const checksum = md5(content.trim()); + return `# rdm-binderhub:hash:${checksum}\n${content}`; + } + + private async loadPathsYaml(files: WaterButlerFile[] | null) { + const pathsFile = await this.getFile(PATHS_YAML_FILENAME, files); + if (!pathsFile) { + this.set('pathsYamlModel', null); + this.set('pathsYaml', ''); + this.set('pathsYamlState', PathsYamlState.DEFAULT); + return; + } + const content = (await pathsFile.getContents()).toString(); + this.set('pathsYamlModel', pathsFile); + this.set('pathsYaml', content); + + // Use computed property to check if manually changed (avoid duplicate hash verification) + if (this.pathsYamlManuallyChanged) { + this.set('pathsYamlState', PathsYamlState.CUSTOM); + return; + } + + // Hash is valid - verify content structure is minimal paths.yaml + const parsed = this.pathsYamlContent; + if (!parsed) { + throw new EmberError('Invalid paths.yaml: hash is valid but failed to parse content'); + } + if (!parsed.override || parsed.paths.length !== 0) { + throw new EmberError( + 'Invalid paths.yaml: hash is valid but content does not match minimal template', + ); + } + this.set('pathsYamlState', PathsYamlState.NO_COPY); + } + + private async persistPathsYamlPreference() { + if (!this.configFolder) { + throw new EmberError('Illegal config'); + } + const files = await this.getRootFiles(true); + const pathsFile = await this.getFile(PATHS_YAML_FILENAME, files); + if (this.pathsYamlState === PathsYamlState.DEFAULT) { + if (pathsFile) { + await pathsFile.delete(); + } + } else { + const content = this.getHashedMinimalPathsYaml(); + if (!pathsFile) { + await this.configFolder.createFile(PATHS_YAML_FILENAME, content); + } else { + await pathsFile.updateContents(content); + } + } + await this.loadPathsYaml(await this.getRootFiles(true)); + } + + private async performPathsYamlReset() { + const files = await this.getRootFiles(true); + const pathsFile = await this.getFile(PATHS_YAML_FILENAME, files); + if (pathsFile) { + await pathsFile.delete(); + } + await this.loadPathsYaml(await this.getRootFiles(true)); + } } diff --git a/app/guid-node/binderhub/-components/project-editor/template.hbs b/app/guid-node/binderhub/-components/project-editor/template.hbs index fdb91b54c..e6fcd65fc 100644 --- a/app/guid-node/binderhub/-components/project-editor/template.hbs +++ b/app/guid-node/binderhub/-components/project-editor/template.hbs @@ -266,10 +266,42 @@ @beforeLaunch={{this.performDockerfileSave}} @buildLog={{this.buildLog}} @buildPhase={{this.buildPhase}} + @copyDefaultStorage={{this.copyDefaultStorage}} + @pathsYamlIsCustom={{this.pathsYamlIsCustom}} + @userHasWritePermission={{this.node.userHasWritePermission}} + @toggleCopyDefaultStorage={{action this.toggleCopyDefaultStorage}} + @openPathsYamlResetConfirm={{action this.openPathsYamlResetConfirm}} /> {{/if}}
+ + +

+ {{t 'binderhub.deployment.reset_paths_yaml_confirm_title'}} +

+
+ + {{t 'binderhub.deployment.reset_paths_yaml_confirm_body'}} + + + + {{t 'general.cancel'}} + + + {{t 'binderhub.deployment.reset_paths_yaml_confirm_ok'}} + + +
+ Date: Wed, 22 Oct 2025 15:08:59 +0900 Subject: [PATCH 2/2] Fix the messages for the BinderHub Addon --- translations/en-us.yml | 2 +- translations/ja.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/translations/en-us.yml b/translations/en-us.yml index f36657bea..95d297874 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -1831,7 +1831,7 @@ binderhub: no_environment: 'This environment is not configured by the editor.' dockerfile_editor_title: 'Dockerfile' save_file: 'Save' - copy_default_storage_checkbox: 'Copy the contents of this project''s default storage.' + copy_default_storage_checkbox: 'Copy the contents of the default storage.' copy_default_storage_disabled_hint: 'paths.yaml has custom settings. Reset it to manage the copy setting here.' reset_paths_yaml: 'Reset paths.yaml' reset_paths_yaml_confirm_title: 'Reset paths.yaml' diff --git a/translations/ja.yml b/translations/ja.yml index fd7e0da5c..5c7595afb 100644 --- a/translations/ja.yml +++ b/translations/ja.yml @@ -1831,11 +1831,11 @@ binderhub: no_environment: 'エディタにより設定された環境ではありません。' dockerfile_editor_title: 'Dockerfile' save_file: '保存' - copy_default_storage_checkbox: 'このプロジェクトのデフォルトストレージの内容をコピーする' + copy_default_storage_checkbox: 'デフォルトストレージの内容をコピーする' copy_default_storage_disabled_hint: 'paths.yaml にカスタム設定があるため、ここでは切り替えできません。リセットすると既定のコピー設定に戻ります。' reset_paths_yaml: 'paths.yaml をリセット' reset_paths_yaml_confirm_title: 'paths.yaml をリセット' - reset_paths_yaml_confirm_body: '現在の paths.yaml を削除します。元に戻せません。よろしいですか?' + reset_paths_yaml_confirm_body: '現在の paths.yaml を削除します。この操作は元に戻せません。よろしいですか?' reset_paths_yaml_confirm_ok: 'リセット' post_build_placeholder: |- #!/bin/bash