mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 20:57:31 +00:00
feat(console): change default organization (#5151)
Adds the possibility to change the default organization from the organization overview
This commit is contained in:
@@ -52,7 +52,12 @@
|
|||||||
<th mat-header-cell *matHeaderCellDef>
|
<th mat-header-cell *matHeaderCellDef>
|
||||||
{{ 'ORG.PAGES.NAME' | translate }}
|
{{ 'ORG.PAGES.NAME' | translate }}
|
||||||
</th>
|
</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>
|
||||||
|
|
||||||
<ng-container matColumnDef="state">
|
<ng-container matColumnDef="state">
|
||||||
@@ -88,6 +93,17 @@
|
|||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</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-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||||
</table>
|
</table>
|
||||||
|
@@ -9,6 +9,10 @@ td {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.orgdefaultlabel {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.cpy-button {
|
.cpy-button {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
@@ -11,6 +11,8 @@ import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
|||||||
import { ToastService } from 'src/app/services/toast.service';
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
|
|
||||||
import { PaginatorComponent } from '../paginator/paginator.component';
|
import { PaginatorComponent } from '../paginator/paginator.component';
|
||||||
|
import { AdminService } from 'src/app/services/admin.service';
|
||||||
|
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||||
|
|
||||||
enum OrgListSearchKey {
|
enum OrgListSearchKey {
|
||||||
NAME = 'NAME',
|
NAME = 'NAME',
|
||||||
@@ -30,7 +32,7 @@ export class OrgTableComponent {
|
|||||||
@ViewChild('input') public filter!: Input;
|
@ViewChild('input') public filter!: Input;
|
||||||
|
|
||||||
public dataSource: MatTableDataSource<Org.AsObject> = new MatTableDataSource<Org.AsObject>([]);
|
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);
|
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||||
public activeOrg!: Org.AsObject;
|
public activeOrg!: Org.AsObject;
|
||||||
@@ -50,10 +52,13 @@ export class OrgTableComponent {
|
|||||||
offset: 0,
|
offset: 0,
|
||||||
queries: [],
|
queries: [],
|
||||||
});
|
});
|
||||||
|
public defaultOrgId: string = '';
|
||||||
private requestOrgsObservable$ = this.requestOrgs$.pipe(takeUntil(this.destroy$));
|
private requestOrgsObservable$ = this.requestOrgs$.pipe(takeUntil(this.destroy$));
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private authService: GrpcAuthService,
|
private authService: GrpcAuthService,
|
||||||
|
private mgmtService: ManagementService,
|
||||||
|
private adminService: AdminService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private toast: ToastService,
|
private toast: ToastService,
|
||||||
private _liveAnnouncer: LiveAnnouncer,
|
private _liveAnnouncer: LiveAnnouncer,
|
||||||
@@ -65,6 +70,10 @@ export class OrgTableComponent {
|
|||||||
this.requestOrgsObservable$.pipe(switchMap((req) => this.loadOrgs(req))).subscribe((orgs) => {
|
this.requestOrgsObservable$.pipe(switchMap((req) => this.loadOrgs(req))).subscribe((orgs) => {
|
||||||
this.dataSource = new MatTableDataSource<Org.AsObject>(orgs);
|
this.dataSource = new MatTableDataSource<Org.AsObject>(orgs);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.mgmtService.getIAM().then((iam) => {
|
||||||
|
this.defaultOrgId = iam.defaultOrgId;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public loadOrgs(request: Request): Observable<Org.AsObject[]> {
|
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 {
|
public applySearchQuery(searchQueries: OrgQuery[]): void {
|
||||||
this.searchQueries = searchQueries;
|
this.searchQueries = searchQueries;
|
||||||
this.requestOrgs$.next({
|
this.requestOrgs$.next({
|
||||||
|
@@ -20,6 +20,7 @@ import { InputModule } from '../input/input.module';
|
|||||||
import { PaginatorModule } from '../paginator/paginator.module';
|
import { PaginatorModule } from '../paginator/paginator.module';
|
||||||
import { RefreshTableModule } from '../refresh-table/refresh-table.module';
|
import { RefreshTableModule } from '../refresh-table/refresh-table.module';
|
||||||
import { OrgTableComponent } from './org-table.component';
|
import { OrgTableComponent } from './org-table.component';
|
||||||
|
import { TableActionsModule } from '../table-actions/table-actions.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [OrgTableComponent],
|
declarations: [OrgTableComponent],
|
||||||
@@ -33,6 +34,7 @@ import { OrgTableComponent } from './org-table.component';
|
|||||||
TimestampToDatePipeModule,
|
TimestampToDatePipeModule,
|
||||||
LocalizedDatePipeModule,
|
LocalizedDatePipeModule,
|
||||||
MatSortModule,
|
MatSortModule,
|
||||||
|
TableActionsModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
PaginatorModule,
|
PaginatorModule,
|
||||||
HasRoleModule,
|
HasRoleModule,
|
||||||
|
@@ -222,6 +222,8 @@ import {
|
|||||||
GetCustomPasswordChangeMessageTextRequest,
|
GetCustomPasswordChangeMessageTextRequest,
|
||||||
AddNotificationPolicyRequest,
|
AddNotificationPolicyRequest,
|
||||||
AddNotificationPolicyResponse,
|
AddNotificationPolicyResponse,
|
||||||
|
SetDefaultOrgRequest,
|
||||||
|
SetDefaultOrgResponse,
|
||||||
} from '../proto/generated/zitadel/admin_pb';
|
} from '../proto/generated/zitadel/admin_pb';
|
||||||
import { SearchQuery } from '../proto/generated/zitadel/member_pb';
|
import { SearchQuery } from '../proto/generated/zitadel/member_pb';
|
||||||
import { ListQuery } from '../proto/generated/zitadel/object_pb';
|
import { ListQuery } from '../proto/generated/zitadel/object_pb';
|
||||||
@@ -233,6 +235,13 @@ import { GrpcService } from './grpc.service';
|
|||||||
export class AdminService {
|
export class AdminService {
|
||||||
constructor(private readonly grpcService: GrpcService) {}
|
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> {
|
public listEvents(req: ListEventsRequest): Promise<ListEventsResponse> {
|
||||||
return this.grpcService.admin.listEvents(req, null).then((resp) => resp);
|
return this.grpcService.admin.listEvents(req, null).then((resp) => resp);
|
||||||
}
|
}
|
||||||
|
@@ -843,6 +843,9 @@
|
|||||||
"ORGDETAIL_TITLE_WITHOUT_DOMAIN": "Geben Sie den Namen der neuen Organisation ein.",
|
"ORGDETAIL_TITLE_WITHOUT_DOMAIN": "Geben Sie den Namen der neuen Organisation ein.",
|
||||||
"ORGDETAILUSER_TITLE": "Organisationsbesitzer hinzufügen",
|
"ORGDETAILUSER_TITLE": "Organisationsbesitzer hinzufügen",
|
||||||
"DELETE": "Organisation löschen",
|
"DELETE": "Organisation löschen",
|
||||||
|
"DEFAULTLABEL": "Standard",
|
||||||
|
"SETASDEFAULT": "Als Standardorganisation festlegen",
|
||||||
|
"DEFAULTORGSET": "Standardorganisation erfolgreich geändert",
|
||||||
"RENAME": {
|
"RENAME": {
|
||||||
"ACTION": "Umbenennen",
|
"ACTION": "Umbenennen",
|
||||||
"TITLE": "Organisation umbenennen",
|
"TITLE": "Organisation umbenennen",
|
||||||
|
@@ -843,6 +843,9 @@
|
|||||||
"ORGDETAIL_TITLE_WITHOUT_DOMAIN": "Enter the name of your new organization.",
|
"ORGDETAIL_TITLE_WITHOUT_DOMAIN": "Enter the name of your new organization.",
|
||||||
"ORGDETAILUSER_TITLE": "Configure Organization Owner",
|
"ORGDETAILUSER_TITLE": "Configure Organization Owner",
|
||||||
"DELETE": "Delete organization",
|
"DELETE": "Delete organization",
|
||||||
|
"DEFAULTLABEL": "Default",
|
||||||
|
"SETASDEFAULT": "Set as default organization",
|
||||||
|
"DEFAULTORGSET": "Default organization changed successfully",
|
||||||
"RENAME": {
|
"RENAME": {
|
||||||
"ACTION": "Rename",
|
"ACTION": "Rename",
|
||||||
"TITLE": "Rename Organization",
|
"TITLE": "Rename Organization",
|
||||||
|
@@ -843,6 +843,9 @@
|
|||||||
"ORGDETAIL_TITLE_WITHOUT_DOMAIN": "Saisissez le nom de votre nouvelle organisation.",
|
"ORGDETAIL_TITLE_WITHOUT_DOMAIN": "Saisissez le nom de votre nouvelle organisation.",
|
||||||
"ORGDETAILUSER_TITLE": "Configurer le propriétaire de l'organisation",
|
"ORGDETAILUSER_TITLE": "Configurer le propriétaire de l'organisation",
|
||||||
"DELETE": "Supprimer 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": {
|
"RENAME": {
|
||||||
"ACTION": "Renommer",
|
"ACTION": "Renommer",
|
||||||
"TITLE": "Renommer l'organisation",
|
"TITLE": "Renommer l'organisation",
|
||||||
|
@@ -844,6 +844,9 @@
|
|||||||
"ORGDETAIL_TITLE_WITHOUT_DOMAIN": "Inserisci il nome della tua nuova organizzazione.",
|
"ORGDETAIL_TITLE_WITHOUT_DOMAIN": "Inserisci il nome della tua nuova organizzazione.",
|
||||||
"ORGDETAILUSER_TITLE": "Configurare il proprietario dell'organizzazione",
|
"ORGDETAILUSER_TITLE": "Configurare il proprietario dell'organizzazione",
|
||||||
"DELETE": "Elimina organizzazione",
|
"DELETE": "Elimina organizzazione",
|
||||||
|
"DEFAULTLABEL": "Standard",
|
||||||
|
"SETASDEFAULT": "Imposta come organizzazione predefinita",
|
||||||
|
"DEFAULTORGSET": "Organizzazione predefinita cambiata con successo",
|
||||||
"RENAME": {
|
"RENAME": {
|
||||||
"ACTION": "Rinomina",
|
"ACTION": "Rinomina",
|
||||||
"TITLE": "Rinomina organizzazione",
|
"TITLE": "Rinomina organizzazione",
|
||||||
|
@@ -843,6 +843,9 @@
|
|||||||
"ORGDETAIL_TITLE_WITHOUT_DOMAIN": "输入新组织的名称。",
|
"ORGDETAIL_TITLE_WITHOUT_DOMAIN": "输入新组织的名称。",
|
||||||
"ORGDETAILUSER_TITLE": "配置组织所有者",
|
"ORGDETAILUSER_TITLE": "配置组织所有者",
|
||||||
"DELETE": "删除组织",
|
"DELETE": "删除组织",
|
||||||
|
"DEFAULTLABEL": "默认情况下",
|
||||||
|
"SETASDEFAULT": "设置为默认组织",
|
||||||
|
"DEFAULTORGSET": "默认的组织已经改变",
|
||||||
"RENAME": {
|
"RENAME": {
|
||||||
"ACTION": "改名",
|
"ACTION": "改名",
|
||||||
"TITLE": "重命名组织",
|
"TITLE": "重命名组织",
|
||||||
|
@@ -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 { apiAuth } from '../../support/api/apiauth';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
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');
|
it('should add an organization with the personal account as org owner');
|
||||||
describe('changing the current organization', () => {
|
describe('changing the current organization', () => {
|
||||||
it('should update displayed organization details');
|
it('should update displayed organization details');
|
||||||
|
@@ -2,6 +2,7 @@ import { ensureSomething } from './ensure';
|
|||||||
import { searchSomething } from './search';
|
import { searchSomething } from './search';
|
||||||
import { API } from './types';
|
import { API } from './types';
|
||||||
import { host } from '../login/users';
|
import { host } from '../login/users';
|
||||||
|
import { requestHeaders } from './apiauth';
|
||||||
|
|
||||||
export function ensureOrgExists(api: API, name: string): Cypress.Chainable<number> {
|
export function ensureOrgExists(api: API, name: string): Cypress.Chainable<number> {
|
||||||
return ensureSomething(
|
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> {
|
export function getOrgUnderTest(api: API): Cypress.Chainable<number> {
|
||||||
return searchSomething(api, `${api.mgmtBaseURL}/orgs/me`, 'GET', (res) => {
|
return searchSomething(api, `${api.mgmtBaseURL}/orgs/me`, 'GET', (res) => {
|
||||||
return { entity: res.org, id: res.org.id, sequence: parseInt(<string>res.org.details.sequence) };
|
return { entity: res.org, id: res.org.id, sequence: parseInt(<string>res.org.details.sequence) };
|
||||||
|
Reference in New Issue
Block a user