Skip to content

Commit 5849a54

Browse files
guguclaude
andcommitted
Replace permissions form with list-based Cedar policy editor
Replace the old grid-based permissions form (PermissionsFormComponent) and standalone permissions dialog (PermissionsAddDialogComponent) with a new list-based CedarPolicyListComponent. Each policy is shown as a row with add/edit/delete controls. Remove the "Configure permissions" button from the users page since permissions are now managed inline in group dialogs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6419ea3 commit 5849a54

23 files changed

Lines changed: 1255 additions & 819 deletions
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
.policy-list {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 4px;
5+
}
6+
7+
.empty-state {
8+
color: var(--mdc-theme-text-secondary-on-background, rgba(0, 0, 0, 0.6));
9+
font-size: 14px;
10+
padding: 12px 0;
11+
}
12+
13+
.policy-item {
14+
display: flex;
15+
align-items: center;
16+
justify-content: space-between;
17+
padding: 4px 8px;
18+
border-radius: 4px;
19+
border: 1px solid var(--mdc-outlined-text-field-outline-color, rgba(0, 0, 0, 0.12));
20+
}
21+
22+
.policy-item--add {
23+
border-style: dashed;
24+
}
25+
26+
.policy-item__content {
27+
display: flex;
28+
align-items: center;
29+
gap: 8px;
30+
flex: 1;
31+
min-width: 0;
32+
}
33+
34+
.policy-item__icon {
35+
font-size: 20px;
36+
width: 20px;
37+
height: 20px;
38+
color: var(--color-accentedPalette-500, #1976d2);
39+
flex-shrink: 0;
40+
}
41+
42+
.policy-item__label {
43+
font-size: 14px;
44+
font-weight: 500;
45+
}
46+
47+
.policy-item__table {
48+
font-size: 14px;
49+
color: var(--mdc-theme-text-secondary-on-background, rgba(0, 0, 0, 0.6));
50+
}
51+
52+
.policy-item__actions {
53+
display: flex;
54+
align-items: center;
55+
flex-shrink: 0;
56+
}
57+
58+
.policy-item__edit-form {
59+
display: flex;
60+
align-items: flex-start;
61+
gap: 8px;
62+
flex: 1;
63+
flex-wrap: wrap;
64+
}
65+
66+
.policy-field {
67+
flex: 1;
68+
min-width: 180px;
69+
}
70+
71+
.policy-item__edit-actions {
72+
display: flex;
73+
align-items: center;
74+
gap: 4px;
75+
padding-top: 8px;
76+
}
77+
78+
.add-policy-button {
79+
align-self: flex-start;
80+
margin-top: 4px;
81+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<div class="policy-list">
2+
<app-content-loader *ngIf="loading"></app-content-loader>
3+
4+
<div *ngIf="!loading && policies.length === 0 && !showAddForm" class="empty-state">
5+
No policies defined. Add a policy to grant permissions.
6+
</div>
7+
8+
<div *ngFor="let policy of policies; let i = index" class="policy-item">
9+
<!-- Display mode -->
10+
<ng-container *ngIf="editingIndex !== i">
11+
<div class="policy-item__content">
12+
<mat-icon class="policy-item__icon">security</mat-icon>
13+
<span class="policy-item__label">{{ getActionLabel(policy.action) }}</span>
14+
<span *ngIf="policy.tableName" class="policy-item__table">
15+
— {{ getTableDisplayName(policy.tableName) }}
16+
</span>
17+
</div>
18+
<div class="policy-item__actions">
19+
<button mat-icon-button type="button" (click)="startEdit(i)" matTooltip="Edit">
20+
<mat-icon>edit</mat-icon>
21+
</button>
22+
<button mat-icon-button type="button" (click)="removePolicy(i)" matTooltip="Delete">
23+
<mat-icon>delete</mat-icon>
24+
</button>
25+
</div>
26+
</ng-container>
27+
28+
<!-- Edit mode -->
29+
<ng-container *ngIf="editingIndex === i">
30+
<div class="policy-item__edit-form">
31+
<mat-form-field appearance="outline" class="policy-field">
32+
<mat-label>Action</mat-label>
33+
<mat-select [(ngModel)]="editAction" [ngModelOptions]="{standalone: true}">
34+
<mat-option *ngFor="let action of availableActions" [value]="action.value">
35+
{{ action.label }}
36+
</mat-option>
37+
</mat-select>
38+
</mat-form-field>
39+
40+
<mat-form-field *ngIf="editNeedsTable" appearance="outline" class="policy-field">
41+
<mat-label>Table</mat-label>
42+
<mat-select [(ngModel)]="editTableName" [ngModelOptions]="{standalone: true}">
43+
<mat-option *ngFor="let table of availableTables" [value]="table.tableName">
44+
{{ table.displayName }}
45+
</mat-option>
46+
</mat-select>
47+
</mat-form-field>
48+
49+
<div class="policy-item__edit-actions">
50+
<button mat-button color="primary" type="button" (click)="saveEdit(i)"
51+
[disabled]="!editAction || (editNeedsTable && !editTableName)">
52+
Save
53+
</button>
54+
<button mat-button type="button" (click)="cancelEdit()">Cancel</button>
55+
</div>
56+
</div>
57+
</ng-container>
58+
</div>
59+
60+
<!-- Add form -->
61+
<div *ngIf="showAddForm" class="policy-item policy-item--add">
62+
<div class="policy-item__edit-form">
63+
<mat-form-field appearance="outline" class="policy-field">
64+
<mat-label>Action</mat-label>
65+
<mat-select [(ngModel)]="newAction" [ngModelOptions]="{standalone: true}">
66+
<mat-option *ngFor="let action of availableActions" [value]="action.value">
67+
{{ action.label }}
68+
</mat-option>
69+
</mat-select>
70+
</mat-form-field>
71+
72+
<mat-form-field *ngIf="needsTable" appearance="outline" class="policy-field">
73+
<mat-label>Table</mat-label>
74+
<mat-select [(ngModel)]="newTableName" [ngModelOptions]="{standalone: true}">
75+
<mat-option *ngFor="let table of availableTables" [value]="table.tableName">
76+
{{ table.displayName }}
77+
</mat-option>
78+
</mat-select>
79+
</mat-form-field>
80+
81+
<div class="policy-item__edit-actions">
82+
<button mat-button color="primary" type="button" (click)="addPolicy()"
83+
[disabled]="!newAction || (needsTable && !newTableName)">
84+
Add
85+
</button>
86+
<button mat-button type="button" (click)="resetAddForm()">Cancel</button>
87+
</div>
88+
</div>
89+
</div>
90+
91+
<button *ngIf="!showAddForm && !loading" mat-button color="primary" type="button"
92+
(click)="showAddForm = true" class="add-policy-button">
93+
<mat-icon>add</mat-icon> Add policy
94+
</button>
95+
</div>
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { FormsModule } from '@angular/forms';
3+
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
4+
import { CedarPolicyListComponent } from './cedar-policy-list.component';
5+
6+
describe('CedarPolicyListComponent', () => {
7+
let component: CedarPolicyListComponent;
8+
let fixture: ComponentFixture<CedarPolicyListComponent>;
9+
10+
const fakeTables = [
11+
{ tableName: 'customers', displayName: 'Customers' },
12+
{ tableName: 'orders', displayName: 'Orders' },
13+
];
14+
15+
beforeEach(async () => {
16+
await TestBed.configureTestingModule({
17+
imports: [CedarPolicyListComponent, FormsModule, BrowserAnimationsModule],
18+
}).compileComponents();
19+
20+
fixture = TestBed.createComponent(CedarPolicyListComponent);
21+
component = fixture.componentInstance;
22+
component.availableTables = fakeTables;
23+
fixture.detectChanges();
24+
});
25+
26+
it('should create', () => {
27+
expect(component).toBeTruthy();
28+
});
29+
30+
it('should add a policy', () => {
31+
const emitSpy = vi.spyOn(component.policiesChange, 'emit');
32+
component.showAddForm = true;
33+
component.newAction = 'connection:read';
34+
component.addPolicy();
35+
36+
expect(component.policies.length).toBe(1);
37+
expect(component.policies[0].action).toBe('connection:read');
38+
expect(emitSpy).toHaveBeenCalled();
39+
expect(component.showAddForm).toBe(false);
40+
});
41+
42+
it('should add a table policy with tableName', () => {
43+
component.showAddForm = true;
44+
component.newAction = 'table:read';
45+
component.newTableName = 'customers';
46+
component.addPolicy();
47+
48+
expect(component.policies.length).toBe(1);
49+
expect(component.policies[0].action).toBe('table:read');
50+
expect(component.policies[0].tableName).toBe('customers');
51+
});
52+
53+
it('should not add policy without action', () => {
54+
component.showAddForm = true;
55+
component.newAction = '';
56+
component.addPolicy();
57+
58+
expect(component.policies.length).toBe(0);
59+
});
60+
61+
it('should not add table policy without table name', () => {
62+
component.showAddForm = true;
63+
component.newAction = 'table:read';
64+
component.newTableName = '';
65+
component.addPolicy();
66+
67+
expect(component.policies.length).toBe(0);
68+
});
69+
70+
it('should remove a policy', () => {
71+
component.policies = [{ action: 'connection:read' }, { action: 'group:read' }];
72+
const emitSpy = vi.spyOn(component.policiesChange, 'emit');
73+
74+
component.removePolicy(0);
75+
76+
expect(component.policies.length).toBe(1);
77+
expect(component.policies[0].action).toBe('group:read');
78+
expect(emitSpy).toHaveBeenCalled();
79+
});
80+
81+
it('should start and save edit', () => {
82+
component.policies = [{ action: 'connection:read' }];
83+
const emitSpy = vi.spyOn(component.policiesChange, 'emit');
84+
85+
component.startEdit(0);
86+
expect(component.editingIndex).toBe(0);
87+
expect(component.editAction).toBe('connection:read');
88+
89+
component.editAction = 'connection:edit';
90+
component.saveEdit(0);
91+
92+
expect(component.policies[0].action).toBe('connection:edit');
93+
expect(component.editingIndex).toBeNull();
94+
expect(emitSpy).toHaveBeenCalled();
95+
});
96+
97+
it('should cancel edit', () => {
98+
component.policies = [{ action: 'connection:read' }];
99+
component.startEdit(0);
100+
component.editAction = 'connection:edit';
101+
component.cancelEdit();
102+
103+
expect(component.editingIndex).toBeNull();
104+
expect(component.policies[0].action).toBe('connection:read');
105+
});
106+
107+
it('should return correct action labels', () => {
108+
expect(component.getActionLabel('*')).toBe('Full access (all permissions)');
109+
expect(component.getActionLabel('connection:read')).toBe('Connection: Read');
110+
expect(component.getActionLabel('table:edit')).toBe('Table: Edit');
111+
});
112+
113+
it('should return correct table display names', () => {
114+
expect(component.getTableDisplayName('customers')).toBe('Customers');
115+
expect(component.getTableDisplayName('unknown')).toBe('unknown');
116+
});
117+
118+
it('should detect needsTable correctly', () => {
119+
component.newAction = 'connection:read';
120+
expect(component.needsTable).toBe(false);
121+
122+
component.newAction = 'table:read';
123+
expect(component.needsTable).toBe(true);
124+
});
125+
126+
it('should reset add form', () => {
127+
component.showAddForm = true;
128+
component.newAction = 'connection:read';
129+
component.newTableName = 'test';
130+
component.resetAddForm();
131+
132+
expect(component.showAddForm).toBe(false);
133+
expect(component.newAction).toBe('');
134+
expect(component.newTableName).toBe('');
135+
});
136+
});

0 commit comments

Comments
 (0)