diff --git a/console/src/app/modules/org-table/org-table.component.html b/console/src/app/modules/org-table/org-table.component.html index 695837b38b..245274928f 100644 --- a/console/src/app/modules/org-table/org-table.component.html +++ b/console/src/app/modules/org-table/org-table.component.html @@ -52,7 +52,12 @@ {{ 'ORG.PAGES.NAME' | translate }} - {{ org.name }} + + {{ org.name }}{{ + 'ORG.PAGES.DEFAULTLABEL' | translate + }} + @@ -88,6 +93,17 @@ + + + + + + + + + diff --git a/console/src/app/modules/org-table/org-table.component.scss b/console/src/app/modules/org-table/org-table.component.scss index b2d15ea57b..cc165bacc9 100644 --- a/console/src/app/modules/org-table/org-table.component.scss +++ b/console/src/app/modules/org-table/org-table.component.scss @@ -9,6 +9,10 @@ td { } } + .orgdefaultlabel { + margin-left: 0.5rem; + } + &:hover { .cpy-button { visibility: visible; diff --git a/console/src/app/modules/org-table/org-table.component.ts b/console/src/app/modules/org-table/org-table.component.ts index 833a46201e..2a06fb99f8 100644 --- a/console/src/app/modules/org-table/org-table.component.ts +++ b/console/src/app/modules/org-table/org-table.component.ts @@ -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 = new MatTableDataSource([]); - public displayedColumns: string[] = ['name', 'state', 'primaryDomain', 'creationDate', 'changeDate']; + public displayedColumns: string[] = ['name', 'state', 'primaryDomain', 'creationDate', 'changeDate', 'actions']; private loadingSubject: BehaviorSubject = new BehaviorSubject(false); public loading$: Observable = 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(orgs); }); + + this.mgmtService.getIAM().then((iam) => { + this.defaultOrgId = iam.defaultOrgId; + }); } public loadOrgs(request: Request): Observable { @@ -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({ diff --git a/console/src/app/modules/org-table/org-table.module.ts b/console/src/app/modules/org-table/org-table.module.ts index 58d96b1d8e..55f2d256ef 100644 --- a/console/src/app/modules/org-table/org-table.module.ts +++ b/console/src/app/modules/org-table/org-table.module.ts @@ -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, diff --git a/console/src/app/services/admin.service.ts b/console/src/app/services/admin.service.ts index 2244372bf0..4b203ed93f 100644 --- a/console/src/app/services/admin.service.ts +++ b/console/src/app/services/admin.service.ts @@ -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 { + const req = new SetDefaultOrgRequest(); + req.setOrgId(orgId); + + return this.grpcService.admin.setDefaultOrg(req, null).then((resp) => resp.toObject()); + } + public listEvents(req: ListEventsRequest): Promise { return this.grpcService.admin.listEvents(req, null).then((resp) => resp); } diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 202a29b5dd..0797398537 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -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", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 4e16a7f8b1..9bbf34972d 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -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", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index 48f1fe5c51..7e7e840adf 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -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", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index ad5c70d8b4..a99a507a29 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -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", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 051cc7e1ee..25fe8a6750 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -843,6 +843,9 @@ "ORGDETAIL_TITLE_WITHOUT_DOMAIN": "输入新组织的名称。", "ORGDETAILUSER_TITLE": "配置组织所有者", "DELETE": "删除组织", + "DEFAULTLABEL": "默认情况下", + "SETASDEFAULT": "设置为默认组织", + "DEFAULTORGSET": "默认的组织已经改变", "RENAME": { "ACTION": "改名", "TITLE": "重命名组织", diff --git a/e2e/cypress/e2e/organization/organizations.cy.ts b/e2e/cypress/e2e/organization/organizations.cy.ts index 1fbcb09869..f6ce9721a7 100644 --- a/e2e/cypress/e2e/organization/organizations.cy.ts +++ b/e2e/cypress/e2e/organization/organizations.cy.ts @@ -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'); diff --git a/e2e/cypress/support/api/orgs.ts b/e2e/cypress/support/api/orgs.ts index 1141f1caea..ceaa98b5c1 100644 --- a/e2e/cypress/support/api/orgs.ts +++ b/e2e/cypress/support/api/orgs.ts @@ -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 { return ensureSomething( @@ -23,6 +24,51 @@ export function ensureOrgExists(api: API, name: string): Cypress.Chainable { + 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 { + 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 { return searchSomething(api, `${api.mgmtBaseURL}/orgs/me`, 'GET', (res) => { return { entity: res.org, id: res.org.id, sequence: parseInt(res.org.details.sequence) };