Skip to content

feat: add Application grouping to feature flags#1

Merged
rochar merged 1 commit into
rochar:mainfrom
rrocha-atriis:main
Mar 5, 2026
Merged

feat: add Application grouping to feature flags#1
rochar merged 1 commit into
rochar:mainfrom
rrocha-atriis:main

Conversation

@rrocha-atriis

Copy link
Copy Markdown
Contributor

Add an Application field to feature flags allowing them to be grouped by application name. Existing flags default to 'Default'.

Backend:

  • Add Application column to FeatureFlag entity with EF Core migration
  • Update all CRUD endpoints to include Application
  • Add GET /dashboardapi/applications endpoint for distinct app names
  • Update ManagedFeature and registration APIs to accept application

UI:

  • Add sidebar listing applications with feature counts
  • Add application filter on feature list
  • Add application selector in Add Feature modal
  • Add inline Add Application button in sidebar

Add an Application field to feature flags allowing them to be grouped
by application name. Existing flags default to 'Default'.

Backend:
- Add Application column to FeatureFlag entity with EF Core migration
- Update all CRUD endpoints to include Application
- Add GET /dashboardapi/applications endpoint for distinct app names
- Update ManagedFeature and registration APIs to accept application

UI:
- Add sidebar listing applications with feature counts
- Add application filter on feature list
- Add application selector in Add Feature modal
- Add inline Add Application button in sidebar

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 5, 2026 17:07
@rochar
rochar merged commit c3df8d3 into rochar:main Mar 5, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds an Application grouping dimension to feature flags across backend + UI so flags can be filtered/organized by application, with legacy flags defaulting to "Default".

Changes:

  • Backend: persisted Application on FeatureFlag, wired through DTOs/endpoints, plus a new /dashboardapi/applications endpoint for distinct application names
  • UI: added an Applications sidebar + application filtering, and an Application selector in the “Add Feature” modal
  • Styling: widened main container and added sidebar/layout CSS

Reviewed changes

Copilot reviewed 16 out of 19 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
Stella.FeatureManagement.Dashboard/wwwroot/assets/index.css Updates built/minified dashboard CSS to include new sidebar/layout styling
Stella.FeatureManagement.Dashboard/Services/IDashboardInitializer.cs Extends registration API to accept application with default
Stella.FeatureManagement.Dashboard/Services/DashboardInitializer.cs Persists Application when registering features
Stella.FeatureManagement.Dashboard/IFeatureManagerDashboardAppBuilder.cs Extends public builder + ManagedFeature record to carry Application
Stella.FeatureManagement.Dashboard/FeatureManagerDashboardAppBuilder.cs Maps new applications endpoint and passes Application through registration
Stella.FeatureManagement.Dashboard/FeatureFlagDto.cs Adds Application to DTO contract
Stella.FeatureManagement.Dashboard/EndPoints/PutFeaturesExtension.cs Allows updating the Application on PUT and returns it in responses
Stella.FeatureManagement.Dashboard/EndPoints/PostFeaturesExtension.cs Accepts Application on create and returns it in responses
Stella.FeatureManagement.Dashboard/EndPoints/GetFeaturesExtension.cs Includes Application in GET responses
Stella.FeatureManagement.Dashboard/EndPoints/GetApplicationsExtension.cs New endpoint returning distinct application names
Stella.FeatureManagement.Dashboard/Data/Migrations/FeatureFlagDbContextModelSnapshot.cs Snapshot updated for new Application column
Stella.FeatureManagement.Dashboard/Data/Migrations/20260305153101_AddFeatureFlagApplication.cs New EF migration adding Application column with default
Stella.FeatureManagement.Dashboard/Data/FeatureFlagDbContext.cs Configures Application column constraints/default
Stella.FeatureManagement.Dashboard/Data/FeatureFlag.cs Adds Application property to entity (default "Default")
Stella.FeatureManagement.Dashboard.UI/src/index.css Adds sidebar/layout styles and widens container
Stella.FeatureManagement.Dashboard.UI/src/App.tsx Adds application fetching, sidebar filtering, and application selection on create
Files not reviewed (2)
  • Stella.FeatureManagement.Dashboard.UI/package-lock.json: Language not supported
  • Stella.FeatureManagement.Dashboard/Data/Migrations/20260305153101_AddFeatureFlagApplication.Designer.cs: Language not supported
Comments suppressed due to low confidence (1)

Stella.FeatureManagement.Dashboard/wwwroot/assets/index.css:1

  • The built/minified CSS file appears to include an accidental 1 | prefix at the start of the stylesheet (1 |*{...}), which makes the CSS invalid and can prevent styles from applying. Regenerate the asset (or remove the stray prefix) so the file begins with the expected selector (e.g., *{...}) without embedded line-number/diff artifacts.
*{box-sizing:border-box;margin:0;padding:0}:root{--color-primary: #6366f1;--color-primary-dark: #4f46e5;--color-success: #10b981;--color-success-bg: #d1fae5;--color-danger: #ef4444;--color-danger-bg: #fee2e2;--color-text: #1f2937;--color-text-secondary: #6b7280;--color-bg: #f8fafc;--color-card: #ffffff;--color-border: #e5e7eb;--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / .05);--shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--shadow-md: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--radius: 12px;--radius-sm: 8px}body{font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background:var(--color-bg);color:var(--color-text);line-height:1.6;min-height:100vh}.container{max-width:1200px;margin:0 auto;padding:32px 24px}.header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:32px}.header-content{flex:1}.header-title{display:flex;align-items:center;gap:12px;margin-bottom:4px}.header-icon{width:32px;height:32px;color:var(--color-primary)}h1{font-size:1.75rem;font-weight:700;color:var(--color-text);letter-spacing:-.025em}.header-subtitle{color:var(--color-text-secondary);font-size:.95rem}.refresh-btn{display:flex;align-items:center;justify-content:center;width:44px;height:44px;background:var(--color-card);border:1px solid var(--color-border);border-radius:var(--radius-sm);cursor:pointer;transition:all .2s ease;color:var(--color-text-secondary)}.refresh-btn:hover:not(:disabled){background:var(--color-primary);border-color:var(--color-primary);color:#fff;box-shadow:var(--shadow-md)}.refresh-btn:disabled{opacity:.6;cursor:not-allowed}.refresh-icon{width:20px;height:20px}.refresh-icon.spinning{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.stats-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:24px}.stat-card{background:var(--color-card);border-radius:var(--radius);padding:20px;text-align:center;box-shadow:var(--shadow);border:1px solid var(--color-border);transition:transform .2s ease,box-shadow .2s ease}.stat-card:hover{transform:translateY(-2px);box-shadow:var(--shadow-md)}.stat-value{font-size:2rem;font-weight:700;color:var(--color-text);line-height:1.2}.stat-label{font-size:.875rem;color:var(--color-text-secondary);margin-top:4px}.stat-enabled .stat-value{color:var(--color-success)}.stat-disabled .stat-value{color:#6b7280}.header-actions{display:flex;gap:8px}.add-btn{display:flex;align-items:center;justify-content:center;gap:6px;padding:0 16px;height:44px;background:var(--color-primary);border:none;border-radius:var(--radius-sm);cursor:pointer;transition:all .2s ease;color:#fff;font-size:.9rem;font-weight:500}.add-btn:hover{background:var(--color-primary-dark);box-shadow:var(--shadow-md)}.add-btn svg{width:18px;height:18px}.modal-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.modal{background:var(--color-card);border-radius:var(--radius);box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;width:100%;max-width:420px;margin:16px;animation:slideUp .2s ease}@keyframes slideUp{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.modal-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid var(--color-border)}.modal-header h2{font-size:1.125rem;font-weight:600;color:var(--color-text)}.modal-close{display:flex;align-items:center;justify-content:center;width:32px;height:32px;background:transparent;border:none;border-radius:6px;cursor:pointer;color:var(--color-text-secondary);transition:all .2s ease}.modal-close:hover{background:var(--color-bg);color:var(--color-text)}.modal-close svg{width:18px;height:18px}.modal-body{padding:24px}.modal-label{display:block;font-size:.875rem;font-weight:500;color:var(--color-text);margin-bottom:8px}.modal-input{width:100%;padding:12px 14px;font-size:1rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-card);color:var(--color-text);transition:border-color .2s ease,box-shadow .2s ease}.modal-input:focus{outline:none;border-color:var(--color-primary);box-shadow:0 0 0 3px #6366f11a}.modal-input::placeholder{color:var(--color-text-secondary)}.modal-input:disabled{opacity:.6;cursor:not-allowed}.modal-footer{display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid var(--color-border);background:var(--color-bg);border-radius:0 0 var(--radius) var(--radius)}.modal-btn{display:flex;align-items:center;justify-content:center;padding:10px 18px;font-size:.9rem;font-weight:500;border:none;border-radius:var(--radius-sm);cursor:pointer;transition:all .2s ease;min-width:90px}.modal-btn-cancel{background:var(--color-card);color:var(--color-text);border:1px solid var(--color-border)}.modal-btn-cancel:hover:not(:disabled){background:var(--color-bg)}.modal-btn-primary{background:var(--color-primary);color:#fff}.modal-btn-primary:hover:not(:disabled){background:var(--color-primary-dark)}.modal-btn:disabled{opacity:.5;cursor:not-allowed}.modal-btn-danger{background:var(--color-danger);color:#fff}.modal-btn-danger:hover:not(:disabled){background:#dc2626}.modal-sm{max-width:380px}.delete-confirm-text{color:var(--color-text-secondary);line-height:1.6}.delete-confirm-text strong{color:var(--color-text)}.btn-loading{width:18px;height:18px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .8s linear infinite}.btn-loading-danger{border-color:#ffffff4d;border-top-color:#fff}.search-container{position:relative;margin-bottom:24px}.search-icon{position:absolute;left:16px;top:50%;transform:translateY(-50%);width:20px;height:20px;color:var(--color-text-secondary);pointer-events:none}.search-input{width:100%;padding:14px 48px;font-size:.95rem;border:1px solid var(--color-border);border-radius:var(--radius);background:var(--color-card);color:var(--color-text);transition:all .2s ease;box-shadow:var(--shadow-sm)}.search-input:focus{outline:none;border-color:var(--color-primary);box-shadow:0 0 0 3px #6366f11a}.search-input::placeholder{color:var(--color-text-secondary)}.search-clear{position:absolute;right:12px;top:50%;transform:translateY(-50%);width:28px;height:28px;display:flex;align-items:center;justify-content:center;background:var(--color-bg);border:none;border-radius:50%;cursor:pointer;color:var(--color-text-secondary);transition:all .15s ease}.search-clear:hover{background:var(--color-border);color:var(--color-text)}.search-clear svg{width:16px;height:16px}.feature-list{background:var(--color-card);border-radius:var(--radius);box-shadow:var(--shadow);border:1px solid var(--color-border);overflow:hidden}.feature-item{border-bottom:1px solid var(--color-border);transition:background-color .15s ease}.feature-item:last-child{border-bottom:none}.feature-item:hover,.feature-item.expanded{background-color:var(--color-bg)}.feature-row{display:flex;align-items:center;gap:12px;padding:16px 20px;cursor:pointer}.expand-icon{width:20px;height:20px;color:var(--color-text-secondary);flex-shrink:0;transition:transform .2s ease}.expand-icon.rotated{transform:rotate(180deg)}.filter-badge{display:inline-flex;align-items:center;gap:4px;margin-left:8px;padding:2px 8px;background:var(--color-primary);color:#fff;border-radius:12px;font-size:.7rem;font-weight:600;vertical-align:middle}.filter-badge svg{width:12px;height:12px}.feature-details{padding:0 20px 16px 64px;animation:slideDown .2s ease}@keyframes slideDown{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.no-filters{color:var(--color-text-secondary);font-size:.875rem;font-style:italic}.no-filters-container{display:flex;align-items:center;justify-content:space-between;gap:16px}.add-filter-btn{display:inline-flex;align-items:center;gap:6px;padding:8px 14px;background:transparent;border:1px dashed var(--color-border);border-radius:var(--radius-sm);color:var(--color-text-secondary);font-size:.8rem;font-weight:500;cursor:pointer;transition:all .15s ease}.add-filter-btn:hover{background:var(--color-primary);border-color:var(--color-primary);border-style:solid;color:#fff}.add-filter-btn svg{width:16px;height:16px}.add-filter-btn-inline{margin-top:12px}.modal-select{width:100%;padding:12px 40px 12px 14px;font-size:1rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-card);color:var(--color-text);transition:border-color .2s ease,box-shadow .2s ease;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center}.modal-select:focus{outline:none;border-color:var(--color-primary);box-shadow:0 0 0 3px #6366f11a}.modal-select:disabled{opacity:.6;cursor:not-allowed}.modal-subtitle{color:var(--color-text-secondary);font-size:.9rem;margin-bottom:16px}.modal-subtitle strong{color:var(--color-text)}.filters-section h4{font-size:.8rem;font-weight:600;color:var(--color-text-secondary);text-transform:uppercase;letter-spacing:.05em;margin-bottom:12px}.filter-item{background:var(--color-card);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:12px;margin-bottom:8px}.filter-item:last-child{margin-bottom:0}.filter-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.filter-type{font-size:.875rem;font-weight:600;color:var(--color-primary)}.filter-actions{display:flex;align-items:center;gap:6px}.edit-filter-btn,.delete-filter-btn{display:flex;align-items:center;justify-content:center;width:28px;height:28px;background:transparent;border:1px solid var(--color-border);border-radius:6px;cursor:pointer;color:var(--color-text-secondary);transition:all .15s ease}.edit-filter-btn:hover:not(:disabled){background:var(--color-primary);border-color:var(--color-primary);color:#fff}.delete-filter-btn:hover:not(:disabled){background:var(--color-danger);border-color:var(--color-danger);color:#fff}.edit-filter-btn:disabled,.delete-filter-btn:disabled{opacity:.5;cursor:not-allowed}.edit-filter-btn svg,.delete-filter-btn svg{width:14px;height:14px}.btn-loading-small{width:14px;height:14px;border:2px solid rgba(107,114,128,.3);border-top-color:var(--color-text-secondary);border-radius:50%;animation:spin .8s linear infinite}.filter-params{background:#1e293b;color:#e2e8f0;padding:12px;border-radius:6px;font-family:Fira Code,Cascadia Code,Consolas,monospace;font-size:.8rem;overflow-x:auto;margin:0;line-height:1.5}.filter-edit{display:flex;flex-direction:column;gap:8px}.filter-params-input{width:100%;padding:12px;background:#1e293b;color:#e2e8f0;border:2px solid var(--color-border);border-radius:6px;font-family:Fira Code,Cascadia Code,Consolas,monospace;font-size:.8rem;line-height:1.5;resize:vertical;min-height:100px}.filter-params-input:focus{outline:none;border-color:var(--color-primary)}.filter-params-input.error{border-color:var(--color-danger)}.filter-params-input:disabled{opacity:.6}.json-error{color:var(--color-danger);font-size:.75rem;font-weight:500}.filter-edit-actions{display:flex;justify-content:flex-end;gap:8px}.filter-btn{display:flex;align-items:center;justify-content:center;padding:6px 14px;font-size:.8rem;font-weight:500;border:none;border-radius:6px;cursor:pointer;transition:all .15s ease;min-width:70px}.filter-btn-cancel{background:var(--color-card);color:var(--color-text);border:1px solid var(--color-border)}.filter-btn-cancel:hover:not(:disabled){background:var(--color-bg)}.filter-btn-save{background:var(--color-primary);color:#fff}.filter-btn-save:hover:not(:disabled){background:var(--color-primary-dark)}.filter-btn:disabled{opacity:.5;cursor:not-allowed}.feature-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.feature-name{font-weight:600;font-size:.95rem;color:var(--color-text);word-break:break-word}.feature-description{font-size:.8rem;color:var(--color-text-secondary);font-weight:400;line-height:1.4}.delete-btn{display:flex;align-items:center;justify-content:center;width:32px;height:32px;background:transparent;border:none;border-radius:6px;cursor:pointer;color:var(--color-text-secondary);opacity:.35;transition:all .15s ease;flex-shrink:0}.feature-item:hover .delete-btn{opacity:1}.delete-btn:hover{background:var(--color-danger-bg);color:var(--color-danger)}.delete-btn svg{width:18px;height:18px}.status-badge{display:inline-flex;align-items:center;gap:8px;font-weight:600;font-size:.8rem;padding:6px 14px;border-radius:20px;text-transform:uppercase;letter-spacing:.025em;flex-shrink:0}.status-dot{width:8px;height:8px;border-radius:50%}.status-badge.enabled{color:#065f46;background-color:var(--color-success-bg)}.status-badge.enabled .status-dot{background-color:var(--color-success);box-shadow:0 0 0 3px #10b98133}.status-badge.disabled{color:#991b1b;background-color:var(--color-danger-bg)}.status-badge.disabled .status-dot{background-color:var(--color-danger);box-shadow:0 0 0 3px #ef444433}.toggle-switch{display:inline-flex;align-items:center;gap:10px;padding:4px;border:none;border-radius:8px;cursor:pointer;background:transparent;transition:background-color .2s ease;flex-shrink:0}.toggle-switch:hover{background-color:var(--color-bg)}.toggle-track{position:relative;width:44px;height:24px;border-radius:12px;transition:background-color .2s ease}.toggle-thumb{position:absolute;top:2px;left:0;width:20px;height:20px;background-color:#fff;border-radius:50%;box-shadow:0 1px 3px #0003;transition:transform .2s ease}.toggle-switch.enabled .toggle-track{background-color:var(--color-success)}.toggle-switch.enabled .toggle-thumb{transform:translate(22px)}.toggle-switch.disabled .toggle-track{background-color:#9ca3af}.toggle-switch.disabled .toggle-thumb{transform:translate(2px)}.toggle-text{font-weight:600;font-size:.8rem;min-width:60px;text-align:left}.toggle-switch.enabled .toggle-text{color:#065f46}.toggle-switch.disabled .toggle-text{color:#6b7280}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:64px 24px;background:var(--color-card);border-radius:var(--radius);box-shadow:var(--shadow);border:1px solid var(--color-border)}.loading-spinner{width:40px;height:40px;border:3px solid var(--color-border);border-top-color:var(--color-primary);border-radius:50%;animation:spin .8s linear infinite;margin-bottom:16px}.loading-container p{color:var(--color-text-secondary)}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:64px 24px;color:var(--color-text-secondary)}.empty-icon{width:48px;height:48px;margin-bottom:16px;opacity:.5}.error-banner{display:flex;align-items:center;gap:12px;padding:16px 20px;background:var(--color-danger-bg);border:1px solid rgba(239,68,68,.3);border-radius:var(--radius);margin-bottom:24px;color:#991b1b}.error-icon{width:20px;height:20px;flex-shrink:0}.error-banner span{flex:1;font-size:.95rem}.error-retry{padding:8px 16px;background:var(--color-danger);color:#fff;border:none;border-radius:var(--radius-sm);font-weight:600;font-size:.875rem;cursor:pointer;transition:background-color .15s ease}.error-retry:hover{background:#dc2626}.modal-header-error{background:transparent;border-bottom:none;padding:16px 20px 0}.modal-header-error h2{font-size:.875rem;font-weight:600;color:var(--color-danger);text-transform:uppercase;letter-spacing:.05em}.error-modal-content{display:flex;flex-direction:column;align-items:center;text-align:center;gap:12px;padding:8px 0}.error-modal-icon{width:40px;height:40px;color:var(--color-danger);opacity:.8}.error-modal-content p{color:var(--color-text);font-size:.95rem;line-height:1.5;word-break:break-word}.footer{display:flex;justify-content:space-between;align-items:center;padding:24px 0;color:var(--color-text-secondary);font-size:.8rem}.footer-updated{opacity:.7}.footer-brand-container{display:flex;align-items:center;gap:12px}.footer-version{opacity:.5;font-size:.8rem;background:var(--color-bg)}.footer-brand{opacity:.4;font-size:.75rem;letter-spacing:.02em;color:var(--color-text-secondary);text-decoration:underline;transition:opacity .15s ease}.footer-brand:hover{opacity:.7}@media (max-width: 640px){.container{padding:20px 16px}.stats-grid{grid-template-columns:1fr}.header{flex-direction:column;gap:16px}.refresh-btn{align-self:flex-start}.feature-item{flex-direction:column;align-items:flex-start;gap:12px}.dashboard-layout{flex-direction:column}.app-sidebar{width:100%;min-width:unset}}.dashboard-layout{display:flex;gap:24px}.dashboard-content{flex:1;min-width:0}.app-sidebar{width:220px;min-width:220px;background:var(--color-card);border:1px solid var(--color-border);border-radius:var(--radius);box-shadow:var(--shadow);padding:0;align-self:flex-start;position:sticky;top:24px}.sidebar-header{display:flex;align-items:center;gap:8px;padding:16px 16px 12px;font-size:.8rem;font-weight:600;color:var(--color-text-secondary);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--color-border)}.sidebar-icon{width:16px;height:16px;opacity:.6}.sidebar-list{list-style:none;padding:8px}.sidebar-item{display:flex;align-items:center;justify-content:space-between;width:100%;padding:8px 12px;border:none;background:transparent;border-radius:var(--radius-sm);cursor:pointer;font-size:.9rem;color:var(--color-text);transition:background-color .15s ease}.sidebar-item:hover{background:var(--color-bg)}.sidebar-item.active{background:var(--color-primary);color:#fff}.sidebar-item.active .sidebar-item-count{background:#fff3;color:#fff}.sidebar-item-name{font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sidebar-item-count{font-size:.75rem;font-weight:600;background:var(--color-bg);color:var(--color-text-secondary);padding:2px 8px;border-radius:12px;flex-shrink:0}.sidebar-add-app{padding:8px;border-top:1px solid var(--color-border)}.sidebar-add-btn{display:flex;align-items:center;gap:6px;width:100%;padding:8px 12px;border:1px dashed var(--color-border);background:transparent;border-radius:var(--radius-sm);cursor:pointer;font-size:.85rem;color:var(--color-text-secondary);transition:all .15s ease}.sidebar-add-btn:hover{border-color:var(--color-primary);color:var(--color-primary);background:#6366f10d}.sidebar-add-btn svg{width:14px;height:14px}.sidebar-add-form{display:flex;flex-direction:column;gap:6px}.sidebar-add-input{width:100%;padding:6px 10px;border:1px solid var(--color-border);border-radius:var(--radius-sm);font-size:.85rem;color:var(--color-text);background:var(--color-card);outline:none;transition:border-color .15s ease}.sidebar-add-input:focus{border-color:var(--color-primary)}.sidebar-add-actions{display:flex;gap:4px;justify-content:flex-end}.sidebar-add-confirm,.sidebar-add-cancel{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:var(--radius-sm);cursor:pointer;transition:all .15s ease}.sidebar-add-confirm{background:var(--color-primary);color:#fff}.sidebar-add-confirm:hover:not(:disabled){background:var(--color-primary-dark)}.sidebar-add-confirm:disabled{opacity:.4;cursor:not-allowed}.sidebar-add-cancel{background:var(--color-bg);color:var(--color-text-secondary)}.sidebar-add-cancel:hover{background:var(--color-border)}.sidebar-add-confirm svg,.sidebar-add-cancel svg{width:14px;height:14px}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +592 to +605
{applications.map(app => {
const count = features.filter(f => f.application === app).length
return (
<li key={app}>
<button
className={`sidebar-item ${selectedApplication === app ? 'active' : ''}`}
onClick={() => setSelectedApplication(app)}
>
<span className="sidebar-item-name">{app}</span>
<span className="sidebar-item-count">{count}</span>
</button>
</li>
)
})}

Copilot AI Mar 5, 2026

Copy link

Choose a reason for hiding this comment

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

Computing count via features.filter(...).length inside the render loop makes this O(applications × features) every render. Precompute a Map<string, number> (e.g., via useMemo + a single pass over features) and read counts in O(1) for each application.

Suggested change
{applications.map(app => {
const count = features.filter(f => f.application === app).length
return (
<li key={app}>
<button
className={`sidebar-item ${selectedApplication === app ? 'active' : ''}`}
onClick={() => setSelectedApplication(app)}
>
<span className="sidebar-item-name">{app}</span>
<span className="sidebar-item-count">{count}</span>
</button>
</li>
)
})}
{(() => {
const featureCountByApp = new Map<string, number>()
features.forEach(f => {
featureCountByApp.set(f.application, (featureCountByApp.get(f.application) ?? 0) + 1)
})
return applications.map(app => {
const count = featureCountByApp.get(app) ?? 0
return (
<li key={app}>
<button
className={`sidebar-item ${selectedApplication === app ? 'active' : ''}`}
onClick={() => setSelectedApplication(app)}
>
<span className="sidebar-item-name">{app}</span>
<span className="sidebar-item-count">{count}</span>
</button>
</li>
)
})
})()}

Copilot uses AI. Check for mistakes.
/// <param name="Filters">Optional filter configurations for the feature.</param>
internal record CreateFeatureRequest(string Name, bool IsEnabled, string? Description = null, List<FeatureFilterDto>? Filters = null);
/// <param name="Application">The application this feature belongs to. Defaults to "Default".</param>
internal record CreateFeatureRequest(string Name, bool IsEnabled, string? Description = null, List<FeatureFilterDto>? Filters = null, string Application = "Default");

Copilot AI Mar 5, 2026

Copy link

Choose a reason for hiding this comment

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

The DB column enforces Application max length 256, but the create request currently accepts arbitrary-length strings, which will surface as a DB exception/500 on insert. Add request-level validation (e.g., reject/trim when Application.Length > 256, returning 400 with a clear message) to keep failures predictable for API consumers.

Copilot uses AI. Check for mistakes.
/// <param name="Filters">Optional filter configurations for the feature.</param>
internal record UpdateFeatureRequest(bool IsEnabled, string? Description = null, List<FeatureFilterDto>? Filters = null)
/// <param name="Application">The application this feature belongs to.</param>
internal record UpdateFeatureRequest(bool IsEnabled, string? Description = null, List<FeatureFilterDto>? Filters = null, string? Application = null)

Copilot AI Mar 5, 2026

Copy link

Choose a reason for hiding this comment

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

Same as create: updates can set Application to a value exceeding the DB max length (256), which will fail at save-time. Add validation for Application length on update (when provided) and return 400 rather than relying on DB constraints/exceptions.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants