Skip to content

feat(FR-3243): add SFTP resource group settings from the Resource Group page#8119

Open
agatha197 wants to merge 1 commit into
mainfrom
fix-FR-3243-sftp-scaling-group
Open

feat(FR-3243): add SFTP resource group settings from the Resource Group page#8119
agatha197 wants to merge 1 commit into
mainfrom
fix-FR-3243-sftp-scaling-group

Conversation

@agatha197

@agatha197 agatha197 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

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)

  • Checkbox multi-select resource-group rows (superadmin only) → a N selected label + a gear Settings button appears (same pattern as ProjectStoragePermissionTable).
  • The gear opens a modal that lists the selected groups (tags) and a multi-select storage proxy field.
  • On submit, the selected groups are union-added into each chosen proxy's existing list. Add-only.

2. Per group (from the Resource Group create/edit modal)

  • New superadmin-only SFTP Storage Proxies multi-select, prefilled with the proxies that currently handle this group (derived from vfolder.list_hosts() — each host id is proxy:volume and carries sftp_scaling_groups).
  • On save, this group's membership is synced to the selected proxies: added to newly-selected proxies, removed from deselected ones — and only this group is touched, every other group on each proxy is preserved.

Implementation

  • Shared useSFTPScalingGroups hook centralizes the etcd read-merge-write: addGroupsToProxies (add-only union), syncGroupProxies (per-group add/remove delta), and getProxyScalingGroups (prefill source).
  • No GraphQL mutation exists for this etcd key, and Setting.set/get force-prefix config/, 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.
  • New reusable BAIStorageProxySelect in backend.ai-ui (+ Storybook), deriving distinct proxy names from storage_volume_list (with a TODO(needs-backend) for a dedicated field).
  • New storageProxy i18n namespace across all 21 locales.

Verification

Relay / Lint / Format / TypeScript / Vite warmup: PASS
Terminology: WARN-ONLY (1 pre-existing, unrelated key)

Checklist:

  • Documentation
  • Minimum required manager version — none; uses existing /config/{get,set} REST (superadmin), no schema change
  • Minimum requirements to check during review — confirm both flows preserve other groups on a proxy (union / delta), and the per-group sync only removes this group from deselected proxies
  • Test case(s) to demonstrate the difference of before/after

@github-actions github-actions Bot added area:ux UI / UX issue. area:i18n Localization labels Jul 2, 2026
@agatha197 agatha197 marked this pull request as ready for review July 2, 2026 06:47
Copilot AI review requested due to automatic review settings July 2, 2026 06:47
@github-actions github-actions Bot added the size:XL 500~ LoC label Jul 2, 2026

agatha197 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • flow:merge-queue - adds this PR to the back of the merge queue
  • flow:hotfix - for urgent changes, fast-track this PR to the front of 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.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for react-coverage (./react)

Status Category Percentage Covered / Total
🔵 Lines 6.83% 2021 / 29567
🔵 Statements 5.57% 2285 / 40960
🔵 Functions 5.79% 332 / 5728
🔵 Branches 3.7% 1433 / 38640
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
react/src/components/InviteFolderSettingModal.tsx 0% 0% 0% 0% 47-295
react/src/components/ResourceGroupList.tsx 0% 0% 0% 0% 64-433
react/src/components/ResourceGroupSettingModal.tsx 0% 0% 0% 0% 68-327
react/src/components/SFTPResourceGroupSettingModal.tsx 0% 0% 0% 0% 32-140
react/src/components/StorageProxyList.tsx 0% 0% 0% 0% 34-233
react/src/helper/index.tsx 63.88% 67.02% 51.56% 64.56% 42-57, 127, 134, 147, 292, 326-328, 332-338, 348-353, 358, 377, 388, 506-552, 562-600, 669, 674, 687, 688, 744-746, 754, 760, 771, 809, 826-939, 973-987
react/src/hooks/useSFTPResourceGroups.tsx 0% 0% 0% 0% 12-121
Generated in workflow #2699 for commit 04e9975 by the Vitest Coverage Report Action

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 SFTPScalingGroupSettingModal to 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

Comment thread react/src/components/SFTPScalingGroupSettingModal.tsx Outdated
Comment thread react/src/components/SFTPScalingGroupSettingModal.tsx
Comment thread react/src/components/SFTPScalingGroupSettingModal.tsx Outdated
@agatha197 agatha197 marked this pull request as draft July 2, 2026 06:55
@agatha197 agatha197 force-pushed the fix-FR-3243-sftp-scaling-group branch from 4e1d5b0 to e04f66b Compare July 2, 2026 07:15
@agatha197 agatha197 marked this pull request as ready for review July 2, 2026 07:16
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for backend-ai-ui-coverage (./packages/backend.ai-ui)

Status Category Percentage Covered / Total
🔵 Lines 11.56% 607 / 5250
🔵 Statements 9.79% 680 / 6942
🔵 Functions 12.85% 151 / 1175
🔵 Branches 8.18% 558 / 6814
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/backend.ai-ui/src/components/fragments/BAIStorageProxySelect.tsx 0% 0% 0% 0% 17-43
packages/backend.ai-ui/src/components/fragments/index.ts 100% 100% 100% 100%
Generated in workflow #2699 for commit 04e9975 by the Vitest Coverage Report Action

@agatha197 agatha197 force-pushed the fix-FR-3243-sftp-scaling-group branch from e04f66b to 48ab376 Compare July 2, 2026 07:30
@agatha197

Copy link
Copy Markdown
Contributor Author

Follow-up review pass (independent, on Sonnet) surfaced one real issue Copilot's round missed: in handleProxyChange's /config/get .catch(), any genuine request failure (network/permission/5xx) was silently treated the same as "key doesn't exist yet" and reset the resource-groups field to empty with no indication to the user — a subsequent Save would then fire /config/delete and wipe a real, previously-configured setting. Fixed: the catch now logs via useBAILogger and shows message.error(...) so the failure is visible before the user can accidentally submit over it. Also added the missing 'use memo' directive to StorageProxyList per the React Compiler convention, since this PR substantially expands its state/logic.

@agatha197 agatha197 marked this pull request as draft July 2, 2026 07:31
@agatha197 agatha197 marked this pull request as ready for review July 2, 2026 07:34
@agatha197 agatha197 force-pushed the fix-FR-3243-sftp-scaling-group branch from 48ab376 to 0b02c9d Compare July 2, 2026 07:43
@agatha197

Copy link
Copy Markdown
Contributor Author

Ran a quality pass (reuse / simplification / efficiency / altitude, 4 independent reviewers) over the diff. Applied:

  • Hoisted etcdKey out of the component body (was redefined every render for no reason).
  • Deduped the repeated form.getFieldValue('proxy') !== proxy staleness check in handleProxyChange into one local isStale() helper shared by the .then/.catch branches.
  • BAIStorageProxySelect.tsx: replaced the hand-written _.filter(..., (proxy): proxy is string => !!proxy) type-guard with _.compact, matching the established _.compact(_.map(...)) idiom already used elsewhere in this package (e.g. BAIProjectBulkEditModal.tsx).
  • Added a TODO(needs-backend, FR-3243) marker on the volume→proxy derivation workaround, so a future dedicated proxy-list field is a one-line swap.
  • Added a per-proxy in-session cache (useRef) in the modal so toggling back and forth between already-visited proxies doesn't re-issue a /config/get request.

Deliberately not applied, flagged as a follow-up instead: two independent review angles (reuse + altitude) both pointed out that the raw useBaiSignedRequestWithPromise + hardcoded /config/get|set|delete calls in the modal reimplement the pattern the existing Setting/Registry classes in src/lib/backend.ai-client-node.ts already establish for other etcd-backed settings modals (OverlayNetworkSettingModal.tsx, SchedulerSettingModal.tsx, ConfigurationsSettingList.tsx), and suggested adding typed methods to the existing StorageProxy client class instead. This is architecturally sound, but touches a different workspace package (packages/backend.ai-client) with different TS strictness settings that this PR's diff and verification never covered — treating it as out of scope for this PR rather than editing an unverified file. Worth a small follow-up PR.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 46 out of 47 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • packages/backend.ai-ui/src/generated/BAIStorageProxySelectQuery.graphql.ts: Generated file

Comment thread react/src/components/SFTPScalingGroupSettingModal.tsx Outdated
@agatha197 agatha197 force-pushed the fix-FR-3243-sftp-scaling-group branch 3 times, most recently from c905a28 to 7b153c3 Compare July 2, 2026 08:39
@agatha197 agatha197 marked this pull request as draft July 3, 2026 04:35
@agatha197 agatha197 force-pushed the fix-FR-3243-sftp-scaling-group branch from 7b153c3 to 63403df Compare July 3, 2026 04:59
@agatha197 agatha197 changed the title feat(FR-3243): add SFTP scaling group settings to Storage Proxy page feat(FR-3243): add SFTP resource group settings from the Resource Group page Jul 3, 2026
@agatha197 agatha197 force-pushed the fix-FR-3243-sftp-scaling-group branch 6 times, most recently from d066b58 to 4cb7246 Compare July 3, 2026 10:29
@agatha197 agatha197 marked this pull request as ready for review July 3, 2026 10:48
@agatha197 agatha197 force-pushed the fix-FR-3243-sftp-scaling-group branch from 4cb7246 to 1da13cc Compare July 3, 2026 10:56
Comment thread react/src/hooks/useSFTPScalingGroups.tsx Outdated
@agatha197 agatha197 force-pushed the fix-FR-3243-sftp-scaling-group branch from 1da13cc to 282ffae Compare July 6, 2026 07:50
@agatha197 agatha197 requested a review from nowgnuesLee July 6, 2026 07:51
@agatha197 agatha197 marked this pull request as draft July 6, 2026 07:52
@agatha197 agatha197 force-pushed the fix-FR-3243-sftp-scaling-group branch from 282ffae to 1c76f44 Compare July 6, 2026 15:20
@agatha197 agatha197 marked this pull request as ready for review July 6, 2026 15:20
graphite-app Bot pushed a commit that referenced this pull request Jul 7, 2026
…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)
@agatha197 agatha197 marked this pull request as draft July 7, 2026 02:50
@agatha197 agatha197 force-pushed the fix-FR-3243-sftp-scaling-group branch from 1c76f44 to e57f09a Compare July 7, 2026 03:15
@agatha197 agatha197 marked this pull request as ready for review July 7, 2026 03:16
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Storybook Coverage Report

Analysis Results

| Component | Status | Details |
|-----------|--------|---------||
| BAIStorageProxySelect | Update | +ghost, +tooltip, +header, +footer, +mode, +value, +defaultValue, +atBottomThreshold, +atBottomStateChange, +bottomLoading, +endReached, +searchAction |

Commands

@claude /manage-bui-component-story BAIStorageProxySelect

Detailed Changes

BAIStorageProxySelect (Update)

Component: packages/backend.ai-ui/src/components/fragments/BAIStorageProxySelect.tsx
Story: packages/backend.ai-ui/src/components/fragments/BAIStorageProxySelect.stories.tsx

Changes:

The story has good basic coverage (5 props documented) but is missing several BAI-specific props and commonly-used Select props:

Change Prop Type Description
+ Added ghost boolean Ghost style (transparent background)
+ Added tooltip string Tooltip text for the select
+ Added header ReactNode Custom header content in dropdown
+ Added footer ReactNode Custom footer content in dropdown
+ Added mode 'multiple' | 'tags' Selection mode (used in stories but not in argTypes)
+ Added value any Controlled value
+ Added defaultValue any Default value
+ Added atBottomThreshold number Threshold for bottom detection
+ Added atBottomStateChange (atBottom: boolean) => void Callback when scroll reaches bottom
+ Added bottomLoading boolean Show loading at bottom
+ Added endReached () => void Callback when end is reached
+ Added searchAction (value: string) => Promise<void> Custom search action

Currently documented props (5):

  • placeholder - Placeholder text
  • autoSelectOption - Auto-select first option
  • disabled - Disabled state
  • allowClear - Show clear button
  • onChange - Change callback

Note: The component extends Omit<BAISelectProps, 'options'>, inheriting all BAISelect props except options (which is generated from GraphQL data).

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>' },
  },
},

Storybook guidelines

AI generated by Storybook Coverage Checker

Generated by Storybook Coverage Checker · sonnet45 1.4M ·

@agatha197 agatha197 requested a review from yomybaby July 8, 2026 05:40
@agatha197 agatha197 force-pushed the fix-FR-3243-sftp-scaling-group branch from e57f09a to 5696a18 Compare July 9, 2026 10:06
…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>
@agatha197 agatha197 force-pushed the fix-FR-3243-sftp-scaling-group branch from 5696a18 to 04e9975 Compare July 9, 2026 10:15
{baiClient.is_superadmin ? (
isSftpMapLoading ? (
<Form.Item label={t('storageProxy.SFTPStorageProxies')}>
<Skeleton.Input active block />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<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 />}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<Suspense fallback={<Skeleton.Input active block />}>
<Suspense fallback={<Select loading />}>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add SFTP scaling group settings to Storage Proxy page

3 participants