feat(FR-3243): add SFTP resource group settings from the Resource Group page#8119
feat(FR-3243): add SFTP resource group settings from the Resource Group page#8119agatha197 wants to merge 1 commit into
Conversation
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has required the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
Coverage Report for react-coverage (./react)
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Pull request overview
Adds a superadmin-only UI flow on the Storage Proxy list page to view/update each proxy’s SFTP scaling groups (stored in etcd at volumes/proxies/{proxy}/sftp_scaling_groups) via signed REST calls to /config/{get,set,delete}, including i18n coverage across all supported locales.
Changes:
- Added a superadmin-only overflow menu on the Storage Proxy list page to open the new settings modal.
- Implemented
SFTPScalingGroupSettingModalto fetch/prefill and persist SFTP scaling group selections per proxy using signed requests. - Added a new
storageProxy.*i18n namespace across all 21 supported locales.
Reviewed changes
Copilot reviewed 23 out of 24 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| react/src/components/StorageProxyList.tsx | Adds superadmin-only overflow menu entry and mounts the new SFTP settings modal. |
| react/src/components/SFTPScalingGroupSettingModal.tsx | New modal to select proxy + resource groups and persist to etcd via signed REST endpoints. |
| react/src/generated/SFTPScalingGroupSettingModalStorageProxyQuery.graphql.ts | Generated Relay artifact for the modal’s proxy list query. |
| resources/i18n/en.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/ko.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/ja.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/zh-CN.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/zh-TW.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/de.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/el.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/es.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/fi.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/fr.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/id.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/it.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/mn.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/ms.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/pl.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/pt.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/pt-BR.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/ru.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/th.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/tr.json | Adds storageProxy i18n keys used by the new menu/modal. |
| resources/i18n/vi.json | Adds storageProxy i18n keys used by the new menu/modal. |
Files not reviewed (1)
- react/src/generated/SFTPScalingGroupSettingModalStorageProxyQuery.graphql.ts: Generated file
4e1d5b0 to
e04f66b
Compare
Coverage Report for backend-ai-ui-coverage (./packages/backend.ai-ui)
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||
e04f66b to
48ab376
Compare
|
Follow-up review pass (independent, on Sonnet) surfaced one real issue Copilot's round missed: in |
48ab376 to
0b02c9d
Compare
|
Ran a quality pass (reuse / simplification / efficiency / altitude, 4 independent reviewers) over the diff. Applied:
Deliberately not applied, flagged as a follow-up instead: two independent review angles (reuse + altitude) both pointed out that the raw |
c905a28 to
7b153c3
Compare
7b153c3 to
63403df
Compare
d066b58 to
4cb7246
Compare
4cb7246 to
1da13cc
Compare
1da13cc to
282ffae
Compare
282ffae to
1c76f44
Compare
…cker (#8169) Resolves #8168 (FR-3266) ## Problem The **Storybook Coverage Checker** agentic workflow (`storybook-coverage-checker`) fails its safe-output step on every run: > The `add_comment` safe output handler failed because the target was set to `*` (wildcard) but no specific item number (`item_number` / `issue_number` / `pull_request_number` / …) was provided — 0 successful, 1 failed. ## Root cause `storybook-check.yml` fires on `pull_request: ready_for_review` and then launches `storybook-coverage-checker` via `workflow_dispatch` (passing only `ref: branch`). Running as `workflow_dispatch`, the workflow has **no triggering PR context**, so it configures `safe-outputs.add-comment.target: "*"`. With `target: "*"`, gh-aw requires each `add_comment` output to carry the PR number (`item_number`). The prompt instead told the agent that `item_number` was **not** required, so the emitted comment had no target and the handler rejected it. ## Fix - `storybook-coverage-checker.md`: instruct the agent to remember the PR it identifies in Step 1 and pass it as `item_number` in the `add_comment` output; correct the misleading "not required" note in Important Notes. - Recompiled `storybook-coverage-checker.lock.yml` with `gh aw compile` (pinned **v0.77.5** to match the repo). The lock imports the prompt at runtime via `{{#runtime-import}}`, so the only change is the `body_hash` in the metadata header. ## Verification - `gh aw compile storybook-coverage-checker` → 0 errors, 0 warnings. - Lock diff = 1 line (`body_hash`); no compiler/version churn. Stacked on #8119. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1c76f44 to
e57f09a
Compare
Storybook Coverage ReportAnalysis Results| Component | Status | Details | Commands
Detailed ChangesBAIStorageProxySelect (Update)Component: Changes: The story has good basic coverage (5 props documented) but is missing several BAI-specific props and commonly-used Select props:
Currently documented props (5):
Note: The component extends Suggested argTypes additions: ghost: {
control: { type: 'boolean' },
description: 'Apply ghost style (transparent background)',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false' },
},
},
tooltip: {
control: { type: 'text' },
description: 'Tooltip text shown on hover',
table: {
type: { summary: 'string' },
defaultValue: { summary: "''" },
},
},
mode: {
control: { type: 'select' },
options: ['multiple', 'tags'],
description: 'Selection mode (already used in MultiSelect story)',
table: {
type: { summary: "'multiple' | 'tags'" },
},
},
value: {
control: { disable: true },
description: 'Controlled value',
table: {
type: { summary: 'any' },
},
},
defaultValue: {
control: { disable: true },
description: 'Default value for uncontrolled mode',
table: {
type: { summary: 'any' },
},
},
header: {
control: { disable: true },
description: 'Custom header content in dropdown',
table: {
type: { summary: 'ReactNode' },
},
},
footer: {
control: { disable: true },
description: 'Custom footer content in dropdown',
table: {
type: { summary: 'ReactNode' },
},
},
atBottomThreshold: {
control: { type: 'number' },
description: 'Threshold in pixels for bottom detection',
table: {
type: { summary: 'number' },
defaultValue: { summary: '30' },
},
},
atBottomStateChange: {
control: { disable: true },
description: 'Callback when scroll reaches bottom',
table: {
type: { summary: '(atBottom: boolean) => void' },
},
},
bottomLoading: {
control: { type: 'boolean' },
description: 'Show loading indicator at bottom',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false' },
},
},
endReached: {
control: { disable: true },
description: 'Callback when end is reached',
table: {
type: { summary: '() => void' },
},
},
searchAction: {
control: { disable: true },
description: 'Custom async search action',
table: {
type: { summary: '(value: string) => Promise<void>' },
},
},
|
e57f09a to
5696a18
Compare
…up page Resolves #8113 (FR-3243) Superadmins can now designate which storage proxy handles SFTP sessions for one or more resource groups (scaling groups), from the Resource Group list: - Multi-select resource-group rows via checkboxes -> a 'N selected' label + a gear (Settings) button appears (mirrors ProjectStoragePermissionTable). - The gear opens a modal listing the selected groups and a storage-proxy select (first proxy auto-selected). - On submit, the selection is UNIONED into the chosen proxy's existing etcd list at volumes/proxies/{proxy}/sftp_scaling_groups (add-only; etcd set overwrites, so the current value is read and merged rather than replaced). Persisted via a raw signed request to /config/get + /config/set (superadmin REST), since there is no GraphQL mutation for this etcd key. Also adds a reusable BAIStorageProxySelect component (+ Storybook) in backend.ai-ui. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5696a18 to
04e9975
Compare
| {baiClient.is_superadmin ? ( | ||
| isSftpMapLoading ? ( | ||
| <Form.Item label={t('storageProxy.SFTPStorageProxies')}> | ||
| <Skeleton.Input active block /> |
There was a problem hiding this comment.
| <Skeleton.Input active block /> | |
| <Select loading /> |
| // value/onChange binding) in Suspense: BAIStorageProxySelect's | ||
| // proxy-options query suspends. The prefill is registered as the | ||
| // Form.Item `initialValue`, computed from the now-resolved map. | ||
| <Suspense fallback={<Skeleton.Input active block />}> |
There was a problem hiding this comment.
| <Suspense fallback={<Skeleton.Input active block />}> | |
| <Suspense fallback={<Select loading />}> |

Resolves #8113 (FR-3243)
Summary
Lets superadmins designate which storage proxy handles SFTP sessions for resource groups (scaling groups), from the Resource Group management page. Two complementary entry points, both add/merge into the per-proxy etcd key
volumes/proxies/{proxy}/sftp_scaling_groups(comma-joined) without clobbering the other groups already on a proxy:1. Bulk (from the Resource Group list)
N selectedlabel + a gear Settings button appears (same pattern asProjectStoragePermissionTable).2. Per group (from the Resource Group create/edit modal)
vfolder.list_hosts()— each host id isproxy:volumeand carriessftp_scaling_groups).Implementation
useSFTPScalingGroupshook centralizes the etcd read-merge-write:addGroupsToProxies(add-only union),syncGroupProxies(per-group add/remove delta), andgetProxyScalingGroups(prefill source).Setting.set/getforce-prefixconfig/, so writes use a raw superadmin signed request to/config/get+/config/set— the exact location the manager CLI (backend.ai mgr etcd set-storage-sftp-scaling-group) writes to.BAIStorageProxySelectinbackend.ai-ui(+ Storybook), deriving distinct proxy names fromstorage_volume_list(with aTODO(needs-backend)for a dedicated field).storageProxyi18n namespace across all 21 locales.Verification
Checklist:
/config/{get,set}REST (superadmin), no schema change