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