feat(console): change default organization (#5151)

Adds the possibility to change the default organization from the organization overview
This commit is contained in:
Max Peintner 2023-02-14 09:31:32 +01:00 committed by GitHub
parent 6a97c3e233
commit 3696c1b2d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 150 additions and 3 deletions

View File

@ -52,7 +52,12 @@
<th mat-header-cell *matHeaderCellDef>
{{ 'ORG.PAGES.NAME' | translate }}
</th>
<td mat-cell *matCellDef="let org" (click)="setAndNavigateToOrg(org)">{{ org.name }}</td>
<td mat-cell *matCellDef="let org" (click)="setAndNavigateToOrg(org)">
<span>{{ org.name }}</span
><span *ngIf="defaultOrgId === org.id" class="state orgdefaultlabel">{{
'ORG.PAGES.DEFAULTLABEL' | translate
}}</span>
</td>
</ng-container>
<ng-container matColumnDef="state">
@ -88,6 +93,17 @@
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef class="user-tr-actions"></th>
<td mat-cell *matCellDef="let org" class="user-tr-actions">
<cnsl-table-actions [hasActions]="true">
<button menuActions mat-menu-item (click)="setDefaultOrg(org)" data-e2e="set-default-button">
{{ 'ORG.PAGES.SETASDEFAULT' | translate }}
</button>
</cnsl-table-actions>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>

View File

@ -9,6 +9,10 @@ td {
}
}
.orgdefaultlabel {
margin-left: 0.5rem;
}
&:hover {
.cpy-button {
visibility: visible;

View File

@ -11,6 +11,8 @@ import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
import { PaginatorComponent } from '../paginator/paginator.component';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
enum OrgListSearchKey {
NAME = 'NAME',
@ -30,7 +32,7 @@ export class OrgTableComponent {
@ViewChild('input') public filter!: Input;
public dataSource: MatTableDataSource<Org.AsObject> = new MatTableDataSource<Org.AsObject>([]);
public displayedColumns: string[] = ['name', 'state', 'primaryDomain', 'creationDate', 'changeDate'];
public displayedColumns: string[] = ['name', 'state', 'primaryDomain', 'creationDate', 'changeDate', 'actions'];
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public activeOrg!: Org.AsObject;
@ -50,10 +52,13 @@ export class OrgTableComponent {
offset: 0,
queries: [],
});
public defaultOrgId: string = '';
private requestOrgsObservable$ = this.requestOrgs$.pipe(takeUntil(this.destroy$));
constructor(
private authService: GrpcAuthService,
private mgmtService: ManagementService,
private adminService: AdminService,
private router: Router,
private toast: ToastService,
private _liveAnnouncer: LiveAnnouncer,
@ -65,6 +70,10 @@ export class OrgTableComponent {
this.requestOrgsObservable$.pipe(switchMap((req) => this.loadOrgs(req))).subscribe((orgs) => {
this.dataSource = new MatTableDataSource<Org.AsObject>(orgs);
});
this.mgmtService.getIAM().then((iam) => {
this.defaultOrgId = iam.defaultOrgId;
});
}
public loadOrgs(request: Request): Observable<Org.AsObject[]> {
@ -111,6 +120,18 @@ export class OrgTableComponent {
}
}
public setDefaultOrg(org: Org.AsObject) {
this.adminService
.setDefaultOrg(org.id)
.then(() => {
this.toast.showInfo('ORG.PAGES.DEFAULTORGSET', true);
this.defaultOrgId = org.id;
})
.catch((error) => {
this.toast.showError(error);
});
}
public applySearchQuery(searchQueries: OrgQuery[]): void {
this.searchQueries = searchQueries;
this.requestOrgs$.next({

View File

@ -20,6 +20,7 @@ import { InputModule } from '../input/input.module';
import { PaginatorModule } from '../paginator/paginator.module';
import { RefreshTableModule } from '../refresh-table/refresh-table.module';
import { OrgTableComponent } from './org-table.component';
import { TableActionsModule } from '../table-actions/table-actions.module';
@NgModule({
declarations: [OrgTableComponent],
@ -33,6 +34,7 @@ import { OrgTableComponent } from './org-table.component';
TimestampToDatePipeModule,
LocalizedDatePipeModule,
MatSortModule,
TableActionsModule,
MatIconModule,
PaginatorModule,
HasRoleModule,

View File

@ -222,6 +222,8 @@ import {
GetCustomPasswordChangeMessageTextRequest,
AddNotificationPolicyRequest,
AddNotificationPolicyResponse,
SetDefaultOrgRequest,
SetDefaultOrgResponse,
} from '../proto/generated/zitadel/admin_pb';
import { SearchQuery } from '../proto/generated/zitadel/member_pb';
import { ListQuery } from '../proto/generated/zitadel/object_pb';
@ -233,6 +235,13 @@ import { GrpcService } from './grpc.service';
export class AdminService {
constructor(private readonly grpcService: GrpcService) {}
public setDefaultOrg(orgId: string): Promise<SetDefaultOrgResponse.AsObject> {
const req = new SetDefaultOrgRequest();
req.setOrgId(orgId);
return this.grpcService.admin.setDefaultOrg(req, null).then((resp) => resp.toObject());
}
public listEvents(req: ListEventsRequest): Promise<ListEventsResponse> {
return this.grpcService.admin.listEvents(req, null).then((resp) => resp);
}

View File

@ -843,6 +843,9 @@
"ORGDETAIL_TITLE_WITHOUT_DOMAIN": "Geben Sie den Namen der neuen Organisation ein.",
"ORGDETAILUSER_TITLE": "Organisationsbesitzer hinzufügen",
"DELETE": "Organisation löschen",
"DEFAULTLABEL": "Standard",
"SETASDEFAULT": "Als Standardorganisation festlegen",
"DEFAULTORGSET": "Standardorganisation erfolgreich geändert",
"RENAME": {
"ACTION": "Umbenennen",
"TITLE": "Organisation umbenennen",

View File

@ -843,6 +843,9 @@
"ORGDETAIL_TITLE_WITHOUT_DOMAIN": "Enter the name of your new organization.",
"ORGDETAILUSER_TITLE": "Configure Organization Owner",
"DELETE": "Delete organization",
"DEFAULTLABEL": "Default",
"SETASDEFAULT": "Set as default organization",
"DEFAULTORGSET": "Default organization changed successfully",
"RENAME": {
"ACTION": "Rename",
"TITLE": "Rename Organization",

View File

@ -843,6 +843,9 @@
"ORGDETAIL_TITLE_WITHOUT_DOMAIN": "Saisissez le nom de votre nouvelle organisation.",
"ORGDETAILUSER_TITLE": "Configurer le propriétaire de l'organisation",
"DELETE": "Supprimer l'organisation",
"DEFAULTLABEL": "Défaut",
"SETASDEFAULT": "Définir comme organisation par défaut",
"DEFAULTORGSET": "L'organisation par défaut a été modifiée avec succès",
"RENAME": {
"ACTION": "Renommer",
"TITLE": "Renommer l'organisation",

View File

@ -844,6 +844,9 @@
"ORGDETAIL_TITLE_WITHOUT_DOMAIN": "Inserisci il nome della tua nuova organizzazione.",
"ORGDETAILUSER_TITLE": "Configurare il proprietario dell'organizzazione",
"DELETE": "Elimina organizzazione",
"DEFAULTLABEL": "Standard",
"SETASDEFAULT": "Imposta come organizzazione predefinita",
"DEFAULTORGSET": "Organizzazione predefinita cambiata con successo",
"RENAME": {
"ACTION": "Rinomina",
"TITLE": "Rinomina organizzazione",

View File

@ -843,6 +843,9 @@
"ORGDETAIL_TITLE_WITHOUT_DOMAIN": "输入新组织的名称。",
"ORGDETAILUSER_TITLE": "配置组织所有者",
"DELETE": "删除组织",
"DEFAULTLABEL": "默认情况下",
"SETASDEFAULT": "设置为默认组织",
"DEFAULTORGSET": "默认的组织已经改变",
"RENAME": {
"ACTION": "改名",
"TITLE": "重命名组织",

View File

@ -1,4 +1,4 @@
import { ensureOrgExists } from 'support/api/orgs';
import { ensureOrgExists, ensureOrgIsDefault, isDefaultOrg } from 'support/api/orgs';
import { apiAuth } from '../../support/api/apiauth';
import { v4 as uuidv4 } from 'uuid';
@ -33,6 +33,40 @@ describe('organizations', () => {
});
});
const orgOverviewPath = `/orgs`;
const initialDefaultOrg = 'e2eorgolddefault';
const orgNameForNewDefault = 'e2eorgnewdefault';
describe('set default org', () => {
beforeEach(() => {
apiAuth()
.as('api')
.then((api) => {
ensureOrgExists(api, orgNameForNewDefault)
.as('newDefaultOrgId')
.then(() => {
ensureOrgExists(api, initialDefaultOrg)
.as('defaultOrg')
.then((id) => {
ensureOrgIsDefault(api, id)
.as('orgWasDefault')
.then(() => {
cy.visit(`${orgOverviewPath}`).as('orgsite');
});
});
});
});
});
it('should rename the organization', function () {
const rowSelector = `tr:contains(${orgNameForNewDefault})`;
cy.get(rowSelector).find('[data-e2e="table-actions-button"]').click({ force: true });
cy.get('[data-e2e="set-default-button"]', { timeout: 1000 }).should('be.visible').click();
cy.shouldConfirmSuccess();
isDefaultOrg(this.api, this.newDefaultOrgId);
});
});
it('should add an organization with the personal account as org owner');
describe('changing the current organization', () => {
it('should update displayed organization details');

View File

@ -2,6 +2,7 @@ import { ensureSomething } from './ensure';
import { searchSomething } from './search';
import { API } from './types';
import { host } from '../login/users';
import { requestHeaders } from './apiauth';
export function ensureOrgExists(api: API, name: string): Cypress.Chainable<number> {
return ensureSomething(
@ -23,6 +24,51 @@ export function ensureOrgExists(api: API, name: string): Cypress.Chainable<numbe
);
}
export function isDefaultOrg(api: API, orgId: number): Cypress.Chainable<boolean> {
console.log('huhu', orgId);
return cy
.request({
method: 'GET',
url: encodeURI(`${api.mgmtBaseURL}/iam`),
headers: requestHeaders(api, orgId),
})
.then((res) => {
const { defaultOrgId } = res.body;
expect(defaultOrgId).to.equal(orgId);
return defaultOrgId === orgId;
});
}
export function ensureOrgIsDefault(api: API, orgId: number): Cypress.Chainable<boolean> {
return cy
.request({
method: 'GET',
url: encodeURI(`${api.mgmtBaseURL}/iam`),
headers: requestHeaders(api, orgId),
})
.then((res) => {
return res.body;
})
.then(({ defaultOrgId }) => {
if (defaultOrgId === orgId) {
return true;
} else {
return cy
.request({
method: 'PUT',
url: `${api.adminBaseURL}/orgs/default/${orgId}`,
headers: requestHeaders(api, orgId),
failOnStatusCode: true,
followRedirect: false,
})
.then((cRes) => {
expect(cRes.status).to.equal(200);
return !!cRes.body;
});
}
});
}
export function getOrgUnderTest(api: API): Cypress.Chainable<number> {
return searchSomething(api, `${api.mgmtBaseURL}/orgs/me`, 'GET', (res) => {
return { entity: res.org, id: res.org.id, sequence: parseInt(<string>res.org.details.sequence) };