feat: add Application grouping to feature flags#1
Conversation
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>
There was a problem hiding this comment.
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
ApplicationonFeatureFlag, wired through DTOs/endpoints, plus a new/dashboardapi/applicationsendpoint 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.
| {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> | ||
| ) | ||
| })} |
There was a problem hiding this comment.
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.
| {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> | |
| ) | |
| }) | |
| })()} |
| /// <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"); |
There was a problem hiding this comment.
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.
| /// <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) |
There was a problem hiding this comment.
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.
Add an Application field to feature flags allowing them to be grouped by application name. Existing flags default to 'Default'.
Backend:
UI: