mirror of
https://github.com/zitadel/zitadel.git
synced 2025-02-28 20:47:22 +00:00
feat: rehauled console (#3525)
* new console * move npm ci to angular build * rel path for assets * local grpc copy * login policy, rm clear views, features rel path * lock Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
parent
00f7dbe875
commit
08ae39ae19
@ -7,9 +7,6 @@ ARG NODE_VERSION=14
|
||||
FROM node:${NODE_VERSION} as npm-base
|
||||
WORKDIR /console
|
||||
|
||||
COPY console/package.json console/package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY console .
|
||||
COPY --from=zitadel-base:local /proto /proto
|
||||
COPY --from=zitadel-base:local /usr/local/bin /usr/local/bin/.
|
||||
@ -20,12 +17,16 @@ RUN build/console/generate-grpc.sh
|
||||
## copy for local dev
|
||||
#######################
|
||||
FROM scratch as npm-copy
|
||||
COPY --from=npm-base /console/src/app/proto/generated .
|
||||
COPY --from=npm-base /console/src/app/proto/generated /console/src/app/proto/generated
|
||||
|
||||
#######################
|
||||
## angular lint workspace and prod build
|
||||
#######################
|
||||
FROM npm-base as angular-build
|
||||
|
||||
COPY console/package.json console/package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
RUN npm run lint
|
||||
RUN npm run prodbuild
|
||||
RUN ls -la /console/dist/console
|
||||
|
1
console/.gitignore
vendored
1
console/.gitignore
vendored
@ -47,3 +47,4 @@ Thumbs.db
|
||||
|
||||
# Proto generated js files
|
||||
src/app/proto
|
||||
|
||||
|
@ -20,7 +20,7 @@ Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
Please refer to [guides](../guides/quickstart.md#developing-zitadel)
|
||||
|
||||
## Further help
|
||||
|
||||
|
@ -27,7 +27,9 @@
|
||||
"scripts": ["./node_modules/tinycolor2/dist/tinycolor-min.js"],
|
||||
"allowedCommonJsDependencies": [
|
||||
"@angular/common/locales/de",
|
||||
"codemirror/mode/javascript/javascript",
|
||||
"src/app/proto/generated/zitadel/admin_pb",
|
||||
"src/app/proto/generated/zitadel/org_pb",
|
||||
"src/app/proto/generated/zitadel/management_pb",
|
||||
"src/app/proto/generated/**",
|
||||
"google-protobuf/google/protobuf/empty_pb",
|
||||
|
14
console/cypress.json
Normal file
14
console/cypress.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"supportFile": "./cypress/support/index.ts",
|
||||
"reporter": "mochawesome",
|
||||
"reporterOptions": {
|
||||
"reportDir": "cypress/results",
|
||||
"overwrite": false,
|
||||
"html": true,
|
||||
"json": true
|
||||
},
|
||||
"chromeWebSecurity": false,
|
||||
"experimentalSessionSupport": true,
|
||||
"trashAssetsBeforeRuns": false
|
||||
}
|
||||
|
21
console/cypress.sh
Executable file
21
console/cypress.sh
Executable file
@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ACTION=$1
|
||||
ENVFILE=$2
|
||||
|
||||
shift
|
||||
shift
|
||||
|
||||
projectRoot=".."
|
||||
|
||||
set -a; source $ENVFILE; set +a
|
||||
|
||||
NPX=""
|
||||
if ! command -v cypress &> /dev/null; then
|
||||
NPX="npx"
|
||||
fi
|
||||
|
||||
$NPX cypress $ACTION \
|
||||
--port ${E2E_CYPRESS_PORT} \
|
||||
--env org="${E2E_ORG}",org_owner_password="${E2E_ORG_OWNER_PW}",org_owner_viewer_password="${E2E_ORG_OWNER_VIEWER_PW}",org_project_creator_password="${E2E_ORG_PROJECT_CREATOR_PW}",login_policy_user_password="${E2E_LOGIN_POLICY_USER_PW}",password_complexity_user_password="${E2E_PASSWORD_COMPLEXITY_USER_PW}",consoleUrl=${E2E_CONSOLE_URL},apiUrl="${E2E_API_URL}",accountsUrl="${E2E_ACCOUNTS_URL}",issuerUrl="${E2E_ISSUER_URL}",serviceAccountKey="${E2E_SERVICEACCOUNT_KEY}",serviceAccountKeyPath="${E2E_SERVICEACCOUNT_KEY_PATH}",otherZitadelIdpInstance="${E2E_OTHER_ZITADEL_IDP_INSTANCE}",zitadelProjectResourceId="${E2E_ZITADEL_PROJECT_RESOURCE_ID}" \
|
||||
"$@"
|
4
console/cypress/.gitignore
vendored
Normal file
4
console/cypress/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
results
|
||||
videos
|
||||
screenshots
|
||||
downloads
|
5
console/cypress/fixtures/example.json
Normal file
5
console/cypress/fixtures/example.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
BIN
console/cypress/fixtures/logo.png
Normal file
BIN
console/cypress/fixtures/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.8 KiB |
50
console/cypress/integration/applications/applications.ts
Normal file
50
console/cypress/integration/applications/applications.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { login, User } from "../../support/login/users";
|
||||
import { Apps, ensureProjectExists, ensureProjectResourceDoesntExist } from "../../support/api/projects";
|
||||
import { apiAuth } from "../../support/api/apiauth";
|
||||
|
||||
describe('applications', () => {
|
||||
|
||||
const testProjectName = 'e2eprojectapplication'
|
||||
const testAppName = 'e2eappundertest'
|
||||
|
||||
;[User.OrgOwner].forEach(user => {
|
||||
|
||||
describe(`as user "${user}"`, () => {
|
||||
|
||||
beforeEach(`ensure it doesn't exist already`, () => {
|
||||
login(user)
|
||||
apiAuth().then(api => {
|
||||
ensureProjectExists(api, testProjectName).then(projectID => {
|
||||
ensureProjectResourceDoesntExist(api, projectID, Apps, testAppName).then(() => {
|
||||
cy.visit(`${Cypress.env('consoleUrl')}/projects/${projectID}`)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('add app', () => {
|
||||
cy.get('mat-spinner')
|
||||
cy.get('mat-spinner').should('not.exist')
|
||||
cy.get('[data-e2e="app-card-add"]').should('be.visible').click()
|
||||
// select webapp
|
||||
cy.get('[formcontrolname="name"]').type(testAppName)
|
||||
cy.get('[for="WEB"]').click()
|
||||
cy.get('[data-e2e="continue-button-nameandtype"]').click()
|
||||
//select authentication
|
||||
cy.get('[for="PKCE"]').click()
|
||||
cy.get('[data-e2e="continue-button-authmethod"]').click()
|
||||
//enter URL
|
||||
cy.get('cnsl-redirect-uris').eq(0).type("https://testurl.org")
|
||||
cy.get('cnsl-redirect-uris').eq(1).type("https://testlogouturl.org")
|
||||
cy.get('[data-e2e="continue-button-redirecturis"]').click()
|
||||
cy.get('[data-e2e="create-button"]').click().then(() => {
|
||||
cy.get('[id*=overlay]').should('exist')
|
||||
})
|
||||
cy.get('.data-e2e-success')
|
||||
cy.wait(200)
|
||||
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist')
|
||||
//TODO: check client ID/Secret
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
83
console/cypress/integration/humans/humans.ts
Normal file
83
console/cypress/integration/humans/humans.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { apiAuth } from '../../support/api/apiauth';
|
||||
import { ensureHumanUserExists, ensureUserDoesntExist } from '../../support/api/users';
|
||||
import { login, User, username } from '../../support/login/users';
|
||||
|
||||
describe('humans', () => {
|
||||
const humansPath = `${Cypress.env('consoleUrl')}/users?type=human`;
|
||||
const testHumanUserNameAdd = 'e2ehumanusernameadd';
|
||||
const testHumanUserNameRemove = 'e2ehumanusernameremove';
|
||||
|
||||
[User.OrgOwner].forEach((user) => {
|
||||
describe(`as user "${user}"`, () => {
|
||||
beforeEach(() => {
|
||||
login(user);
|
||||
cy.visit(humansPath);
|
||||
cy.get('[data-cy=timestamp]');
|
||||
});
|
||||
|
||||
describe('add', () => {
|
||||
before(`ensure it doesn't exist already`, () => {
|
||||
apiAuth().then((apiCallProperties) => {
|
||||
ensureUserDoesntExist(apiCallProperties, testHumanUserNameAdd);
|
||||
});
|
||||
});
|
||||
|
||||
it('should add a user', () => {
|
||||
cy.get('a[href="/users/create"]').click();
|
||||
cy.url().should('contain', 'users/create');
|
||||
cy.get('[formcontrolname="email"]').type(username('e2ehuman'));
|
||||
//force needed due to the prefilled username prefix
|
||||
cy.get('[formcontrolname="userName"]').type(testHumanUserNameAdd, { force: true });
|
||||
cy.get('[formcontrolname="firstName"]').type('e2ehumanfirstname');
|
||||
cy.get('[formcontrolname="lastName"]').type('e2ehumanlastname');
|
||||
cy.get('[formcontrolname="phone"]').type('+41 123456789');
|
||||
cy.get('[data-e2e="create-button"]').click();
|
||||
cy.get('.data-e2e-success');
|
||||
cy.wait(200);
|
||||
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
before('ensure it exists', () => {
|
||||
apiAuth().then((api) => {
|
||||
ensureHumanUserExists(api, testHumanUserNameRemove);
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete a human user', () => {
|
||||
cy.contains('tr', testHumanUserNameRemove, { timeout: 1000 })
|
||||
.find('button')
|
||||
//force due to angular hidden buttons
|
||||
.click({ force: true });
|
||||
cy.get('[e2e-data="confirm-dialog-input"]').type(username(testHumanUserNameRemove, Cypress.env('org')));
|
||||
cy.get('[e2e-data="confirm-dialog-button"]').click();
|
||||
cy.get('.data-e2e-success');
|
||||
cy.wait(200);
|
||||
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
/*
|
||||
describe("users", ()=> {
|
||||
|
||||
before(()=> {
|
||||
cy.consolelogin(Cypress.env('username'), Cypress.env('password'), Cypress.env('consoleUrl'))
|
||||
})
|
||||
|
||||
it('should show personal information', () => {
|
||||
cy.log(`USER: show personal information`);
|
||||
//click on user information
|
||||
cy.get('a[href*="users/me"').eq(0).click()
|
||||
cy.url().should('contain', '/users/me')
|
||||
})
|
||||
|
||||
it('should show users', () => {
|
||||
cy.visit(Cypress.env('consoleUrl') + '/users/list/humans')
|
||||
cy.url().should('contain', 'users/list/humans')
|
||||
})
|
||||
})
|
||||
|
||||
*/
|
60
console/cypress/integration/machines/machines.ts
Normal file
60
console/cypress/integration/machines/machines.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { apiAuth } from '../../support/api/apiauth';
|
||||
import { ensureMachineUserExists, ensureUserDoesntExist } from '../../support/api/users';
|
||||
import { login, User, username } from '../../support/login/users';
|
||||
|
||||
describe('machines', () => {
|
||||
const machinesPath = `${Cypress.env('consoleUrl')}/users?type=machine`;
|
||||
const testMachineUserNameAdd = 'e2emachineusernameadd';
|
||||
const testMachineUserNameRemove = 'e2emachineusernameremove';
|
||||
|
||||
[User.OrgOwner].forEach((user) => {
|
||||
describe(`as user "${user}"`, () => {
|
||||
beforeEach(() => {
|
||||
login(user);
|
||||
cy.visit(machinesPath);
|
||||
cy.get('[data-cy=timestamp]');
|
||||
});
|
||||
|
||||
describe('add', () => {
|
||||
before(`ensure it doesn't exist already`, () => {
|
||||
apiAuth().then((apiCallProperties) => {
|
||||
ensureUserDoesntExist(apiCallProperties, testMachineUserNameAdd);
|
||||
});
|
||||
});
|
||||
|
||||
it('should add a machine', () => {
|
||||
cy.get('a[href="/users/create-machine"]').click();
|
||||
cy.url().should('contain', 'users/create-machine');
|
||||
//force needed due to the prefilled username prefix
|
||||
cy.get('[formcontrolname="userName"]').type(testMachineUserNameAdd, { force: true });
|
||||
cy.get('[formcontrolname="name"]').type('e2emachinename');
|
||||
cy.get('[formcontrolname="description"]').type('e2emachinedescription');
|
||||
cy.get('[data-e2e="create-button"]').click();
|
||||
cy.get('.data-e2e-success');
|
||||
cy.wait(200);
|
||||
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
before('ensure it exists', () => {
|
||||
apiAuth().then((api) => {
|
||||
ensureMachineUserExists(api, testMachineUserNameRemove);
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete a machine', () => {
|
||||
cy.contains('tr', testMachineUserNameRemove, { timeout: 1000 })
|
||||
.find('button')
|
||||
//force due to angular hidden buttons
|
||||
.click({ force: true });
|
||||
cy.get('[e2e-data="confirm-dialog-input"]').type(username(testMachineUserNameRemove, Cypress.env('org')));
|
||||
cy.get('[e2e-data="confirm-dialog-button"]').click();
|
||||
cy.get('.data-e2e-success');
|
||||
cy.wait(200);
|
||||
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
120
console/cypress/integration/permissions/permissions.ts
Normal file
120
console/cypress/integration/permissions/permissions.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { apiAuth, apiCallProperties } from "../../support/api/apiauth";
|
||||
import { ensureProjectExists, ensureProjectResourceDoesntExist, Roles } from "../../support/api/projects";
|
||||
import { login, User } from "../../support/login/users";
|
||||
|
||||
describe('permissions', () => {
|
||||
|
||||
const testProjectName = 'e2eprojectpermission'
|
||||
const testAppName = 'e2eapppermission'
|
||||
const testRoleName = 'e2eroleundertestname'
|
||||
const testRoleDisplay = 'e2eroleundertestdisplay'
|
||||
const testRoleGroup = 'e2eroleundertestgroup'
|
||||
const testGrantName = 'e2egrantundertest'
|
||||
|
||||
;[User.OrgOwner].forEach(user => {
|
||||
|
||||
describe(`as user "${user}"`, () => {
|
||||
|
||||
var api: apiCallProperties
|
||||
var projectId: number
|
||||
|
||||
beforeEach(() => {
|
||||
login(user)
|
||||
apiAuth().then(apiCalls => {
|
||||
api = apiCalls
|
||||
ensureProjectExists(apiCalls, testProjectName).then(projId => {
|
||||
projectId = projId
|
||||
cy.visit(`${Cypress.env('consoleUrl')}/projects/${projId}`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe('add role', () => {
|
||||
beforeEach(()=> {
|
||||
ensureProjectResourceDoesntExist(api, projectId, Roles, testRoleName)
|
||||
})
|
||||
|
||||
it('should add a role', () => {
|
||||
cy.get('[data-e2e="add-new-role"]').click()
|
||||
cy.get('[formcontrolname="key"]').type(testRoleName)
|
||||
cy.get('[formcontrolname="displayName"]').type(testRoleDisplay)
|
||||
cy.get('[formcontrolname="group"]').type(testRoleGroup)
|
||||
cy.get('[data-e2e="save-button"]').click()
|
||||
cy.get('.data-e2e-success')
|
||||
cy.wait(200)
|
||||
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
/*
|
||||
|
||||
describe('permissions', () => {
|
||||
|
||||
before(()=> {
|
||||
// cy.consolelogin(Cypress.env('username'), Cypress.env('password'), Cypress.env('consoleUrl'))
|
||||
})
|
||||
|
||||
it('should show projects ', () => {
|
||||
cy.visit(Cypress.env('consoleUrl') + '/projects')
|
||||
cy.url().should('contain', '/projects')
|
||||
})
|
||||
|
||||
it('should add a role', () => {
|
||||
cy.visit(Cypress.env('consoleUrl') + '/org').then(() => {
|
||||
cy.url().should('contain', '/org');
|
||||
})
|
||||
cy.visit(Cypress.env('consoleUrl') + '/projects').then(() => {
|
||||
cy.url().should('contain', '/projects');
|
||||
cy.get('.card').should('contain.text', "newProjectToTest")
|
||||
})
|
||||
cy.get('.card').filter(':contains("newProjectToTest")').click()
|
||||
cy.get('.app-container').filter(':contains("newAppToTest")').should('be.visible').click()
|
||||
let projectID
|
||||
cy.url().then(url => {
|
||||
cy.log(url.split('/')[4])
|
||||
projectID = url.split('/')[4]
|
||||
});
|
||||
|
||||
cy.then(() => cy.visit(Cypress.env('consoleUrl') + '/projects/' + projectID +'/roles/create'))
|
||||
cy.get('[formcontrolname^=key]').type("newdemorole")
|
||||
cy.get('[formcontrolname^=displayName]').type("newdemodisplayname")
|
||||
cy.get('[formcontrolname^=group]').type("newdemogroupname")
|
||||
cy.get('button').filter(':contains("Save")').should('be.visible').click()
|
||||
//let the Role get processed
|
||||
cy.wait(5000)
|
||||
})
|
||||
|
||||
it('should add a grant', () => {
|
||||
cy.visit(Cypress.env('consoleUrl') + '/org').then(() => {
|
||||
cy.url().should('contain', '/org');
|
||||
})
|
||||
cy.visit(Cypress.env('consoleUrl') + '/projects').then(() => {
|
||||
cy.url().should('contain', '/projects');
|
||||
cy.get('.card').should('contain.text', "newProjectToTest")
|
||||
})
|
||||
cy.get('.card').filter(':contains("newProjectToTest")').click()
|
||||
cy.get('.app-container').filter(':contains("newAppToTest")').should('be.visible').click()
|
||||
let projectID
|
||||
cy.url().then(url => {
|
||||
cy.log(url.split('/')[4])
|
||||
projectID = url.split('/')[4]
|
||||
});
|
||||
|
||||
cy.then(() => cy.visit(Cypress.env('consoleUrl') + '/grant-create/project/' + projectID ))
|
||||
cy.get('input').type("demo")
|
||||
cy.get('[role^=listbox]').filter(`:contains("${Cypress.env("fullUserName")}")`).should('be.visible').click()
|
||||
cy.wait(5000)
|
||||
//cy.get('.button').contains('Continue').click()
|
||||
cy.get('button').filter(':contains("Continue")').click()
|
||||
cy.wait(5000)
|
||||
cy.get('tr').filter(':contains("demo")').find('label').click()
|
||||
cy.get('button').filter(':contains("Save")').should('be.visible').click()
|
||||
//let the grant get processed
|
||||
cy.wait(5000)
|
||||
})
|
||||
})
|
||||
|
||||
*/
|
78
console/cypress/integration/projects/projects.ts
Normal file
78
console/cypress/integration/projects/projects.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { apiAuth } from '../../support/api/apiauth';
|
||||
import { ensureProjectDoesntExist, ensureProjectExists } from '../../support/api/projects';
|
||||
import { login, User } from '../../support/login/users';
|
||||
|
||||
describe('projects', () => {
|
||||
const testProjectNameCreate = 'e2eprojectcreate';
|
||||
const testProjectNameDeleteList = 'e2eprojectdeletelist';
|
||||
const testProjectNameDeleteGrid = 'e2eprojectdeletegrid';
|
||||
|
||||
[User.OrgOwner].forEach((user) => {
|
||||
describe(`as user "${user}"`, () => {
|
||||
beforeEach(() => {
|
||||
login(user);
|
||||
});
|
||||
|
||||
describe('add project', () => {
|
||||
beforeEach(`ensure it doesn't exist already`, () => {
|
||||
apiAuth().then((api) => {
|
||||
ensureProjectDoesntExist(api, testProjectNameCreate);
|
||||
});
|
||||
cy.visit(`${Cypress.env('consoleUrl')}/projects`);
|
||||
});
|
||||
|
||||
it('should add a project', () => {
|
||||
cy.get('.add-project-button').click({ force: true });
|
||||
cy.get('input').type(testProjectNameCreate);
|
||||
cy.get('[data-e2e="continue-button"]').click();
|
||||
cy.get('.data-e2e-success');
|
||||
cy.wait(200);
|
||||
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove project', () => {
|
||||
describe('list view', () => {
|
||||
beforeEach('ensure it exists', () => {
|
||||
apiAuth().then((api) => {
|
||||
ensureProjectExists(api, testProjectNameDeleteList);
|
||||
});
|
||||
cy.visit(`${Cypress.env('consoleUrl')}/projects`);
|
||||
});
|
||||
|
||||
it('removes the project', () => {
|
||||
cy.get('[data-e2e=toggle-grid]').click();
|
||||
cy.get('[data-cy=timestamp]');
|
||||
cy.contains('tr', testProjectNameDeleteList, { timeout: 1000 })
|
||||
.find('[data-e2e=delete-project-button]')
|
||||
.click({ force: true });
|
||||
cy.get('[e2e-data="confirm-dialog-button"]').click();
|
||||
cy.get('.data-e2e-success');
|
||||
cy.wait(200);
|
||||
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('grid view', () => {
|
||||
beforeEach('ensure it exists', () => {
|
||||
apiAuth().then((api) => {
|
||||
ensureProjectExists(api, testProjectNameDeleteGrid);
|
||||
});
|
||||
cy.visit(`${Cypress.env('consoleUrl')}/projects`);
|
||||
});
|
||||
|
||||
it('removes the project', () => {
|
||||
cy.contains('[data-e2e=grid-card]', testProjectNameDeleteGrid)
|
||||
.find('[data-e2e=delete-project-button]')
|
||||
.trigger('mouseover')
|
||||
.click();
|
||||
cy.get('[e2e-data="confirm-dialog-button"]').click();
|
||||
cy.get('.data-e2e-success');
|
||||
cy.wait(200);
|
||||
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
0
console/cypress/integration/register/register.ts
Normal file
0
console/cypress/integration/register/register.ts
Normal file
45
console/cypress/integration/settings/login-policy.ts
Normal file
45
console/cypress/integration/settings/login-policy.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { apiAuth } from "../../support/api/apiauth";
|
||||
import { ensureHumanUserExists } from "../../support/api/users";
|
||||
import { login, User } from "../../support/login/users";
|
||||
|
||||
describe("login policy", ()=> {
|
||||
|
||||
const orgPath = `${Cypress.env('consoleUrl')}/org`
|
||||
|
||||
;[User.OrgOwner].forEach(user => {
|
||||
|
||||
describe(`as user "${user}"`, () => {
|
||||
|
||||
beforeEach(()=> {
|
||||
login(user)
|
||||
cy.visit(orgPath)
|
||||
// TODO: Why force?
|
||||
cy.contains('[data-e2e=policy-card]', 'Login Policy').contains('button', 'Modify').click({force: true}) // TODO: select data-e2e
|
||||
apiAuth().then(api => {
|
||||
ensureHumanUserExists(api, User.LoginPolicyUser)
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: verify email
|
||||
|
||||
it.skip(`username and password disallowed`, () => {
|
||||
login(User.LoginPolicyUser, "123abcABC?&*")
|
||||
})
|
||||
it(`registering is allowed`)
|
||||
it(`registering is disallowed`)
|
||||
it(`login by an external IDP is allowed`)
|
||||
it(`login by an external IDP is disallowed`)
|
||||
it(`MFA is forced`)
|
||||
it(`MFA is not forced`)
|
||||
it(`the password reset option is hidden`)
|
||||
it(`the password reset option is shown`)
|
||||
it(`passwordless login is allowed`)
|
||||
it(`passwordless login is disallowed`)
|
||||
describe('identity providers', () => {
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
34
console/cypress/integration/settings/password-complexity.ts
Normal file
34
console/cypress/integration/settings/password-complexity.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { login, User } from "../../support/login/users";
|
||||
|
||||
describe("password complexity", ()=> {
|
||||
|
||||
const orgPath = `${Cypress.env('consoleUrl')}/org`
|
||||
const testProjectName = 'e2eproject'
|
||||
|
||||
;[User.OrgOwner].forEach(user => {
|
||||
|
||||
describe(`as user "${user}"`, () => {
|
||||
|
||||
beforeEach(()=> {
|
||||
login(user)
|
||||
cy.visit(orgPath)
|
||||
// TODO: Why force?
|
||||
cy.contains('[data-e2e=policy-card]', 'Password Complexity').contains('button', 'Modify').click({force: true}) // TODO: select data-e2e
|
||||
})
|
||||
|
||||
// TODO: fix saving password complexity policy bug
|
||||
|
||||
it(`should restrict passwords that don't have the minimal length`)
|
||||
it(`should require passwords to contain a number if option is switched on`)
|
||||
it(`should not require passwords to contain a number if option is switched off`)
|
||||
it(`should require passwords to contain a symbol if option is switched on`)
|
||||
it(`should not require passwords to contain a symbol if option is switched off`)
|
||||
it(`should require passwords to contain a lowercase letter if option is switched on`)
|
||||
it(`should not require passwords to contain a lowercase letter if option is switched off`)
|
||||
it(`should require passwords to contain an uppercase letter if option is switched on`)
|
||||
it(`should not require passwords to contain an uppercase letter if option is switched off`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
85
console/cypress/integration/settings/private-labeling.ts
Normal file
85
console/cypress/integration/settings/private-labeling.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { apiAuth, apiCallProperties } from "../../support/api/apiauth";
|
||||
import { Policy, resetPolicy } from "../../support/api/policies";
|
||||
import { login, User } from "../../support/login/users";
|
||||
|
||||
describe("private labeling", ()=> {
|
||||
|
||||
const orgPath = `${Cypress.env('consoleUrl')}/org`
|
||||
|
||||
;[User.OrgOwner].forEach(user => {
|
||||
|
||||
describe(`as user "${user}"`, () => {
|
||||
|
||||
let api: apiCallProperties
|
||||
|
||||
|
||||
beforeEach(()=> {
|
||||
login(user)
|
||||
cy.visit(orgPath)
|
||||
// TODO: Why force?
|
||||
cy.contains('[data-e2e=policy-card]', 'Private Labeling').contains('button', 'Modify').click({force: true}) // TODO: select data-e2e
|
||||
})
|
||||
|
||||
customize('white', user)
|
||||
customize('dark', user)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
function customize(theme: string, user: User) {
|
||||
|
||||
describe(`${theme} theme`, () => {
|
||||
|
||||
beforeEach(() => {
|
||||
apiAuth().then(api => {
|
||||
resetPolicy(api, Policy.Label)
|
||||
})
|
||||
})
|
||||
|
||||
describe.skip('logo', () => {
|
||||
|
||||
beforeEach('expand logo category', () => {
|
||||
cy.contains('[data-e2e=policy-category]', 'Logo').click() // TODO: select data-e2e
|
||||
cy.fixture('logo.png').as('logo')
|
||||
})
|
||||
|
||||
it('should update a logo', () => {
|
||||
cy.get('[data-e2e=image-part-logo]').find('input').then(function (el) {
|
||||
const blob = Cypress.Blob.base64StringToBlob(this.logo, 'image/png')
|
||||
const file = new File([blob], 'images/logo.png', { type: 'image/png' })
|
||||
const list = new DataTransfer()
|
||||
|
||||
list.items.add(file)
|
||||
const myFileList = list.files
|
||||
|
||||
el[0].files = myFileList
|
||||
el[0].dispatchEvent(new Event('change', { bubbles: true }))
|
||||
})
|
||||
})
|
||||
it('should delete a logo')
|
||||
})
|
||||
it('should update an icon')
|
||||
it('should delete an icon')
|
||||
it.skip('should update the background color', () => {
|
||||
cy.contains('[data-e2e=color]', 'Background Color').find('button').click() // TODO: select data-e2e
|
||||
cy.get('color-editable-input').find('input').clear().type('#ae44dc')
|
||||
cy.get('[data-e2e=save-colors-button]').click()
|
||||
cy.get('[data-e2e=header-user-avatar]').click()
|
||||
cy.contains('Logout All Users').click() // TODO: select data-e2e
|
||||
login(User.LoginPolicyUser, true, null, () => {
|
||||
cy.pause()
|
||||
})
|
||||
})
|
||||
it('should update the primary color')
|
||||
it('should update the warning color')
|
||||
it('should update the font color')
|
||||
it('should update the font style')
|
||||
it('should hide the loginname suffix')
|
||||
it('should show the loginname suffix')
|
||||
it('should hide the watermark')
|
||||
it('should show the watermark')
|
||||
it('should show the current configuration')
|
||||
it('should reset the policy')
|
||||
})
|
||||
}
|
15
console/cypress/plugins/index.ts
Normal file
15
console/cypress/plugins/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
module.exports = (on, config) => {
|
||||
|
||||
require('cypress-terminal-report/src/installLogsPrinter')(on);
|
||||
|
||||
config.defaultCommandTimeout = 10_000
|
||||
|
||||
config.env.parsedServiceAccountKey = config.env.serviceAccountKey
|
||||
if (config.env.serviceAccountKeyPath) {
|
||||
config.env.parsedServiceAccountKey = JSON.parse(readFileSync(config.env.serviceAccountKeyPath, 'utf-8'))
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
49
console/cypress/support/api/apiauth.ts
Normal file
49
console/cypress/support/api/apiauth.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { sign } from 'jsonwebtoken'
|
||||
|
||||
export interface apiCallProperties {
|
||||
authHeader: string
|
||||
mgntBaseURL: string
|
||||
}
|
||||
|
||||
export function apiAuth(): Cypress.Chainable<apiCallProperties> {
|
||||
const apiUrl = Cypress.env('apiUrl')
|
||||
const issuerUrl = Cypress.env('issuerUrl')
|
||||
const zitadelProjectResourceID = (<string>Cypress.env('zitadelProjectResourceId')).replace('bignumber-', '')
|
||||
|
||||
const key = Cypress.env("parsedServiceAccountKey")
|
||||
|
||||
const now = new Date().getTime()
|
||||
const iat = Math.floor(now / 1000)
|
||||
const exp = Math.floor(new Date(now + 1000 * 60 * 55).getTime() / 1000) // 55 minutes
|
||||
const bearerToken = sign({
|
||||
iss: key.userId,
|
||||
sub: key.userId,
|
||||
aud: `${issuerUrl}`,
|
||||
iat: iat,
|
||||
exp: exp
|
||||
}, key.key, {
|
||||
header: {
|
||||
alg: "RS256",
|
||||
kid: key.keyId
|
||||
}
|
||||
})
|
||||
|
||||
return cy.request({
|
||||
method: 'POST',
|
||||
url: `${apiUrl}/oauth/v2/token`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: {
|
||||
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||
scope: `openid urn:zitadel:iam:org:project:id:${zitadelProjectResourceID}:aud`,
|
||||
assertion: bearerToken,
|
||||
}
|
||||
}).its('body.access_token').then(token => {
|
||||
|
||||
return <apiCallProperties>{
|
||||
authHeader: `Bearer ${token}`,
|
||||
mgntBaseURL: `${apiUrl}/management/v1/`,
|
||||
}
|
||||
})
|
||||
}
|
86
console/cypress/support/api/ensure.ts
Normal file
86
console/cypress/support/api/ensure.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { apiCallProperties } from "./apiauth"
|
||||
|
||||
export function ensureSomethingExists(api: apiCallProperties, searchPath: string, find: (entity: any) => boolean, createPath: string, body: any): Cypress.Chainable<number> {
|
||||
|
||||
return searchSomething(api, searchPath, find).then(sRes => {
|
||||
if (sRes.entity) {
|
||||
return cy.wrap({
|
||||
id: sRes.entity.id,
|
||||
initialSequence: 0
|
||||
})
|
||||
}
|
||||
return cy.request({
|
||||
method: 'POST',
|
||||
url: `${api.mgntBaseURL}${createPath}`,
|
||||
headers: {
|
||||
Authorization: api.authHeader
|
||||
},
|
||||
body: body,
|
||||
failOnStatusCode: false,
|
||||
followRedirect: false,
|
||||
}).then(cRes => {
|
||||
expect(cRes.status).to.equal(200)
|
||||
return {
|
||||
id: cRes.body.id,
|
||||
initialSequence: sRes.sequence
|
||||
}
|
||||
})
|
||||
}).then((data) => {
|
||||
awaitDesired(30, (entity) => !!entity, data.initialSequence, api, searchPath, find)
|
||||
return cy.wrap<number>(data.id)
|
||||
})
|
||||
}
|
||||
|
||||
export function ensureSomethingDoesntExist(api: apiCallProperties, searchPath: string, find: (entity: any) => boolean, deletePath: (entity: any) => string): Cypress.Chainable<null> {
|
||||
|
||||
return searchSomething(api, searchPath, find).then(sRes => {
|
||||
if (!sRes.entity) {
|
||||
return cy.wrap(0)
|
||||
}
|
||||
return cy.request({
|
||||
method: 'DELETE',
|
||||
url: `${api.mgntBaseURL}${deletePath(sRes.entity)}`,
|
||||
headers: {
|
||||
Authorization: api.authHeader
|
||||
},
|
||||
failOnStatusCode: false
|
||||
}).then((dRes) => {
|
||||
expect(dRes.status).to.equal(200)
|
||||
return sRes.sequence
|
||||
})
|
||||
}).then((initialSequence) => {
|
||||
awaitDesired(30, (entity) => !entity , initialSequence, api, searchPath, find)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
type SearchResult = {
|
||||
entity: any
|
||||
sequence: number
|
||||
}
|
||||
|
||||
function searchSomething(api: apiCallProperties, searchPath: string, find: (entity: any) => boolean): Cypress.Chainable<SearchResult> {
|
||||
|
||||
return cy.request({
|
||||
method: 'POST',
|
||||
url: `${api.mgntBaseURL}${searchPath}`,
|
||||
headers: {
|
||||
Authorization: api.authHeader
|
||||
},
|
||||
}).then(res => {
|
||||
return {
|
||||
entity: res.body.result?.find(find) || null,
|
||||
sequence: res.body.details.processedSequence
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function awaitDesired(trials: number, expectEntity: (entity: any) => boolean, initialSequence: number, api: apiCallProperties, searchPath: string, find: (entity: any) => boolean) {
|
||||
searchSomething(api, searchPath, find).then(resp => {
|
||||
if (!expectEntity(resp.entity) || resp.sequence <= initialSequence) {
|
||||
expect(trials, `trying ${trials} more times`).to.be.greaterThan(0);
|
||||
cy.wait(1000)
|
||||
awaitDesired(trials - 1, expectEntity, initialSequence, api, searchPath, find)
|
||||
}
|
||||
})
|
||||
}
|
19
console/cypress/support/api/policies.ts
Normal file
19
console/cypress/support/api/policies.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { apiCallProperties } from "./apiauth"
|
||||
|
||||
|
||||
export enum Policy {
|
||||
Label = "label"
|
||||
}
|
||||
|
||||
export function resetPolicy(api: apiCallProperties, policy: Policy) {
|
||||
cy.request({
|
||||
method: 'DELETE',
|
||||
url: `${api.mgntBaseURL}/policies/${policy}`,
|
||||
headers: {
|
||||
Authorization: api.authHeader
|
||||
},
|
||||
}).then(res => {
|
||||
expect(res.status).to.equal(200)
|
||||
return null
|
||||
})
|
||||
}
|
80
console/cypress/support/api/projects.ts
Normal file
80
console/cypress/support/api/projects.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { apiCallProperties } from "./apiauth"
|
||||
import { ensureSomethingDoesntExist, ensureSomethingExists } from "./ensure"
|
||||
|
||||
export function ensureProjectExists(api: apiCallProperties, projectName: string): Cypress.Chainable<number> {
|
||||
|
||||
return ensureSomethingExists(
|
||||
api,
|
||||
`projects/_search`,
|
||||
(project: any) => project.name === projectName,
|
||||
'projects',
|
||||
{ name: projectName },
|
||||
)
|
||||
}
|
||||
|
||||
export function ensureProjectDoesntExist(api: apiCallProperties, projectName: string): Cypress.Chainable<null> {
|
||||
|
||||
return ensureSomethingDoesntExist(
|
||||
api,
|
||||
`projects/_search`,
|
||||
(project: any) => project.name === projectName,
|
||||
(project) => `projects/${project.id}`,
|
||||
)
|
||||
}
|
||||
|
||||
class ResourceType {
|
||||
constructor(
|
||||
public resourcePath: string,
|
||||
public compareProperty: string,
|
||||
public identifierProperty: string,
|
||||
){}
|
||||
}
|
||||
|
||||
export const Apps = new ResourceType('apps', 'name', 'id')
|
||||
export const Roles = new ResourceType('roles', 'key', 'key')
|
||||
//export const Grants = new ResourceType('apps', 'name')
|
||||
|
||||
|
||||
export function ensureProjectResourceDoesntExist(api: apiCallProperties, projectId: number, resourceType: ResourceType, resourceName: string): Cypress.Chainable<null> {
|
||||
return ensureSomethingDoesntExist(
|
||||
api,
|
||||
`projects/${projectId}/${resourceType.resourcePath}/_search`,
|
||||
(resource: any) => {
|
||||
return resource[resourceType.compareProperty] === resourceName
|
||||
},
|
||||
(resource) => {
|
||||
return `projects/${projectId}/${resourceType.resourcePath}/${resource[resourceType.identifierProperty]}`
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function ensureApplicationExists(api: apiCallProperties, projectId: number, appName: string): Cypress.Chainable<number> {
|
||||
|
||||
return ensureSomethingExists(
|
||||
api,
|
||||
`projects/${projectId}/${Apps.resourcePath}/_search`,
|
||||
(resource: any) => resource.name === appName,
|
||||
`projects/${projectId}/${Apps.resourcePath}/oidc`,
|
||||
{
|
||||
name: appName,
|
||||
redirectUris: [
|
||||
'https://e2eredirecturl.org'
|
||||
],
|
||||
responseTypes: [
|
||||
"OIDC_RESPONSE_TYPE_CODE"
|
||||
],
|
||||
grantTypes: [
|
||||
"OIDC_GRANT_TYPE_AUTHORIZATION_CODE"
|
||||
],
|
||||
authMethodType: "OIDC_AUTH_METHOD_TYPE_NONE",
|
||||
postLogoutRedirectUris: [
|
||||
'https://e2elogoutredirecturl.org'
|
||||
],
|
||||
/* "clientId": "129383004379407963@e2eprojectpermission",
|
||||
"clockSkew": "0s",
|
||||
"allowedOrigins": [
|
||||
"https://testurl.org"
|
||||
]*/
|
||||
},
|
||||
)
|
||||
}
|
49
console/cypress/support/api/users.ts
Normal file
49
console/cypress/support/api/users.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { apiCallProperties } from "./apiauth"
|
||||
import { ensureSomethingDoesntExist, ensureSomethingExists } from "./ensure"
|
||||
|
||||
export function ensureHumanUserExists(api: apiCallProperties, username: string): Cypress.Chainable<number> {
|
||||
|
||||
return ensureSomethingExists(
|
||||
api,
|
||||
'users/_search',
|
||||
(user: any) => user.userName === username,
|
||||
'users/human',
|
||||
{
|
||||
user_name: username,
|
||||
profile: {
|
||||
first_name: 'e2efirstName',
|
||||
last_name: 'e2elastName',
|
||||
},
|
||||
email: {
|
||||
email: 'e2e@email.ch',
|
||||
},
|
||||
phone: {
|
||||
phone: '+41 123456789',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function ensureMachineUserExists(api: apiCallProperties, username: string): Cypress.Chainable<number> {
|
||||
|
||||
return ensureSomethingExists(
|
||||
api,
|
||||
'users/_search',
|
||||
(user: any) => user.userName === username,
|
||||
'users/machine',
|
||||
{
|
||||
user_name: username,
|
||||
name: 'e2emachinename',
|
||||
description: 'e2emachinedescription',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export function ensureUserDoesntExist(api: apiCallProperties, username: string): Cypress.Chainable<null> {
|
||||
|
||||
return ensureSomethingDoesntExist(
|
||||
api,
|
||||
'users/_search',
|
||||
(user: any) => user.userName === username,
|
||||
(user) => `users/${user.id}`
|
||||
)
|
||||
}
|
26
console/cypress/support/commands.ts
Normal file
26
console/cypress/support/commands.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
*/
|
||||
/**
|
||||
* Custom command that authenticates a user.
|
||||
*
|
||||
* @example cy.consolelogin('hodor', 'hodor1234')
|
||||
*/
|
||||
/* consolelogin(username: string, password: string): void
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('consolelogin', { prevSubject: false }, (username: string, password: string) => {
|
||||
|
||||
window.sessionStorage.removeItem("zitadel:access_token")
|
||||
cy.visit(Cypress.env('consoleUrl')).then(() => {
|
||||
// fill the fields and push button
|
||||
cy.get('#loginName').type(username, { log: false })
|
||||
cy.get('#submit-button').click()
|
||||
cy.get('#password').type(password, { log: false })
|
||||
cy.get('#submit-button').click()
|
||||
cy.location('pathname', {timeout: 5 * 1000}).should('eq', '/');
|
||||
})
|
||||
})
|
||||
*/
|
12
console/cypress/support/index.ts
Normal file
12
console/cypress/support/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
require('cypress-terminal-report/src/installLogsCollector')();
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
module.exports = (on, config) => {
|
||||
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
//import './commands'
|
||||
|
164
console/cypress/support/login/users.ts
Normal file
164
console/cypress/support/login/users.ts
Normal file
@ -0,0 +1,164 @@
|
||||
export enum User {
|
||||
OrgOwner = 'org_owner',
|
||||
OrgOwnerViewer = 'org_owner_viewer',
|
||||
OrgProjectCreator = 'org_project_creator',
|
||||
LoginPolicyUser = 'login_policy_user',
|
||||
PasswordComplexityUser = 'password_complexity_user',
|
||||
IAMAdminUser = "zitadel-admin"
|
||||
}
|
||||
|
||||
export function login(user:User, force?: boolean, pw?: string, onUsernameScreen?: () => void, onPasswordScreen?: () => void, onAuthenticated?: () => void): void {
|
||||
let creds = credentials(user, pw)
|
||||
|
||||
const accountsUrl: string = Cypress.env('accountsUrl')
|
||||
const consoleUrl: string = Cypress.env('consoleUrl')
|
||||
const otherZitadelIdpInstance: boolean = Cypress.env('otherZitadelIdpInstance')
|
||||
|
||||
cy.session(creds.username, () => {
|
||||
|
||||
const cookies = new Map<string, string>()
|
||||
|
||||
if (otherZitadelIdpInstance) {
|
||||
cy.intercept({
|
||||
method: 'GET',
|
||||
url: `${accountsUrl}/login*`,
|
||||
times: 1
|
||||
}, (req) => {
|
||||
req.headers['cookie'] = requestCookies(cookies)
|
||||
req.continue((res) => {
|
||||
updateCookies(res.headers['set-cookie'] as string[], cookies)
|
||||
})
|
||||
}).as('login')
|
||||
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: `${accountsUrl}/loginname*`,
|
||||
times: 1
|
||||
}, (req) => {
|
||||
req.headers['cookie'] = requestCookies(cookies)
|
||||
req.continue((res) => {
|
||||
updateCookies(res.headers['set-cookie'] as string[], cookies)
|
||||
})
|
||||
}).as('loginName')
|
||||
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: `${accountsUrl}/password*`,
|
||||
times: 1
|
||||
}, (req) => {
|
||||
req.headers['cookie'] = requestCookies(cookies)
|
||||
req.continue((res) => {
|
||||
updateCookies(res.headers['set-cookie'] as string[], cookies)
|
||||
})
|
||||
}).as('password')
|
||||
|
||||
cy.intercept({
|
||||
method: 'GET',
|
||||
url: `${accountsUrl}/success*`,
|
||||
times: 1
|
||||
}, (req) => {
|
||||
req.headers['cookie'] = requestCookies(cookies)
|
||||
req.continue((res) => {
|
||||
updateCookies(res.headers['set-cookie'] as string[], cookies)
|
||||
})
|
||||
}).as('success')
|
||||
|
||||
cy.intercept({
|
||||
method: 'GET',
|
||||
url: `${accountsUrl}/oauth/v2/authorize/callback*`,
|
||||
times: 1
|
||||
}, (req) => {
|
||||
req.headers['cookie'] = requestCookies(cookies)
|
||||
req.continue((res) => {
|
||||
updateCookies(res.headers['set-cookie'] as string[], cookies)
|
||||
})
|
||||
}).as('callback')
|
||||
|
||||
cy.intercept({
|
||||
method: 'GET',
|
||||
url: `${accountsUrl}/oauth/v2/authorize*`,
|
||||
times: 1,
|
||||
}, (req) => {
|
||||
req.continue((res) => {
|
||||
updateCookies(res.headers['set-cookie'] as string[], cookies)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
cy.visit(`${consoleUrl}/loginname`, { retryOnNetworkFailure: true });
|
||||
|
||||
otherZitadelIdpInstance && cy.wait('@login')
|
||||
onUsernameScreen ? onUsernameScreen() : null
|
||||
cy.get('#loginName').type(creds.username)
|
||||
cy.get('#submit-button').click()
|
||||
|
||||
otherZitadelIdpInstance && cy.wait('@loginName')
|
||||
onPasswordScreen ? onPasswordScreen() : null
|
||||
cy.get('#password').type(creds.password)
|
||||
cy.get('#submit-button').click()
|
||||
|
||||
onAuthenticated ? onAuthenticated() : null
|
||||
|
||||
otherZitadelIdpInstance && cy.wait('@callback')
|
||||
|
||||
cy.location('pathname', {timeout: 5 * 1000}).should('eq', '/');
|
||||
|
||||
}, {
|
||||
validate: () => {
|
||||
|
||||
if (force) {
|
||||
throw new Error("clear session");
|
||||
}
|
||||
|
||||
cy.visit(`${consoleUrl}/users/me`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function username(withoutDomain: string, project?: string): string {
|
||||
return `${withoutDomain}@${project ? `${project}.` : ''}${host(Cypress.env('apiUrl')).replace('api.', '')}`
|
||||
}
|
||||
|
||||
function credentials(user: User, pw?: string) {
|
||||
const isAdmin = user == User.IAMAdminUser
|
||||
return {
|
||||
username: username(isAdmin ? user : `${user}_user_name`, isAdmin ? 'caos-ag' : Cypress.env('org')),
|
||||
password: pw ? pw : Cypress.env(`${user}_password`)
|
||||
}
|
||||
}
|
||||
|
||||
function updateCookies(newCookies: string[] | undefined, currentCookies: Map<string, string>) {
|
||||
if (newCookies === undefined) {
|
||||
return
|
||||
}
|
||||
newCookies.forEach(cs => {
|
||||
cs.split('; ').forEach(cookie => {
|
||||
const idx = cookie.indexOf('=')
|
||||
currentCookies.set(cookie.substring(0,idx), cookie.substring(idx+1))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function requestCookies(currentCookies: Map<string, string>): string[] {
|
||||
let list: Array<string> = []
|
||||
currentCookies.forEach((val, key) => {
|
||||
list.push(key+"="+val)
|
||||
})
|
||||
return list
|
||||
}
|
||||
|
||||
export function host(url: string): string {
|
||||
return stripPort(stripProtocol(url))
|
||||
}
|
||||
|
||||
function stripPort(s: string): string {
|
||||
const idx = s.indexOf(":")
|
||||
return idx === -1 ? s : s.substring(0,idx)
|
||||
}
|
||||
|
||||
function stripProtocol(url: string): string {
|
||||
return url.replace('http://', '').replace('https://', '')
|
||||
}
|
||||
|
8
console/cypress/tsconfig.json
Normal file
8
console/cypress/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["es5", "dom"],
|
||||
"types": ["cypress"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
14
console/e2e.env
Normal file
14
console/e2e.env
Normal file
@ -0,0 +1,14 @@
|
||||
E2E_CYPRESS_PORT=5003
|
||||
E2E_ORG=e2e-tests
|
||||
E2E_ORG_OWNER_PW=Password1!
|
||||
E2E_ORG_OWNER_VIEWER_PW=Password1!
|
||||
E2E_ORG_PROJECT_CREATOR_PW=Password1!
|
||||
E2E_PASSWORD_COMPLEXITY_USER_PW=Password1!
|
||||
E2E_LOGIN_POLICY_USER_PW=Password1!
|
||||
E2E_SERVICEACCOUNT_KEY_PATH="${projectRoot}/.keys/e2e.json"
|
||||
E2E_CONSOLE_URL="http://localhost:4200"
|
||||
E2E_API_URL="http://localhost:50002"
|
||||
E2E_ACCOUNTS_URL="http://localhost:50003"
|
||||
E2E_ISSUER_URL="http://localhost:50002/oauth/v2"
|
||||
E2E_OTHER_ZITADEL_IDP_INSTANCE=false
|
||||
E2E_ZITADEL_PROJECT_RESOURCE_ID="bignumber-$(echo -n $(docker compose -f ${projectRoot}/build/local/docker-compose-local.yml exec --no-TTY db cockroach sql --insecure --execute "select aggregate_id from eventstore.events where event_type = 'project.added' and event_data = '{\"name\": \"Zitadel\"}';" --format tsv) | cut -d " " -f 2)"
|
@ -1,32 +0,0 @@
|
||||
// @ts-check
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
/**
|
||||
* @type { import("protractor").Config }
|
||||
*/
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
browserName: 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
};
|
@ -1,26 +0,0 @@
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
import { AppPage } from './app.po';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getTitleText()).toEqual('console app is running!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(
|
||||
jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE
|
||||
} as logging.Entry)
|
||||
);
|
||||
});
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo() {
|
||||
return browser.get(browser.baseUrl) as Promise<any>;
|
||||
}
|
||||
|
||||
getTitleText() {
|
||||
return element(by.css('cnsl-root .content span')).getText() as Promise<string>;
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
23392
console/package-lock.json
generated
23392
console/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,25 +5,27 @@
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"prodbuild": "ng build --aot=true --buildOptimizer=true --base-href=/ui/console/",
|
||||
"lint": "ng lint && stylelint './src/**/*.scss' --syntax scss"
|
||||
"prodbuild": "ng build --configuration production --base-href=/ui/console/",
|
||||
"lint": "ng lint && stylelint './src/**/*.scss' --syntax scss",
|
||||
"e2e": "./cypress.sh run e2e.env",
|
||||
"e2e:open": "./cypress.sh open e2e.env"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~13.2.0",
|
||||
"@angular/cdk": "~13.2.0",
|
||||
"@angular/common": "~13.2.0",
|
||||
"@angular/compiler": "~13.2.0",
|
||||
"@angular/core": "~13.2.0",
|
||||
"@angular/forms": "~13.2.0",
|
||||
"@angular/material": "^13.2.0",
|
||||
"@angular/material-moment-adapter": "^13.2.0",
|
||||
"@angular/platform-browser": "~13.2.0",
|
||||
"@angular/platform-browser-dynamic": "~13.2.0",
|
||||
"@angular/router": "~13.2.0",
|
||||
"@angular/service-worker": "~13.2.0",
|
||||
"@angular/animations": "~13.2.5",
|
||||
"@angular/cdk": "~13.2.5",
|
||||
"@angular/common": "~13.2.5",
|
||||
"@angular/compiler": "~13.2.5",
|
||||
"@angular/core": "~13.2.5",
|
||||
"@angular/forms": "~13.2.5",
|
||||
"@angular/material": "~13.2.5",
|
||||
"@angular/material-moment-adapter": "~13.2.5",
|
||||
"@angular/platform-browser": "~13.2.5",
|
||||
"@angular/platform-browser-dynamic": "~13.2.5",
|
||||
"@angular/router": "~13.2.5",
|
||||
"@angular/service-worker": "~13.2.5",
|
||||
"@ctrl/ngx-codemirror": "^5.1.1",
|
||||
"@grpc/grpc-js": "^1.5.3",
|
||||
"@grpc/grpc-js": "^1.5.7",
|
||||
"@ngx-translate/core": "^14.0.0",
|
||||
"@ngx-translate/http-loader": "^7.0.0",
|
||||
"@types/file-saver": "^2.0.2",
|
||||
@ -34,9 +36,9 @@
|
||||
"cors": "^2.8.5",
|
||||
"file-saver": "^2.0.5",
|
||||
"google-proto-files": "^2.5.0",
|
||||
"google-protobuf": "^3.19.1",
|
||||
"google-protobuf": "^3.19.4",
|
||||
"grpc-web": "^1.3.0",
|
||||
"libphonenumber-js": "^1.9.44",
|
||||
"libphonenumber-js": "^1.9.49",
|
||||
"material-design-icons-iconfont": "^6.1.1",
|
||||
"moment": "^2.29.1",
|
||||
"ng-qrcode": "^6.0.0",
|
||||
@ -49,35 +51,39 @@
|
||||
"zone.js": "~0.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~13.2.0",
|
||||
"@angular-eslint/builder": "^13.0.1",
|
||||
"@angular-eslint/eslint-plugin": "^13.0.1",
|
||||
"@angular-eslint/eslint-plugin-template": "^13.0.1",
|
||||
"@angular-eslint/schematics": "^13.0.1",
|
||||
"@angular-eslint/template-parser": "^13.0.1",
|
||||
"@angular/cli": "~13.2.0",
|
||||
"@angular/compiler-cli": "~13.2.0",
|
||||
"@angular/language-service": "~13.2.0",
|
||||
"@angular-devkit/build-angular": "~13.2.5",
|
||||
"@angular-eslint/builder": "^13.1.0",
|
||||
"@angular-eslint/eslint-plugin": "^13.1.0",
|
||||
"@angular-eslint/eslint-plugin-template": "^13.1.0",
|
||||
"@angular-eslint/schematics": "^13.1.0",
|
||||
"@angular-eslint/template-parser": "^13.1.0",
|
||||
"@angular/cli": "~13.2.5",
|
||||
"@angular/compiler-cli": "~13.2.5",
|
||||
"@angular/language-service": "~13.2.5",
|
||||
"@types/jasmine": "~3.10.3",
|
||||
"@types/jasminewd2": "~2.0.10",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/node": "^17.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "5.10.0",
|
||||
"@typescript-eslint/parser": "5.10.0",
|
||||
"codelyzer": "^6.0.0",
|
||||
"eslint": "^8.7.0",
|
||||
"cypress": "^9.5.1",
|
||||
"cypress-terminal-report": "^3.4.1",
|
||||
"eslint": "^8.10.0",
|
||||
"jasmine-core": "~4.0.0",
|
||||
"jasmine-spec-reporter": "~7.0.0",
|
||||
"karma": "~6.3.6",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"karma": "~6.3.16",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.7.0",
|
||||
"mochawesome": "^7.1.2",
|
||||
"prettier": "^2.4.1",
|
||||
"protractor": "~7.0.0",
|
||||
"stylelint": "^13.10.0",
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
"stylelint-scss": "^3.21.0",
|
||||
"ts-node": "~10.2.1",
|
||||
"typescript": "^4.4.4"
|
||||
}
|
||||
}
|
||||
|
@ -10,40 +10,37 @@ import {
|
||||
trigger,
|
||||
} from '@angular/animations';
|
||||
|
||||
|
||||
export const toolbarAnimation: AnimationTriggerMetadata =
|
||||
trigger('toolbar', [
|
||||
transition(':enter', [
|
||||
export const toolbarAnimation: AnimationTriggerMetadata = trigger('toolbar', [
|
||||
transition(':enter', [
|
||||
style({
|
||||
transform: 'translateY(-100%)',
|
||||
opacity: 0,
|
||||
}),
|
||||
animate(
|
||||
'.2s ease-out',
|
||||
style({
|
||||
transform: 'translateY(-100%)',
|
||||
opacity: 0,
|
||||
transform: 'translateY(0%)',
|
||||
opacity: 1,
|
||||
}),
|
||||
animate(
|
||||
'.2s ease-out',
|
||||
style({
|
||||
transform: 'translateY(0%)',
|
||||
opacity: 1,
|
||||
}),
|
||||
),
|
||||
]),
|
||||
]);
|
||||
),
|
||||
]),
|
||||
]);
|
||||
|
||||
export const adminLineAnimation: AnimationTriggerMetadata =
|
||||
trigger('adminline', [
|
||||
transition(':enter', [
|
||||
export const adminLineAnimation: AnimationTriggerMetadata = trigger('adminline', [
|
||||
transition(':enter', [
|
||||
style({
|
||||
transform: 'translateY(100%)',
|
||||
opacity: 0.5,
|
||||
}),
|
||||
animate(
|
||||
'.2s ease-out',
|
||||
style({
|
||||
transform: 'translateY(100%)',
|
||||
opacity: 0.5,
|
||||
transform: 'translateY(0%)',
|
||||
opacity: 1,
|
||||
}),
|
||||
animate(
|
||||
'.2s ease-out',
|
||||
style({
|
||||
transform: 'translateY(0%)',
|
||||
opacity: 1,
|
||||
}),
|
||||
),
|
||||
]),
|
||||
]);
|
||||
),
|
||||
]),
|
||||
]);
|
||||
|
||||
export const accountCard: AnimationTriggerMetadata = trigger('accounts', [
|
||||
transition(':enter', [
|
||||
@ -64,11 +61,7 @@ export const accountCard: AnimationTriggerMetadata = trigger('accounts', [
|
||||
]);
|
||||
|
||||
export const navAnimations: Array<AnimationTriggerMetadata> = [
|
||||
trigger('navAnimation', [
|
||||
transition('* => *', [
|
||||
query('@navitem', stagger('50ms', animateChild()), { optional: true }),
|
||||
]),
|
||||
]),
|
||||
trigger('navAnimation', [transition('* => *', [query('@navitem', stagger('50ms', animateChild()), { optional: true })])]),
|
||||
trigger('navitem', [
|
||||
transition(':enter', [
|
||||
style({
|
||||
@ -95,7 +88,6 @@ export const navAnimations: Array<AnimationTriggerMetadata> = [
|
||||
]),
|
||||
];
|
||||
|
||||
|
||||
export const enterAnimations: Array<AnimationTriggerMetadata> = [
|
||||
trigger('appearfade', [
|
||||
transition(':enter', [
|
||||
@ -129,12 +121,10 @@ export const enterAnimations: Array<AnimationTriggerMetadata> = [
|
||||
|
||||
export const routeAnimations: AnimationTriggerMetadata = trigger('routeAnimations', [
|
||||
transition('HomePage => AddPage', [
|
||||
style({ transform: 'translateX(100%)', opacity: 0.5 }),
|
||||
style({ transform: 'translateX(50%)', opacity: 0.5 }),
|
||||
animate('250ms ease-out', style({ transform: 'translateX(0%)', opacity: 1 })),
|
||||
]),
|
||||
transition('AddPage => HomePage',
|
||||
[animate('250ms', style({ transform: 'translateX(100%)', opacity: 0.5 }))],
|
||||
),
|
||||
transition('AddPage => HomePage', [animate('250ms', style({ transform: 'translateX(50%)', opacity: 0.5 }))]),
|
||||
transition('HomePage => DetailPage', [
|
||||
query(':enter, :leave', style({ position: 'absolute', left: 0, right: 0 }), {
|
||||
optional: true,
|
||||
@ -159,13 +149,9 @@ export const routeAnimations: AnimationTriggerMetadata = trigger('routeAnimation
|
||||
optional: true,
|
||||
},
|
||||
),
|
||||
query(
|
||||
':leave',
|
||||
[style({ opacity: 1, width: '100%' }), animate('.35s ease-out', style({ opacity: 0 }))],
|
||||
{
|
||||
optional: true,
|
||||
},
|
||||
),
|
||||
query(':leave', [style({ opacity: 1, width: '100%' }), animate('.35s ease-out', style({ opacity: 0 }))], {
|
||||
optional: true,
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
transition('DetailPage => HomePage', [
|
||||
|
@ -4,6 +4,7 @@ import { QuicklinkStrategy } from 'ngx-quicklink';
|
||||
|
||||
import { AuthGuard } from './guards/auth.guard';
|
||||
import { RoleGuard } from './guards/role.guard';
|
||||
import { UserGrantContext } from './modules/user-grants/user-grants-datasource';
|
||||
import { OrgCreateComponent } from './pages/org-create/org-create.component';
|
||||
|
||||
const routes: Routes = [
|
||||
@ -12,14 +13,6 @@ const routes: Routes = [
|
||||
loadChildren: () => import('./pages/home/home.module').then((m) => m.HomeModule),
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: 'firststeps',
|
||||
loadChildren: () => import('./modules/onboarding/onboarding.module').then((m) => m.OnboardingModule),
|
||||
canActivate: [AuthGuard, RoleGuard],
|
||||
data: {
|
||||
roles: ['iam.write'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'granted-projects',
|
||||
loadChildren: () =>
|
||||
@ -31,7 +24,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'projects',
|
||||
loadChildren: () => import('./pages/projects/owned-projects/owned-projects.module').then((m) => m.OwnedProjectsModule),
|
||||
loadChildren: () => import('./pages/projects/projects.module').then((m) => m.ProjectsModule),
|
||||
canActivate: [AuthGuard, RoleGuard],
|
||||
data: {
|
||||
roles: ['project.read'],
|
||||
@ -41,22 +34,14 @@ const routes: Routes = [
|
||||
path: 'users',
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
loadChildren: () => import('src/app/pages/users/user-list/user-list.module').then((m) => m.UserListModule),
|
||||
canActivate: [RoleGuard],
|
||||
data: {
|
||||
roles: ['user.read'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('src/app/pages/users/user-detail/user-detail.module').then((m) => m.UserDetailModule),
|
||||
loadChildren: () => import('src/app/pages/users/users.module').then((m) => m.UsersModule),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'iam',
|
||||
path: 'system',
|
||||
loadChildren: () => import('./pages/iam/iam.module').then((m) => m.IamModule),
|
||||
canActivate: [AuthGuard, RoleGuard],
|
||||
data: {
|
||||
@ -93,6 +78,7 @@ const routes: Routes = [
|
||||
loadChildren: () => import('./pages/grants/grants.module').then((m) => m.GrantsModule),
|
||||
canActivate: [AuthGuard, RoleGuard],
|
||||
data: {
|
||||
context: UserGrantContext.NONE,
|
||||
roles: ['user.grant.read'],
|
||||
},
|
||||
},
|
||||
@ -138,6 +124,30 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'failed-events',
|
||||
loadChildren: () => import('./pages/failed-events/failed-events.module').then((m) => m.FailedEventsModule),
|
||||
canActivate: [AuthGuard, RoleGuard],
|
||||
data: {
|
||||
roles: ['iam.read'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'views',
|
||||
loadChildren: () => import('./pages/iam-views/iam-views.module').then((m) => m.IamViewsModule),
|
||||
canActivate: [AuthGuard, RoleGuard],
|
||||
data: {
|
||||
roles: ['iam.read'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'domains',
|
||||
loadChildren: () => import('./pages/domains/domains.module').then((m) => m.DomainsModule),
|
||||
canActivate: [AuthGuard, RoleGuard],
|
||||
data: {
|
||||
roles: ['org.read'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'signedout',
|
||||
loadChildren: () => import('./pages/signedout/signedout.module').then((m) => m.SignedoutModule),
|
||||
@ -153,6 +163,7 @@ const routes: Routes = [
|
||||
RouterModule.forRoot(routes, {
|
||||
preloadingStrategy: QuicklinkStrategy,
|
||||
relativeLinkResolution: 'legacy',
|
||||
scrollPositionRestoration: 'enabled',
|
||||
}),
|
||||
],
|
||||
exports: [RouterModule],
|
||||
|
@ -1,196 +1,18 @@
|
||||
<ng-container *ngIf="$any(authService.user | async) || {} as user">
|
||||
<ng-container *ngIf="(authService.user | async) || undefined as user">
|
||||
<ng-container *ngIf="((['iam.read$','iam.write$'] | hasRole)) as iamuser$">
|
||||
<mat-toolbar class="root-header">
|
||||
<button *ngIf="authenticationService.authenticated" aria-label="Toggle sidenav" mat-icon-button
|
||||
(click)="drawer.toggle()">
|
||||
<i class="icon las la-bars"></i>
|
||||
</button>
|
||||
<ng-container *ngIf="labelpolicy && !labelpolicy?.disableWatermark">
|
||||
<a class="title" [routerLink]="['/']">
|
||||
<img class="logo" alt="zitadel logo" *ngIf="componentCssClass === 'dark-theme'; else lighttheme"
|
||||
src="../assets/images/zitadel-logo-solo-light.svg" />
|
||||
<ng-template #lighttheme>
|
||||
<img alt="zitadel logo" class="logo" src="../assets/images/zitadel-logo-solo-dark.svg" />
|
||||
</ng-template>
|
||||
</a>
|
||||
<div class="main-container">
|
||||
<cnsl-header *ngIf="user" [org]="org" [user]="user" [isDarkTheme]="componentCssClass === 'dark-theme'"
|
||||
[labelpolicy]="labelpolicy" (changedActiveOrg)="changedOrg($event)"></cnsl-header>
|
||||
|
||||
<svg class="slash" viewBox="0 0 24 24" width="32" height="32" stroke="currentColor" stroke-width="1"
|
||||
stroke-linecap="round" stroke-linejoin="round" fill="none" shape-rendering="geometricPrecision">
|
||||
<path d="M16.88 3.549L7.12 20.451"></path>
|
||||
</svg>
|
||||
</ng-container>
|
||||
|
||||
<div class="org-context-wrapper" (clickOutside)="showOrgContext ? showOrgContext = false: null">
|
||||
<button class="org-button dontcloseonclick" (click)="showOrgContext = !showOrgContext" *ngIf="user && org"
|
||||
mat-button>{{org?.name
|
||||
?
|
||||
org.name : 'NO NAME'}}
|
||||
<mat-icon class="dontcloseonclick">
|
||||
arrow_drop_down</mat-icon>
|
||||
</button>
|
||||
<cnsl-org-context class="context_card" *ngIf="showOrgContext" (closedCard)="showOrgContext = false" [org]="org"
|
||||
(setOrg)="setActiveOrg($event)">
|
||||
</cnsl-org-context>
|
||||
<cnsl-nav id="mainnav" class="nav" [ngClass]="{ 'shadow': yoffset > 60}" *ngIf="user" [org]="org" [user]="user"
|
||||
[isDarkTheme]="componentCssClass === 'dark-theme'" [labelpolicy]="labelpolicy"></cnsl-nav>
|
||||
<div class="router-container" [@routeAnimations]="prepareRoute(outlet)">
|
||||
<div class="outlet">
|
||||
<router-outlet class="outlet" #outlet="outlet"></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="fill-space"></span>
|
||||
|
||||
<a class="doc-link" href="https://docs.zitadel.ch" mat-stroked-button target="_blank">{{'MENU.DOCUMENTATION'
|
||||
| translate}}</a>
|
||||
<div (clickOutside)="closeAccountCard()" class="icon-container">
|
||||
<cnsl-avatar
|
||||
*ngIf="user && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))"
|
||||
class="avatar dontcloseonclick" (click)="showAccount = !showAccount" [active]="showAccount"
|
||||
[avatarUrl]="user.human?.profile?.avatarUrl || ''" [forColor]="user?.preferredLoginName"
|
||||
[name]="user.human.profile.displayName ? user.human.profile.displayName : (user.human.profile.firstName + ' '+ user.human.profile.lastName)"
|
||||
[size]="38">
|
||||
</cnsl-avatar>
|
||||
<cnsl-accounts-card @accounts class="a_card mat-elevation-z1" *ngIf="showAccount"
|
||||
(closedCard)="showAccount = false" [user]="user" [iamuser]="iamuser$ | async">
|
||||
</cnsl-accounts-card>
|
||||
</div>
|
||||
</mat-toolbar>
|
||||
<mat-drawer-container class="main-container">
|
||||
<mat-drawer #drawer class="sidenav" [mode]="(isHandset$ | async) ? 'over' : 'side'"
|
||||
[opened]="(isHandset$ | async) === false && authenticationService.authenticated">
|
||||
<div class="side-column">
|
||||
<div class="list">
|
||||
<a @navitem class="nav-item" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }"
|
||||
[routerLink]="['/']">
|
||||
<i class="icon las la-home"></i>
|
||||
<span class="label">{{ 'MENU.DASHBOARD' | translate }}</span>
|
||||
</a>
|
||||
|
||||
<ng-container *ngIf="authenticationService.authenticationChanged | async">
|
||||
<a @navitem matTooltip="{{'MENU.TOOLTIP.PERSONAL' | translate}}" class="nav-item"
|
||||
[routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }"
|
||||
[routerLink]="['/users/me']">
|
||||
<i class="icon las la-user-circle"></i>
|
||||
<span class="label">{{ 'MENU.PERSONAL_INFO' | translate }}</span>
|
||||
</a>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="org" [@navAnimation]="org">
|
||||
<ng-template cnslHasRole [hasRole]="['org.read']">
|
||||
<div @navitem class="divider">
|
||||
<div class="line"></div>
|
||||
<span>{{org?.name ? org.name : ('MENU.ORGSECTION' | translate)}}</span>
|
||||
<div class="hiddenline"></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['org.read']">
|
||||
<a @navitem matTooltip="{{'MENU.TOOLTIP.ORG' | translate}}" class="nav-item"
|
||||
[routerLinkActive]="['active']" [routerLink]="[ '/org']">
|
||||
<i class="icon las la-cog"></i>
|
||||
<span class="label">{{'MENU.ORGANIZATION' | translate}}</span>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['project.read(:[0-9]*)?']">
|
||||
<a @navitem matTooltip="{{'MENU.TOOLTIP.SELFPROJECTS' | translate}}" class="nav-item"
|
||||
[routerLinkActive]="['active']" [routerLink]="[ '/projects']">
|
||||
<i class="icon las la-layer-group"></i>
|
||||
|
||||
<div class="c_label">
|
||||
<span> {{'MENU.PROJECT' | translate}} </span>
|
||||
<span *ngIf="(mgmtService?.ownedProjectsCount | async)"
|
||||
class="count">{{mgmtService?.ownedProjectsCount | async}}</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a @navitem matTooltip="{{'MENU.TOOLTIP.GRANTEDPROJECTS' | translate}}"
|
||||
*ngIf="mgmtService?.grantedProjectsCount && (mgmtService?.grantedProjectsCount | async)"
|
||||
class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/granted-projects']">
|
||||
<i class="icon las la-layer-group"></i>
|
||||
<div class="c_label">
|
||||
<span>{{ 'MENU.GRANTEDPROJECT' | translate }}</span>
|
||||
<span class="count">{{mgmtService?.grantedProjectsCount | async}}</span>
|
||||
</div>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['user.read(:[0-9]*)?']">
|
||||
<a @navitem matTooltip="{{'MENU.TOOLTIP.HUMANUSERS' | translate}}" class="nav-item"
|
||||
[routerLinkActive]="['active']" [routerLink]="[ '/users/list/humans']"
|
||||
[routerLinkActiveOptions]="{ exact: true }">
|
||||
<i class="icon las la-user-friends"></i>
|
||||
<span class="label">{{ 'MENU.HUMANUSERS' | translate }}</span>
|
||||
</a>
|
||||
|
||||
<a @navitem matTooltip="{{'MENU.TOOLTIP.MACHINEUSERS' | translate}}" class="nav-item"
|
||||
[routerLinkActive]="['active']" [routerLink]="[ '/users/list/machines']"
|
||||
[routerLinkActiveOptions]="{ exact: true }">
|
||||
<i class="icon las la-users-cog"></i>
|
||||
<span class="label">{{ 'MENU.MACHINEUSERS' | translate }}</span>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['user.grant.read(:[0-9]*)?']">
|
||||
<a @navitem matTooltip="{{'MENU.TOOLTIP.AUTHZ' | translate}}" class="nav-item"
|
||||
[routerLinkActive]="['active']" [routerLink]="[ '/grants']"
|
||||
[routerLinkActiveOptions]="{ exact: true }">
|
||||
<i class="icon las la-shield-alt"></i>
|
||||
<span class="label">{{ 'MENU.GRANTS' | translate }}</span>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-template cnslHasFeature [hasFeature]="['actions']">
|
||||
<a @navitem matTooltip="{{'MENU.TOOLTIP.ACTIONS' | translate}}" class="nav-item"
|
||||
[routerLinkActive]="['active']" [routerLink]="[ '/actions']"
|
||||
[routerLinkActiveOptions]="{ exact: true }">
|
||||
<i class="icon las la-exchange-alt"></i>
|
||||
<span class="label">{{ 'MENU.ACTIONS' | translate }}</span>
|
||||
</a>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="iamuser$ | async">
|
||||
<div @navitem class="divider">
|
||||
<div class="line"></div>
|
||||
<span>{{'MENU.ADMINSECTION' | translate}}</span>
|
||||
<div class="hiddenline"></div>
|
||||
</div>
|
||||
<a @navitem matTooltip="{{'MENU.TOOLTIP.IAMPOLICIES' | translate}}" class="nav-item"
|
||||
[routerLinkActive]="['active']" [routerLink]="[ '/iam','policies']">
|
||||
<i class="icon las la-cog"></i>
|
||||
<span class="label">{{'MENU.IAMPOLICIES' | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a @navitem matTooltip="{{'MENU.TOOLTIP.IAMEVENTSTORE' | translate}}" class="nav-item"
|
||||
[routerLinkActive]="['active']" [routerLink]="[ '/iam', 'eventstore']">
|
||||
<i class="icon las la-database"></i>
|
||||
<span class="label">{{'MENU.IAMEVENTSTORE' | translate}}</span>
|
||||
</a>
|
||||
</ng-container>
|
||||
|
||||
<span class="fill-space"></span>
|
||||
|
||||
<div class="toc-line" *ngIf="privacyPolicy">
|
||||
<a class="toc" [href]="privacyPolicy.tosLink" alt="Terms and Conditions" target="_blank">{{'MENU.TOS'
|
||||
| translate}}</a>
|
||||
<span class="slash">|</span>
|
||||
<a class="toc" [href]="privacyPolicy.privacyLink" alt="Privacy Policy " target="_blank">{{'MENU.PRIVACY'
|
||||
| translate}}</a>
|
||||
<span> </span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="fill-space"></span>
|
||||
</div>
|
||||
</mat-drawer>
|
||||
<mat-drawer-content class="content">
|
||||
<div class="router" [@routeAnimations]="prepareRoute(outlet)">
|
||||
<router-outlet #outlet="outlet"></router-outlet>
|
||||
</div>
|
||||
</mat-drawer-content>
|
||||
</mat-drawer-container>
|
||||
<div @adminline *ngIf="iamuser$ | async" class="admin-line" [ngClass]="{'expanded': !hideAdminWarn}"
|
||||
matTooltip="IAM Administrator">
|
||||
<button [matTooltip]="!hideAdminWarn ? 'Unpin': 'Pin'" (click)="toggleAdminHide()" mat-icon-button>
|
||||
<mat-icon *ngIf="!hideAdminWarn" svgIcon="mdi_pin"></mat-icon>
|
||||
<mat-icon *ngIf="hideAdminWarn" svgIcon="mdi_pin_outline"></mat-icon>
|
||||
</button>
|
||||
<span>{{'MENU.IAMADMIN' | translate}}</span>
|
||||
<cnsl-footer [privateLabelPolicy]="labelpolicy"></cnsl-footer>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
@ -1,297 +1,61 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
.root-header {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
height: 60px;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
@mixin main-theme($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
$warn: map-get($theme, warn);
|
||||
$background: map-get($theme, background);
|
||||
$foreground: map-get($theme, foreground);
|
||||
$accent: map-get($theme, accent);
|
||||
$primary-color: mat.get-color-from-palette($primary, 500);
|
||||
|
||||
.org-button {
|
||||
font-weight: bold;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
$warn-color: mat.get-color-from-palette($warn, 500);
|
||||
$accent-color: mat.get-color-from-palette($accent, 500);
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
$back: map-get($background, background);
|
||||
$base: map-get($foreground, base);
|
||||
|
||||
.logo {
|
||||
max-height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
border-radius: 0.5rem;
|
||||
background-color: #2d2e30;
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.doc-link {
|
||||
margin-right: 1rem;
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.org-context-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
|
||||
.context_card {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
|
||||
.docs {
|
||||
text-decoration: none;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: block;
|
||||
margin: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.a_card {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
padding-top: 60px;
|
||||
|
||||
.sidenav {
|
||||
width: 280px;
|
||||
border-right: none;
|
||||
|
||||
.side-column {
|
||||
padding-top: 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
height: calc(100% - 60px);
|
||||
|
||||
.list {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
margin-top: 2rem;
|
||||
|
||||
.logout-icon {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
padding: 0 1rem;
|
||||
margin-right: 0.5rem;
|
||||
border-top-right-radius: 1.5rem;
|
||||
border-bottom-right-radius: 1.5rem;
|
||||
|
||||
.icon {
|
||||
margin: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.iam-i {
|
||||
object-fit: contain;
|
||||
max-height: 24px;
|
||||
margin: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.c_label {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
|
||||
.count {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
// background-color: #00000010;
|
||||
border-top-right-radius: 1.5rem;
|
||||
border-bottom-right-radius: 1.5rem;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-top-right-radius: 1.5rem;
|
||||
border-bottom-right-radius: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.project-status {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.toc-line {
|
||||
margin: 2rem 2rem;
|
||||
|
||||
.toc {
|
||||
font-size: 12px;
|
||||
color: var(--grey);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.slash {
|
||||
margin: 0 0.5rem;
|
||||
color: var(--grey);
|
||||
}
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
|
||||
.router {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
.router-container {
|
||||
padding: 0 2rem;
|
||||
|
||||
.theme-section {
|
||||
display: block;
|
||||
padding: 0 0.5rem;
|
||||
margin-top: 2rem;
|
||||
align-self: flex-start;
|
||||
border-radius: 1rem;
|
||||
@media only screen and (max-width: 500px) {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.round-light {
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
margin: 0.5rem;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(315deg, #e6e6e6, #fff);
|
||||
.outlet {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.round-dark {
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
margin: 0.5rem;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(315deg, #000, #000);
|
||||
.nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
background-color: map-get($background, toolbar);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid map-get($foreground, divider);
|
||||
z-index: 50;
|
||||
transform: all 0.2s ease;
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
background-color: map-get($background, moz-toolbar);
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
&.shadow {
|
||||
box-shadow: 0 0 15px 0 rgb(0 0 0 / 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 5px 0;
|
||||
|
||||
span {
|
||||
border: 1px solid #81868a40;
|
||||
padding: 2px 1rem;
|
||||
border-radius: 50vw;
|
||||
color: var(--grey);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.line {
|
||||
display: block;
|
||||
background-color: #81868a40;
|
||||
height: 1px;
|
||||
margin: 0.5rem 0;
|
||||
flex: 1;
|
||||
min-width: 10px;
|
||||
}
|
||||
|
||||
.hiddenline {
|
||||
display: block;
|
||||
visibility: hidden;
|
||||
width: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin textvar($theme) {
|
||||
.filter-form {
|
||||
margin: 0 0.5rem;
|
||||
/* stylelint-disable */
|
||||
$foreground: map-get($theme, foreground);
|
||||
color: mat.get-color-from-palette($foreground, text) !important;
|
||||
}
|
||||
|
||||
.show-all {
|
||||
$primary: map-get($theme, primary);
|
||||
color: mat.get-color-from-palette($primary, 300) !important;
|
||||
border-bottom: 1px solid var(--grey);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
/* stylelint-enable */
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||
import { DOCUMENT, ViewportScroller } from '@angular/common';
|
||||
import { Component, HostBinding, Inject, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { Component, HostBinding, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatIconRegistry } from '@angular/material/icon';
|
||||
import { MatDrawer } from '@angular/material/sidenav';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
@ -15,7 +15,12 @@ import { Org } from './proto/generated/zitadel/org_pb';
|
||||
import { LabelPolicy, PrivacyPolicy } from './proto/generated/zitadel/policy_pb';
|
||||
import { AuthenticationService } from './services/authentication.service';
|
||||
import { GrpcAuthService } from './services/grpc-auth.service';
|
||||
import { KeyboardShortcutsService } from './services/keyboard-shortcuts/keyboard-shortcuts.service';
|
||||
import { ManagementService } from './services/mgmt.service';
|
||||
import { NavigationService } from './services/navigation.service';
|
||||
import { OverlayWorkflowService } from './services/overlay/overlay-workflow.service';
|
||||
import { IntroWorkflowOverlays } from './services/overlay/workflows';
|
||||
import { StorageLocation, StorageService } from './services/storage.service';
|
||||
import { ThemeService } from './services/theme.service';
|
||||
import { UpdateService } from './services/update.service';
|
||||
|
||||
@ -25,7 +30,7 @@ import { UpdateService } from './services/update.service';
|
||||
styleUrls: ['./app.component.scss'],
|
||||
animations: [toolbarAnimation, ...navAnimations, accountCard, routeAnimations, adminLineAnimation],
|
||||
})
|
||||
export class AppComponent implements OnDestroy {
|
||||
export class AppComponent implements OnInit, OnDestroy {
|
||||
@ViewChild('drawer') public drawer!: MatDrawer;
|
||||
public isHandset$: Observable<boolean> = this.breakpointObserver.observe('(max-width: 599px)').pipe(
|
||||
map((result) => {
|
||||
@ -34,10 +39,13 @@ export class AppComponent implements OnDestroy {
|
||||
);
|
||||
@HostBinding('class') public componentCssClass: string = 'dark-theme';
|
||||
|
||||
public showAccount: boolean = false;
|
||||
public showOrgContext: boolean = false;
|
||||
public yoffset: number = 0;
|
||||
@HostListener('window:scroll', ['$event']) onScroll(event: Event): void {
|
||||
this.yoffset = this.viewPortScroller.getScrollPosition()[1];
|
||||
}
|
||||
public org!: Org.AsObject;
|
||||
// public user!: User.AsObject;
|
||||
public orgs$: Observable<Org.AsObject[]> = of([]);
|
||||
public showAccount: boolean = false;
|
||||
public isDarkTheme: Observable<boolean> = of(true);
|
||||
|
||||
public showProjectSection: boolean = false;
|
||||
@ -45,12 +53,11 @@ export class AppComponent implements OnDestroy {
|
||||
private destroy$: Subject<void> = new Subject();
|
||||
public labelpolicy!: LabelPolicy.AsObject;
|
||||
|
||||
public hideAdminWarn: boolean = true;
|
||||
public language: string = 'en';
|
||||
public privacyPolicy!: PrivacyPolicy.AsObject;
|
||||
constructor(
|
||||
public viewPortScroller: ViewportScroller,
|
||||
@Inject('windowObject') public window: Window,
|
||||
public viewPortScroller: ViewportScroller,
|
||||
public translate: TranslateService,
|
||||
public authenticationService: AuthenticationService,
|
||||
public authService: GrpcAuthService,
|
||||
@ -62,7 +69,11 @@ export class AppComponent implements OnDestroy {
|
||||
public domSanitizer: DomSanitizer,
|
||||
private router: Router,
|
||||
update: UpdateService,
|
||||
keyboardShortcuts: KeyboardShortcutsService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private workflowService: OverlayWorkflowService,
|
||||
private storageService: StorageService,
|
||||
private navigationService: NavigationService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {
|
||||
console.log(
|
||||
@ -172,14 +183,17 @@ export class AppComponent implements OnDestroy {
|
||||
this.activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((route) => {
|
||||
const { org } = route;
|
||||
if (org) {
|
||||
this.authService.getActiveOrg(org).then((queriedOrg) => {
|
||||
this.org = queriedOrg;
|
||||
});
|
||||
this.authService
|
||||
.getActiveOrg(org)
|
||||
.then((queriedOrg) => {
|
||||
this.org = queriedOrg;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.router.navigate(['/users/me']);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.loadPrivateLabelling();
|
||||
|
||||
this.getProjectCount();
|
||||
|
||||
this.authService.activeOrgChanged.pipe(takeUntil(this.destroy$)).subscribe((org) => {
|
||||
@ -189,18 +203,19 @@ export class AppComponent implements OnDestroy {
|
||||
|
||||
this.authenticationService.authenticationChanged.pipe(takeUntil(this.destroy$)).subscribe((authenticated) => {
|
||||
if (authenticated) {
|
||||
this.authService.getActiveOrg().then((org) => {
|
||||
this.org = org;
|
||||
});
|
||||
this.authService
|
||||
.getActiveOrg()
|
||||
.then((org) => {
|
||||
this.org = org;
|
||||
|
||||
this.startIntroWorkflow();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.router.navigate(['/users/me']);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const theme = localStorage.getItem('theme');
|
||||
if (theme) {
|
||||
this.overlayContainer.getContainerElement().classList.add(theme);
|
||||
this.componentCssClass = theme;
|
||||
}
|
||||
|
||||
this.isDarkTheme = this.themeService.isDarkTheme;
|
||||
this.isDarkTheme.subscribe((dark) => this.onSetTheme(dark ? 'dark-theme' : 'light-theme'));
|
||||
|
||||
@ -208,10 +223,22 @@ export class AppComponent implements OnDestroy {
|
||||
this.document.documentElement.lang = language.lang;
|
||||
this.language = language.lang;
|
||||
});
|
||||
}
|
||||
|
||||
this.hideAdminWarn = localStorage.getItem('hideAdministratorWarning') === 'true' ? true : false;
|
||||
private startIntroWorkflow(): void {
|
||||
setTimeout(() => {
|
||||
const cb = () => {
|
||||
this.storageService.setItem('intro-dismissed', true, StorageLocation.local);
|
||||
};
|
||||
const dismissed = this.storageService.getItem('intro-dismissed', StorageLocation.local);
|
||||
if (!dismissed) {
|
||||
this.workflowService.startWorkflow(IntroWorkflowOverlays, cb);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
this.loadPolicies();
|
||||
public ngOnInit(): void {
|
||||
this.loadPrivateLabelling();
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
@ -219,20 +246,15 @@ export class AppComponent implements OnDestroy {
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
public toggleAdminHide(): void {
|
||||
this.hideAdminWarn = !this.hideAdminWarn;
|
||||
localStorage.setItem('hideAdministratorWarning', this.hideAdminWarn.toString());
|
||||
}
|
||||
|
||||
public loadPrivateLabelling(): void {
|
||||
const setDefaultColors = () => {
|
||||
const darkPrimary = '#5282c1';
|
||||
const lightPrimary = '#5282c1';
|
||||
const darkPrimary = '#bbbafa';
|
||||
const lightPrimary = '#5469d4';
|
||||
|
||||
const darkWarn = '#cd3d56';
|
||||
const darkWarn = '#ff3b5b';
|
||||
const lightWarn = '#cd3d56';
|
||||
|
||||
const darkBackground = '#212224';
|
||||
const darkBackground = '#111827';
|
||||
const lightBackground = '#fafafa';
|
||||
|
||||
const darkText = '#ffffff';
|
||||
@ -260,10 +282,10 @@ export class AppComponent implements OnDestroy {
|
||||
const isDark = (color: string) => this.themeService.isDark(color);
|
||||
const isLight = (color: string) => this.themeService.isLight(color);
|
||||
|
||||
const darkPrimary = this.labelpolicy?.primaryColorDark || '#5282c1';
|
||||
const lightPrimary = this.labelpolicy?.primaryColor || '#5282c1';
|
||||
const darkPrimary = this.labelpolicy?.primaryColorDark || '#bbbafa';
|
||||
const lightPrimary = this.labelpolicy?.primaryColor || '#5469d4';
|
||||
|
||||
const darkWarn = this.labelpolicy?.warnColorDark || '#cd3d56';
|
||||
const darkWarn = this.labelpolicy?.warnColorDark || '#ff3b5b';
|
||||
const lightWarn = this.labelpolicy?.warnColor || '#cd3d56';
|
||||
|
||||
let darkBackground = this.labelpolicy?.backgroundColorDark;
|
||||
@ -282,9 +304,9 @@ export class AppComponent implements OnDestroy {
|
||||
console.info(
|
||||
`Background (${darkBackground}) is not dark enough for a dark theme. Falling back to zitadel background`,
|
||||
);
|
||||
darkBackground = '#212224';
|
||||
darkBackground = '#111827';
|
||||
}
|
||||
this.themeService.saveBackgroundColor(darkBackground || '#212224', true);
|
||||
this.themeService.saveBackgroundColor(darkBackground || '#111827', true);
|
||||
|
||||
if (lightBackground && !isLight(lightBackground)) {
|
||||
console.info(
|
||||
@ -313,30 +335,23 @@ export class AppComponent implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
public loadPolicies(): void {
|
||||
this.mgmtService.getPrivacyPolicy().then((privacypolicy) => {
|
||||
if (privacypolicy.policy) {
|
||||
this.privacyPolicy = privacypolicy.policy;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public prepareRoute(outlet: RouterOutlet): boolean {
|
||||
return outlet && outlet.activatedRouteData && outlet.activatedRouteData.animation;
|
||||
}
|
||||
|
||||
public closeAccountCard(): void {
|
||||
if (this.showAccount) {
|
||||
this.showAccount = false;
|
||||
}
|
||||
}
|
||||
|
||||
public onSetTheme(theme: string): void {
|
||||
localStorage.setItem('theme', theme);
|
||||
this.overlayContainer.getContainerElement().classList.add(theme);
|
||||
this.componentCssClass = theme;
|
||||
}
|
||||
|
||||
public changedOrg(org: Org.AsObject): void {
|
||||
this.loadPrivateLabelling();
|
||||
this.authService.zitadelPermissionsChanged.pipe(take(1)).subscribe(() => {
|
||||
this.router.navigate(['/']);
|
||||
});
|
||||
}
|
||||
|
||||
private setLanguage(): void {
|
||||
this.translate.addLangs(['en', 'de']);
|
||||
this.translate.setDefaultLang('en');
|
||||
@ -357,16 +372,6 @@ export class AppComponent implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
public setActiveOrg(org: Org.AsObject): void {
|
||||
console.log(this.org);
|
||||
this.org = org;
|
||||
this.authService.setActiveOrg(org);
|
||||
this.loadPrivateLabelling();
|
||||
this.authService.zitadelPermissionsChanged.pipe(take(1)).subscribe(() => {
|
||||
this.router.navigate(['/']);
|
||||
});
|
||||
}
|
||||
|
||||
private getProjectCount(): void {
|
||||
this.authService.isAllowed(['project.read']).subscribe((allowed) => {
|
||||
if (allowed) {
|
||||
|
@ -1,20 +1,12 @@
|
||||
import { OverlayModule } from '@angular/cdk/overlay';
|
||||
import { CommonModule, registerLocaleData } from '@angular/common';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatNativeDateModule } from '@angular/material/core';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
@ -23,37 +15,40 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
|
||||
import { QuicklinkModule } from 'ngx-quicklink';
|
||||
import { from, Observable } from 'rxjs';
|
||||
import { OnboardingModule } from 'src/app/modules/onboarding/onboarding.module';
|
||||
import { RegExpPipeModule } from 'src/app/pipes/regexp-pipe/regexp-pipe.module';
|
||||
import { InfoOverlayModule } from 'src/app/modules/info-overlay/info-overlay.module';
|
||||
import { AssetService } from 'src/app/services/asset.service';
|
||||
import { SubscriptionService } from 'src/app/services/subscription.service';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { HasFeatureModule } from './directives/has-feature/has-feature.module';
|
||||
import { HasRoleModule } from './directives/has-role/has-role.module';
|
||||
import { OutsideClickModule } from './directives/outside-click/outside-click.module';
|
||||
import { AccountsCardModule } from './modules/accounts-card/accounts-card.module';
|
||||
import { AvatarModule } from './modules/avatar/avatar.module';
|
||||
import { InputModule } from './modules/input/input.module';
|
||||
import { OrgContextModule } from './modules/org-context/org-context.module';
|
||||
import { FooterModule } from './modules/footer/footer.module';
|
||||
import { HeaderModule } from './modules/header/header.module';
|
||||
import { KeyboardShortcutsModule } from './modules/keyboard-shortcuts/keyboard-shortcuts.module';
|
||||
import { NavModule } from './modules/nav/nav.module';
|
||||
import { WarnDialogModule } from './modules/warn-dialog/warn-dialog.module';
|
||||
import { SignedoutComponent } from './pages/signedout/signedout.component';
|
||||
import { HasFeaturePipeModule } from './pipes/has-feature-pipe/has-feature-pipe.module';
|
||||
import { HasRolePipeModule } from './pipes/has-role-pipe/has-role-pipe.module';
|
||||
import { AdminService } from './services/admin.service';
|
||||
import { AuthenticationService } from './services/authentication.service';
|
||||
import { BreadcrumbService } from './services/breadcrumb.service';
|
||||
import { GrpcAuthService } from './services/grpc-auth.service';
|
||||
import { GrpcService } from './services/grpc.service';
|
||||
import { AuthInterceptor } from './services/interceptors/auth.interceptor';
|
||||
import { GRPC_INTERCEPTORS } from './services/interceptors/grpc-interceptor';
|
||||
import { I18nInterceptor } from './services/interceptors/i18n.interceptor';
|
||||
import { OrgInterceptor } from './services/interceptors/org.interceptor';
|
||||
import { KeyboardShortcutsService } from './services/keyboard-shortcuts/keyboard-shortcuts.service';
|
||||
import { ManagementService } from './services/mgmt.service';
|
||||
import { NavigationService } from './services/navigation.service';
|
||||
import { OverlayService } from './services/overlay/overlay.service';
|
||||
import { RefreshService } from './services/refresh.service';
|
||||
import { SeoService } from './services/seo.service';
|
||||
import { StatehandlerProcessorService, StatehandlerProcessorServiceImpl } from './services/statehandler-processor.service';
|
||||
import { StatehandlerService, StatehandlerServiceImpl } from './services/statehandler.service';
|
||||
import {
|
||||
StatehandlerProcessorService,
|
||||
StatehandlerProcessorServiceImpl,
|
||||
} from './services/statehandler/statehandler-processor.service';
|
||||
import { StatehandlerService, StatehandlerServiceImpl } from './services/statehandler/statehandler.service';
|
||||
import { StorageService } from './services/storage.service';
|
||||
import { ThemeService } from './services/theme.service';
|
||||
|
||||
@ -89,7 +84,7 @@ const authConfig: AuthConfig = {
|
||||
AppRoutingModule,
|
||||
CommonModule,
|
||||
BrowserModule,
|
||||
OverlayModule,
|
||||
HeaderModule,
|
||||
OAuthModule.forRoot({
|
||||
resourceServer: {
|
||||
allowedUrls: [
|
||||
@ -107,34 +102,22 @@ const authConfig: AuthConfig = {
|
||||
useClass: WebpackTranslateLoader,
|
||||
},
|
||||
}),
|
||||
NavModule,
|
||||
MatNativeDateModule,
|
||||
QuicklinkModule,
|
||||
AccountsCardModule,
|
||||
OrgContextModule,
|
||||
HasRoleModule,
|
||||
InfoOverlayModule,
|
||||
BrowserAnimationsModule,
|
||||
HttpClientModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatTooltipModule,
|
||||
MatSidenavModule,
|
||||
MatCardModule,
|
||||
OutsideClickModule,
|
||||
InputModule,
|
||||
FooterModule,
|
||||
HasRolePipeModule,
|
||||
HasFeaturePipeModule,
|
||||
HasFeatureModule,
|
||||
MatProgressBarModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatToolbarModule,
|
||||
ReactiveFormsModule,
|
||||
MatSnackBarModule,
|
||||
AvatarModule,
|
||||
WarnDialogModule,
|
||||
MatSelectModule,
|
||||
MatDialogModule,
|
||||
RegExpPipeModule,
|
||||
OnboardingModule,
|
||||
KeyboardShortcutsModule,
|
||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: false }),
|
||||
],
|
||||
providers: [
|
||||
@ -182,15 +165,19 @@ const authConfig: AuthConfig = {
|
||||
multi: true,
|
||||
useClass: OrgInterceptor,
|
||||
},
|
||||
OverlayService,
|
||||
SeoService,
|
||||
RefreshService,
|
||||
GrpcService,
|
||||
BreadcrumbService,
|
||||
AuthenticationService,
|
||||
GrpcAuthService,
|
||||
ManagementService,
|
||||
AdminService,
|
||||
SubscriptionService,
|
||||
KeyboardShortcutsService,
|
||||
AssetService,
|
||||
NavigationService,
|
||||
{ provide: 'windowObject', useValue: window },
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
|
20
console/src/app/directives/back/back.directive.ts
Normal file
20
console/src/app/directives/back/back.directive.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Directive, ElementRef, HostListener, Renderer2 } from '@angular/core';
|
||||
import { NavigationService } from 'src/app/services/navigation.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[cnslBack]',
|
||||
})
|
||||
export class BackDirective {
|
||||
@HostListener('click')
|
||||
onClick(): void {
|
||||
this.navigation.back();
|
||||
}
|
||||
|
||||
constructor(private navigation: NavigationService, private elRef: ElementRef, private renderer2: Renderer2) {
|
||||
if (navigation.isBackPossible) {
|
||||
// this.renderer2.removeStyle(this.elRef.nativeElement, 'visibility');
|
||||
} else {
|
||||
this.renderer2.setStyle(this.elRef.nativeElement, 'display', 'none');
|
||||
}
|
||||
}
|
||||
}
|
11
console/src/app/directives/back/back.module.ts
Normal file
11
console/src/app/directives/back/back.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { BackDirective } from './back.directive';
|
||||
|
||||
@NgModule({
|
||||
declarations: [BackDirective],
|
||||
imports: [CommonModule],
|
||||
exports: [BackDirective],
|
||||
})
|
||||
export class BackModule {}
|
@ -7,7 +7,8 @@ export class CopyToClipboardDirective {
|
||||
@Input() valueToCopy: string = '';
|
||||
@Output() copiedValue: EventEmitter<string> = new EventEmitter();
|
||||
|
||||
@HostListener('click', ['$event.target']) onMouseEnter(): void {
|
||||
@HostListener('click', ['$event']) onMouseEnter($event: any): void {
|
||||
$event.preventDefault();
|
||||
this.copytoclipboard(this.valueToCopy);
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,14 @@
|
||||
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
|
||||
|
||||
@Directive({
|
||||
selector: '[cnslHasFeature]',
|
||||
})
|
||||
|
||||
export class HasFeatureDirective {
|
||||
private hasView: boolean = false;
|
||||
@Input() public set hasFeature(features: string[] | RegExp[]) {
|
||||
@Input() public set hasFeature(features: string[] | RegExp[] | undefined) {
|
||||
if (features && features.length > 0) {
|
||||
this.authService.canUseFeature(features).subscribe(isAllowed => {
|
||||
this.authService.canUseFeature(features).subscribe((isAllowed) => {
|
||||
if (isAllowed && !this.hasView) {
|
||||
this.viewContainerRef.clear();
|
||||
this.viewContainerRef.createEmbeddedView(this.templateRef);
|
||||
@ -18,7 +16,12 @@ export class HasFeatureDirective {
|
||||
this.viewContainerRef.clear();
|
||||
this.hasView = false;
|
||||
}
|
||||
})
|
||||
});
|
||||
} else {
|
||||
if (!this.hasView) {
|
||||
this.viewContainerRef.clear();
|
||||
this.viewContainerRef.createEmbeddedView(this.templateRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,5 +29,5 @@ export class HasFeatureDirective {
|
||||
private authService: GrpcAuthService,
|
||||
protected templateRef: TemplateRef<any>,
|
||||
protected viewContainerRef: ViewContainerRef,
|
||||
) { }
|
||||
) {}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
})
|
||||
export class HasRoleDirective {
|
||||
private hasView: boolean = false;
|
||||
@Input() public set hasRole(roles: string[] | RegExp[]) {
|
||||
@Input() public set hasRole(roles: string[] | RegExp[] | undefined) {
|
||||
if (roles && roles.length > 0) {
|
||||
this.authService.isAllowed(roles).subscribe((isAllowed) => {
|
||||
if (isAllowed && !this.hasView) {
|
||||
@ -17,6 +17,11 @@ export class HasRoleDirective {
|
||||
this.hasView = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (!this.hasView) {
|
||||
this.viewContainerRef.clear();
|
||||
this.viewContainerRef.createEmbeddedView(this.templateRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,21 +1,33 @@
|
||||
<div class="card" cnslOutsideClick (clickOutside)="closeCard($event)">
|
||||
<cnsl-avatar
|
||||
*ngIf="user.human?.profile && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))"
|
||||
class="avatar" [forColor]="user.preferredLoginName" [avatarUrl]="user.human?.profile?.avatarUrl || ''"
|
||||
[name]="(user.human && user.human.profile && user.human.profile.displayName) ? user.human.profile.displayName : (user.human?.profile?.firstName + ' '+ user.human?.profile?.lastName)"
|
||||
<div class="accounts-card" cnslOutsideClick (clickOutside)="closeCard($event)">
|
||||
<cnsl-avatar (click)="editUserProfile()" *ngIf="user.human?.profile && user.human?.profile?.displayName"
|
||||
class="avatar" [ngClass]="{'iam-user': iamuser}" [forColor]="user.preferredLoginName"
|
||||
[avatarUrl]="user.human?.profile?.avatarUrl || ''"
|
||||
[name]="(user.human && user.human.profile && user.human.profile?.displayName) ? user.human.profile.displayName : (user.human?.profile?.firstName + ' '+ user.human?.profile?.lastName)"
|
||||
[size]="80">
|
||||
</cnsl-avatar>
|
||||
|
||||
<span class="u-name">{{user.human?.profile?.displayName ? user.human?.profile?.displayName : 'A'}}</span>
|
||||
<span class="u-email" *ngIf="user.preferredLoginName">{{user.preferredLoginName}}</span>
|
||||
<span class="iamuser" *ngIf="iamuser">IAM USER</span>
|
||||
<a [routerLink]="[ '/system']" class="iamuser" *ngIf="iamuser" (click)="close()">
|
||||
<span class="label">{{'MENU.SYSTEM' | translate}}</span>
|
||||
<a class="iambtn">
|
||||
<i class="las la-cog"></i>
|
||||
</a>
|
||||
</a>
|
||||
|
||||
<button color="primary" (click)="editUserProfile()" mat-stroked-button>{{'USER.EDITACCOUNT' | translate}}</button>
|
||||
<a [routerLink]="[ '/org']" class="iamuser" *ngIf="isOnSystem" (click)="close()">
|
||||
<span class="label">{{'MENU.ORGANIZATION' | translate}}</span>
|
||||
<a class="iambtn">
|
||||
<i class="las la-cog"></i>
|
||||
</a>
|
||||
</a>
|
||||
|
||||
<button (click)="editUserProfile()" mat-stroked-button>{{'USER.EDITACCOUNT' | translate}}</button>
|
||||
<div class="l-accounts">
|
||||
<mat-progress-bar *ngIf="loadingUsers" color="primary" mode="indeterminate"></mat-progress-bar>
|
||||
<a class="row" *ngFor="let session of sessions" (click)="selectAccount(session.loginName)">
|
||||
<cnsl-avatar *ngIf="session && session.displayName" class="small-avatar" [avatarUrl]="session.avatarUrl || ''"
|
||||
[forColor]="session.loginName" [size]="32">
|
||||
[name]="session.displayName" [forColor]="session.loginName" [size]="32">
|
||||
</cnsl-avatar>
|
||||
|
||||
<div class="col">
|
||||
@ -38,5 +50,5 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button (click)="logout()" color="primary" mat-stroked-button>{{'MENU.LOGOUT' | translate}}</button>
|
||||
<button (click)="logout()" color="warn" mat-stroked-button>{{'MENU.LOGOUT' | translate}}</button>
|
||||
</div>
|
@ -1,107 +1,164 @@
|
||||
@mixin accounts-card-theme($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
$warn: map-get($theme, warn);
|
||||
$background: map-get($theme, background);
|
||||
$accent: map-get($theme, accent);
|
||||
$primary-color: mat.get-color-from-palette($primary, 500);
|
||||
$card-background-color: mat.get-color-from-palette($background, cards);
|
||||
|
||||
.card {
|
||||
border-radius: .5rem;
|
||||
z-index: 200;
|
||||
border: 1px solid #ffffff30;
|
||||
width: 350px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 1rem 0;
|
||||
$warn-color: mat.get-color-from-palette($warn, 500);
|
||||
$accent-color: mat.get-color-from-palette($accent, 500);
|
||||
$foreground: map-get($theme, foreground);
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
$back: map-get($background, background);
|
||||
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
|
||||
$secondary-text: map-get($foreground, secondary-text);
|
||||
|
||||
.avatar {
|
||||
font-size: 80px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.u-name {
|
||||
font-size: 1rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.u-email {
|
||||
font-size: .8rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.iamuser {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 50vh;
|
||||
margin: .5rem;
|
||||
|
||||
.mat-button-wrapper {
|
||||
font-size: .8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.l-accounts {
|
||||
.accounts-card {
|
||||
border-radius: 0.5rem;
|
||||
z-index: 300;
|
||||
background-color: $card-background-color;
|
||||
transition: background-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
border: 1px solid $border-color;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
width: 350px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: .5rem 0;
|
||||
max-height: 310px;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid rgba(#8795a1, .3);
|
||||
border-bottom: 1px solid rgba(#8795a1, .3);
|
||||
align-items: center;
|
||||
padding: 1rem 0;
|
||||
position: relative;
|
||||
|
||||
.row {
|
||||
padding: .5rem;
|
||||
display: flex;
|
||||
.avatar {
|
||||
font-size: 80px;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 50%;
|
||||
border: 2px solid $border-color;
|
||||
|
||||
&.iam-user {
|
||||
border: 2px solid $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.u-name {
|
||||
font-size: 1rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.u-email {
|
||||
font-size: 0.8rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.iamuser {
|
||||
position: absolute;
|
||||
border-radius: 50vw;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
font-weight: 600;
|
||||
background-color: $primary-color;
|
||||
color: mat.get-color-from-palette($primary, default-contrast);
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
padding: 0 4px;
|
||||
display: none;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: #00000010;
|
||||
.label {
|
||||
margin: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.small-avatar {
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
line-height: 35px;
|
||||
font-size: 35px;
|
||||
border-radius: 50%;
|
||||
margin: 0 1rem;
|
||||
.iambtn {
|
||||
margin: 0 0 0 4px;
|
||||
color: mat.get-color-from-palette($primary, default-contrast);
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
border-radius: 50%;
|
||||
margin: 0 1rem;
|
||||
text-align: center;
|
||||
|
||||
i {
|
||||
margin: auto;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.col {
|
||||
flex: 1;
|
||||
@media only screen and (max-width: 600px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.user-title {
|
||||
font-weight: 500;
|
||||
font-size: .9rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.email,
|
||||
.loginname {
|
||||
color: var(--grey);
|
||||
font-size: .8rem;
|
||||
line-height: 1rem;
|
||||
.label {
|
||||
margin: 0.5rem 0 0.5rem 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
button {
|
||||
border-radius: 50vh;
|
||||
margin: 0.5rem;
|
||||
|
||||
.mat-button-wrapper {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.l-accounts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0;
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid rgba(#8795a1, 0.3);
|
||||
border-bottom: 1px solid rgba(#8795a1, 0.3);
|
||||
|
||||
.row {
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: #00000010;
|
||||
}
|
||||
|
||||
.small-avatar {
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
line-height: 35px;
|
||||
font-size: 35px;
|
||||
border-radius: 50%;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
border-radius: 50%;
|
||||
margin: 0 1rem;
|
||||
text-align: center;
|
||||
|
||||
i {
|
||||
margin: auto;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.user-title {
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.email,
|
||||
.loginname {
|
||||
color: $secondary-text;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,17 +18,20 @@ export class AccountsCardComponent implements OnInit {
|
||||
public sessions: Session.AsObject[] = [];
|
||||
public loadingUsers: boolean = false;
|
||||
constructor(public authService: AuthenticationService, private router: Router, private userService: GrpcAuthService) {
|
||||
this.userService.listMyUserSessions().then(sessions => {
|
||||
this.sessions = sessions.resultList;
|
||||
const index = this.sessions.findIndex(user => user.loginName === this.user.preferredLoginName);
|
||||
if (index > -1) {
|
||||
this.sessions.splice(index, 1);
|
||||
}
|
||||
this.userService
|
||||
.listMyUserSessions()
|
||||
.then((sessions) => {
|
||||
this.sessions = sessions.resultList;
|
||||
const index = this.sessions.findIndex((user) => user.loginName === this.user.preferredLoginName);
|
||||
if (index > -1) {
|
||||
this.sessions.splice(index, 1);
|
||||
}
|
||||
|
||||
this.loadingUsers = false;
|
||||
}).catch(() => {
|
||||
this.loadingUsers = false;
|
||||
});
|
||||
this.loadingUsers = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.loadingUsers = false;
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
@ -46,6 +49,10 @@ export class AccountsCardComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.closedCard.emit();
|
||||
}
|
||||
|
||||
public selectAccount(loginHint?: string): void {
|
||||
const configWithPrompt: Partial<AuthConfig> = {
|
||||
customQueryParams: {
|
||||
@ -71,4 +78,11 @@ export class AccountsCardComponent implements OnInit {
|
||||
this.authService.signout();
|
||||
this.closedCard.emit();
|
||||
}
|
||||
|
||||
public get isOnSystem(): boolean {
|
||||
return (
|
||||
['/system', '/views', '/failed-events', '/system/members', '/system/features'].includes(this.router.url) ||
|
||||
new RegExp('/system/policy/*').test(this.router.url)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { OutsideClickModule } from 'src/app/directives/outside-click/outside-click.module';
|
||||
|
||||
@ -10,20 +11,17 @@ import { AvatarModule } from '../avatar/avatar.module';
|
||||
import { AccountsCardComponent } from './accounts-card.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AccountsCardComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatProgressBarModule,
|
||||
OutsideClickModule,
|
||||
AvatarModule,
|
||||
TranslateModule,
|
||||
],
|
||||
exports: [
|
||||
AccountsCardComponent,
|
||||
],
|
||||
declarations: [AccountsCardComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatProgressBarModule,
|
||||
OutsideClickModule,
|
||||
RouterModule,
|
||||
AvatarModule,
|
||||
TranslateModule,
|
||||
],
|
||||
exports: [AccountsCardComponent],
|
||||
})
|
||||
export class AccountsCardModule { }
|
||||
export class AccountsCardModule {}
|
||||
|
@ -0,0 +1,59 @@
|
||||
<div *ngIf="type !== ActionKeysType.ORGSWITCHER && (isHandset$ | async) === false" class="action-keys-wrapper"
|
||||
[ngSwitch]="type" [ngClass]="{'without-margin': withoutMargin, 'no-contrast-mode': doNotUseContrast}">
|
||||
<div *ngSwitchCase="ActionKeysType.CLEAR" class="action-keys-row">
|
||||
<div class="action-key esc">
|
||||
<div class="key-overlay"></div>
|
||||
<span>ESC</span>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngSwitchCase="ActionKeysType.ADD" class="action-keys-row">
|
||||
<div class="action-key">
|
||||
<div class="key-overlay"></div>
|
||||
<span>N</span>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngSwitchCase="ActionKeysType.DELETE" class="action-keys-row">
|
||||
<div class="action-key">
|
||||
<div class="key-overlay"></div>
|
||||
<span *ngIf="isMacLike || isIOS; else otherOS">⌘</span>
|
||||
</div>
|
||||
+
|
||||
<div class="action-key">
|
||||
<div class="key-overlay"></div>
|
||||
<span>BS</span>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngSwitchCase="ActionKeysType.DEACTIVATE" class="action-keys-row">
|
||||
<div class="action-key">
|
||||
<div class="key-overlay"></div>
|
||||
<span *ngIf="isMacLike || isIOS; else otherOS">⌘</span>
|
||||
</div>
|
||||
+
|
||||
<div class="action-key">
|
||||
<div class="key-overlay"></div>
|
||||
<span>↓</span>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngSwitchCase="ActionKeysType.REACTIVATE" class="action-keys-row">
|
||||
<div class="action-key">
|
||||
<div class="key-overlay"></div>
|
||||
<span *ngIf="isMacLike || isIOS; else otherOS">⌘</span>
|
||||
|
||||
</div>
|
||||
+
|
||||
<div class="action-key">
|
||||
<div class="key-overlay"></div>
|
||||
<span>↑</span>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngSwitchCase="ActionKeysType.FILTER" class="action-keys-row">
|
||||
<div class="action-key">
|
||||
<div class="key-overlay"></div>
|
||||
<span>F</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #otherOS>
|
||||
<span>crtl</span>
|
||||
</ng-template>
|
@ -0,0 +1,73 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
@mixin action-keys-theme($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
$background: map-get($theme, background);
|
||||
$foreground: map-get($theme, foreground);
|
||||
$accent: map-get($theme, accent);
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
$accent-color: mat.get-color-from-palette($primary, 500);
|
||||
$back: map-get($background, background);
|
||||
|
||||
.action-keys-wrapper {
|
||||
display: inline-block;
|
||||
padding-left: 0.5rem;
|
||||
margin-right: -0.5rem;
|
||||
|
||||
&.without-margin {
|
||||
padding: 0;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.action-keys-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 -4px;
|
||||
|
||||
.action-key {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
position: relative;
|
||||
margin: 0 4px;
|
||||
|
||||
&.esc {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.key-overlay {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: mat.get-color-from-palette($primary, default-contrast);
|
||||
opacity: 0.2;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.span {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50% -50%);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.no-contrast-mode {
|
||||
.action-keys-row {
|
||||
.key-overlay {
|
||||
z-index: 0;
|
||||
background: if($is-dark-theme, #fff, #000);
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,20 +1,20 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { OnboardingComponent } from './onboarding.component';
|
||||
import { ActionKeysComponent } from './action-keys.component';
|
||||
|
||||
describe('OnboardingComponent', () => {
|
||||
let component: OnboardingComponent;
|
||||
let fixture: ComponentFixture<OnboardingComponent>;
|
||||
describe('ActionKeysComponent', () => {
|
||||
let component: ActionKeysComponent;
|
||||
let fixture: ComponentFixture<ActionKeysComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ OnboardingComponent ],
|
||||
declarations: [ ActionKeysComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OnboardingComponent);
|
||||
fixture = TestBed.createComponent(ActionKeysComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
101
console/src/app/modules/action-keys/action-keys.component.ts
Normal file
101
console/src/app/modules/action-keys/action-keys.component.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
||||
import { AfterViewInit, Component, EventEmitter, HostListener, Input, Output } from '@angular/core';
|
||||
import { map, Observable } from 'rxjs';
|
||||
|
||||
export enum ActionKeysType {
|
||||
ADD,
|
||||
DELETE,
|
||||
DEACTIVATE,
|
||||
REACTIVATE,
|
||||
FILTER,
|
||||
ORGSWITCHER,
|
||||
CLEAR,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-action-keys',
|
||||
templateUrl: './action-keys.component.html',
|
||||
styleUrls: ['./action-keys.component.scss'],
|
||||
})
|
||||
export class ActionKeysComponent implements AfterViewInit {
|
||||
@Input() type: ActionKeysType = ActionKeysType.ADD;
|
||||
@Input() withoutMargin: boolean = false;
|
||||
@Input() doNotUseContrast: boolean = false;
|
||||
@Output() actionTriggered: EventEmitter<void> = new EventEmitter();
|
||||
@HostListener('document:keydown', ['$event'])
|
||||
handleKeyboardEvent(event: KeyboardEvent) {
|
||||
const tagname = (event.target as any)?.tagName;
|
||||
const exclude = ['input', 'textarea'];
|
||||
|
||||
if (exclude.indexOf(tagname.toLowerCase()) === -1) {
|
||||
switch (this.type) {
|
||||
case ActionKeysType.CLEAR:
|
||||
if (event.code === 'Escape') {
|
||||
event.preventDefault();
|
||||
this.actionTriggered.emit();
|
||||
}
|
||||
break;
|
||||
case ActionKeysType.ORGSWITCHER:
|
||||
if (event.key === '/') {
|
||||
this.actionTriggered.emit();
|
||||
}
|
||||
break;
|
||||
case ActionKeysType.ADD:
|
||||
if (event.code === 'KeyN') {
|
||||
this.actionTriggered.emit();
|
||||
}
|
||||
break;
|
||||
|
||||
case ActionKeysType.DELETE:
|
||||
if ((event.ctrlKey || event.metaKey) && event.code === 'Backspace') {
|
||||
this.actionTriggered.emit();
|
||||
}
|
||||
break;
|
||||
|
||||
case ActionKeysType.DEACTIVATE:
|
||||
if ((event.ctrlKey || event.metaKey) && event.code === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
this.actionTriggered.emit();
|
||||
}
|
||||
break;
|
||||
|
||||
case ActionKeysType.REACTIVATE:
|
||||
if ((event.ctrlKey || event.metaKey) && event.code === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
this.actionTriggered.emit();
|
||||
}
|
||||
break;
|
||||
|
||||
case ActionKeysType.FILTER:
|
||||
if (event.ctrlKey === false && event.code === 'KeyF') {
|
||||
this.actionTriggered.emit();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
public isHandset$: Observable<boolean> = this.breakpointObserver.observe(Breakpoints.Handset).pipe(
|
||||
map((result) => {
|
||||
return result.matches;
|
||||
}),
|
||||
);
|
||||
|
||||
public ActionKeysType: any = ActionKeysType;
|
||||
|
||||
constructor(public breakpointObserver: BreakpointObserver) {}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
window.focus();
|
||||
if (document.activeElement) {
|
||||
(document.activeElement as any).blur();
|
||||
}
|
||||
}
|
||||
|
||||
public get isMacLike(): boolean {
|
||||
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
public get isIOS(): boolean {
|
||||
return /(iPhone|iPod|iPad)/i.test(navigator.userAgent);
|
||||
}
|
||||
}
|
11
console/src/app/modules/action-keys/action-keys.module.ts
Normal file
11
console/src/app/modules/action-keys/action-keys.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { ActionKeysComponent } from './action-keys.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ActionKeysComponent],
|
||||
imports: [CommonModule],
|
||||
exports: [ActionKeysComponent],
|
||||
})
|
||||
export class ActionKeysModule {}
|
@ -1,34 +1,34 @@
|
||||
<span class="title" mat-dialog-title>{{'USER.MACHINE.ADD.TITLE' | translate}}</span>
|
||||
<div mat-dialog-content>
|
||||
<p class="desc"> {{'USER.MACHINE.ADD.DESCRIPTION' | translate}}</p>
|
||||
<p class="desc cnsl-secondary-text"> {{'USER.MACHINE.ADD.DESCRIPTION' | translate}}</p>
|
||||
|
||||
<cnsl-form-field class="form-field" appearance="outline">
|
||||
<cnsl-label>{{'USER.MACHINE.TYPE' | translate}}</cnsl-label>
|
||||
<mat-select [(ngModel)]="type">
|
||||
<mat-option *ngFor="let t of types" [value]="t">
|
||||
{{'USER.MACHINE.KEYTYPES.'+t | translate}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="form-field" appearance="outline">
|
||||
<cnsl-label>{{'USER.MACHINE.TYPE' | translate}}</cnsl-label>
|
||||
<mat-select [(ngModel)]="type">
|
||||
<mat-option *ngFor="let t of types" [value]="t">
|
||||
{{'USER.MACHINE.KEYTYPES.'+t | translate}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</cnsl-form-field>
|
||||
|
||||
<cnsl-form-field class="form-field" appearance="outline">
|
||||
<cnsl-label>{{'USER.MACHINE.CHOOSEEXPIRY' | translate}} (optional)</cnsl-label>
|
||||
<input cnslInput [matDatepicker]="picker" [min]="startDate" [formControl]="dateControl">
|
||||
<mat-datepicker-toggle style="top: 0;" cnslSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker startView="year" [startAt]="startDate"></mat-datepicker>
|
||||
<span cnsl-error *ngIf="dateControl?.errors?.matDatepickerMin?.min">
|
||||
{{'USER.MACHINE.CHOOSEDATEAFTER' | translate}}:
|
||||
{{dateControl?.errors?.matDatepickerMin.min.toDate() | localizedDate: 'EEE dd. MMM'}}
|
||||
</span>
|
||||
</cnsl-form-field>
|
||||
<cnsl-form-field class="add-key-form-field" appearance="outline">
|
||||
<cnsl-label>{{'USER.MACHINE.CHOOSEEXPIRY' | translate}} (optional)</cnsl-label>
|
||||
<input cnslInput [matDatepicker]="picker" [min]="startDate" [formControl]="dateControl">
|
||||
<mat-datepicker-toggle style="top: 0;" cnslSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker startView="year" [startAt]="startDate"></mat-datepicker>
|
||||
<span cnslError *ngIf="dateControl?.errors?.matDatepickerMin?.min">
|
||||
{{'USER.MACHINE.CHOOSEDATEAFTER' | translate}}:
|
||||
{{dateControl?.errors?.matDatepickerMin.min.toDate() | localizedDate: 'EEE dd. MMM'}}
|
||||
</span>
|
||||
</cnsl-form-field>
|
||||
</div>
|
||||
<div mat-dialog-actions class=" action">
|
||||
<button mat-button (click)="closeDialog()">
|
||||
{{'ACTIONS.CANCEL' | translate}}
|
||||
</button>
|
||||
<button mat-stroked-button (click)="closeDialog()">
|
||||
{{'ACTIONS.CANCEL' | translate}}
|
||||
</button>
|
||||
|
||||
<button color="primary" mat-raised-button class="ok-button" [disabled]="type === undefined || dateControl.invalid"
|
||||
(click)="closeDialogWithSuccess()">
|
||||
{{'ACTIONS.ADD' | translate}}
|
||||
</button>
|
||||
<button color="primary" mat-raised-button class="ok-button" [disabled]="type === undefined || dateControl.invalid"
|
||||
(click)="closeDialogWithSuccess()">
|
||||
{{'ACTIONS.ADD' | translate}}
|
||||
</button>
|
||||
</div>
|
@ -4,19 +4,19 @@
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: var(--grey);
|
||||
font-size: .9rem;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
.add-key-form-field {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
justify-content: space-between;
|
||||
|
||||
.ok-button {
|
||||
margin-left: .5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
@ -1,58 +1,49 @@
|
||||
<h1 mat-dialog-title>
|
||||
<span class="title">{{'MEMBER.ADD' | translate}}</span>
|
||||
<span class="title">{{'MEMBER.ADD' | translate}}</span>
|
||||
</h1>
|
||||
<p class="desc"> {{'ORG_DETAIL.MEMBER.ADDDESCRIPTION' | translate}}</p>
|
||||
<p class="desc cnsl-secondary-text"> {{'ORG_DETAIL.MEMBER.ADDDESCRIPTION' | translate}}</p>
|
||||
|
||||
<div mat-dialog-content>
|
||||
<!-- if no context -->
|
||||
<ng-container *ngIf="showCreationTypeSelector">
|
||||
<cnsl-form-field class="full-width" appearance="outline">
|
||||
<cnsl-label>{{ 'MEMBER.CREATIONTYPE' | translate }}</cnsl-label>
|
||||
<mat-select [(ngModel)]="creationType" (selectionChange)="loadRoles()">
|
||||
<mat-option *ngFor="let type of creationTypes" [value]="type.type"
|
||||
[disabled]="(type.disabled$ | async) === false">
|
||||
{{ 'MEMBER.CREATIONTYPES.'+type.type | translate}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</cnsl-form-field>
|
||||
|
||||
<ng-container
|
||||
*ngIf="creationType === CreationType.PROJECT_OWNED || creationType === CreationType.PROJECT_GRANTED">
|
||||
<p>{{'PROJECT.GRANT.CREATE.SEL_PROJECT' | translate}}</p>
|
||||
<cnsl-search-project-autocomplete class="block" [singleOutput]="true"
|
||||
(selectionChanged)="selectProject($event)"
|
||||
[autocompleteType]="creationType === CreationType.PROJECT_OWNED ? ProjectAutocompleteType.PROJECT_OWNED : creationType === CreationType.PROJECT_GRANTED ? ProjectAutocompleteType.PROJECT_GRANTED : undefined">
|
||||
</cnsl-search-project-autocomplete>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<!-- if no context end -->
|
||||
|
||||
<cnsl-search-user-autocomplete [users]="preselectedUsers" (selectionChanged)="users = $any($event)">
|
||||
</cnsl-search-user-autocomplete>
|
||||
|
||||
<cnsl-form-field class="full-width" appearance="outline"
|
||||
*ngIf="creationType === CreationType.PROJECT_OWNED || creationType === CreationType.PROJECT_GRANTED || creationType === CreationType.IAM">
|
||||
<cnsl-label>{{ 'ROLESLABEL' | translate }}</cnsl-label>
|
||||
<mat-select [(ngModel)]="roles" multiple>
|
||||
<mat-option *ngFor="let role of memberRoleOptions" [value]="role">
|
||||
{{ role }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<ng-container *ngIf="showCreationTypeSelector">
|
||||
<cnsl-form-field class="full-width" appearance="outline">
|
||||
<cnsl-label>{{ 'MEMBER.CREATIONTYPE' | translate }}</cnsl-label>
|
||||
<mat-select [(ngModel)]="creationType" (selectionChange)="loadRoles()">
|
||||
<mat-option *ngFor="let type of creationTypes" [value]="type.type"
|
||||
[disabled]="(type.disabled$ | async) === false">
|
||||
{{ 'MEMBER.CREATIONTYPES.'+type.type | translate}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</cnsl-form-field>
|
||||
|
||||
<ng-container *ngIf="creationType === CreationType.ORG">
|
||||
<cnsl-org-member-roles-autocomplete (selectionChanged)="setOrgMemberRoles($event)">
|
||||
</cnsl-org-member-roles-autocomplete>
|
||||
<ng-container *ngIf="creationType === CreationType.PROJECT_OWNED || creationType === CreationType.PROJECT_GRANTED">
|
||||
<p>{{'PROJECT.GRANT.CREATE.SEL_PROJECT' | translate}}</p>
|
||||
<cnsl-search-project-autocomplete class="block" (selectionChanged)="selectProject($event)"
|
||||
[autocompleteType]="creationType === CreationType.PROJECT_OWNED ? ProjectAutocompleteType.PROJECT_OWNED : creationType === CreationType.PROJECT_GRANTED ? ProjectAutocompleteType.PROJECT_GRANTED : undefined">
|
||||
</cnsl-search-project-autocomplete>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<cnsl-search-user-autocomplete [users]="preselectedUsers" (selectionChanged)="users = $any($event)">
|
||||
</cnsl-search-user-autocomplete>
|
||||
|
||||
<div class="roles-selection">
|
||||
<mat-checkbox class="role-cb" *ngFor="let role of memberRoleOptions" color="primary" (change)="toggleRole(role)"
|
||||
[checked]="roles.includes(role)">
|
||||
<div class="role-cb-content">
|
||||
<div class="cnsl-chip-dot" [style.background]="getColor(role)"></div>
|
||||
<span>{{role | roletransform}}</span>
|
||||
</div>
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div mat-dialog-actions class="action">
|
||||
<button mat-button (click)="closeDialog()">
|
||||
{{'ACTIONS.CANCEL' | translate}}
|
||||
</button>
|
||||
<button mat-stroked-button (click)="closeDialog()">
|
||||
{{'ACTIONS.CANCEL' | translate}}
|
||||
</button>
|
||||
|
||||
<button [disabled]="users.length === 0 || roles.length === 0" color="primary" mat-raised-button class="ok-button"
|
||||
(click)="closeDialogWithSuccess()">
|
||||
{{'ACTIONS.ADD' | translate}}
|
||||
</button>
|
||||
<button [disabled]="users.length === 0 || roles.length === 0" color="primary" mat-raised-button class="ok-button"
|
||||
(click)="closeDialogWithSuccess()">
|
||||
{{'ACTIONS.ADD' | translate}}
|
||||
</button>
|
||||
</div>
|
@ -3,19 +3,34 @@
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: var(--grey);
|
||||
font-size: .9rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
.roles-selection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
|
||||
.role-cb {
|
||||
padding: 0.25rem 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
.role-cb-content {
|
||||
padding-left: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
justify-content: space-between;
|
||||
|
||||
.ok-button {
|
||||
margin-left: .5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Observable } from 'rxjs';
|
||||
import { GrantedProject, Project, Role } from 'src/app/proto/generated/zitadel/project_pb';
|
||||
import { GrantedProject, Project } from 'src/app/proto/generated/zitadel/project_pb';
|
||||
import { User } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { getMembershipColor } from 'src/app/utils/color';
|
||||
|
||||
import { ProjectAutocompleteType } from '../search-project-autocomplete/search-project-autocomplete.component';
|
||||
|
||||
@ -33,14 +34,14 @@ export class MemberCreateDialogComponent {
|
||||
* without ending $, to enable write event permission even if user is allowed
|
||||
* to create members for only one specific project.
|
||||
*/
|
||||
public creationTypes: Array<{ type: CreationType, disabled$: Observable<boolean>; }> = [
|
||||
public creationTypes: Array<{ type: CreationType; disabled$: Observable<boolean> }> = [
|
||||
{ type: CreationType.IAM, disabled$: this.authService.isAllowed(['iam.member.write$']) },
|
||||
{ type: CreationType.ORG, disabled$: this.authService.isAllowed(['org.member.write$']) },
|
||||
{ type: CreationType.PROJECT_OWNED, disabled$: this.authService.isAllowed(['project.member.write']) },
|
||||
{ type: CreationType.PROJECT_GRANTED, disabled$: this.authService.isAllowed(['project.grant.member.write']) },
|
||||
];
|
||||
public users: Array<User.AsObject> = [];
|
||||
public roles: Array<Role.AsObject> | string[] = [];
|
||||
public roles: string[] = [];
|
||||
public CreationType: any = CreationType;
|
||||
public ProjectAutocompleteType: any = ProjectAutocompleteType;
|
||||
public memberRoleOptions: string[] = [];
|
||||
@ -72,26 +73,45 @@ export class MemberCreateDialogComponent {
|
||||
|
||||
public loadRoles(): void {
|
||||
switch (this.creationType) {
|
||||
case CreationType.ORG:
|
||||
this.mgmtService
|
||||
.listOrgMemberRoles()
|
||||
.then((resp) => {
|
||||
this.memberRoleOptions = resp.resultList;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastService.showError(error);
|
||||
});
|
||||
break;
|
||||
case CreationType.PROJECT_GRANTED:
|
||||
this.mgmtService.listProjectGrantMemberRoles().then(resp => {
|
||||
this.memberRoleOptions = resp.resultList;
|
||||
}).catch(error => {
|
||||
this.toastService.showError(error);
|
||||
});
|
||||
this.mgmtService
|
||||
.listProjectGrantMemberRoles()
|
||||
.then((resp) => {
|
||||
this.memberRoleOptions = resp.resultList;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastService.showError(error);
|
||||
});
|
||||
break;
|
||||
case CreationType.PROJECT_OWNED:
|
||||
this.mgmtService.listProjectMemberRoles().then(resp => {
|
||||
this.memberRoleOptions = resp.resultList;
|
||||
}).catch(error => {
|
||||
this.toastService.showError(error);
|
||||
});
|
||||
this.mgmtService
|
||||
.listProjectMemberRoles()
|
||||
.then((resp) => {
|
||||
this.memberRoleOptions = resp.resultList;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastService.showError(error);
|
||||
});
|
||||
break;
|
||||
case CreationType.IAM:
|
||||
this.adminService.listIAMMemberRoles().then(resp => {
|
||||
this.memberRoleOptions = resp.rolesList;
|
||||
}).catch(error => {
|
||||
this.toastService.showError(error);
|
||||
});
|
||||
this.adminService
|
||||
.listIAMMemberRoles()
|
||||
.then((resp) => {
|
||||
this.memberRoleOptions = resp.rolesList;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastService.showError(error);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -122,4 +142,17 @@ export class MemberCreateDialogComponent {
|
||||
public setOrgMemberRoles(roles: string[]): void {
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
public toggleRole(role: string): void {
|
||||
const index = this.roles.findIndex((r) => r === role);
|
||||
if (index > -1) {
|
||||
this.roles.splice(index, 1);
|
||||
} else {
|
||||
this.roles.push(role);
|
||||
}
|
||||
}
|
||||
|
||||
public getColor(role: string) {
|
||||
return getMembershipColor(role)[500];
|
||||
}
|
||||
}
|
||||
|
@ -2,36 +2,36 @@ import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { InputModule } from 'src/app/modules/input/input.module';
|
||||
import { SearchUserAutocompleteModule } from 'src/app/modules/search-user-autocomplete/search-user-autocomplete.module';
|
||||
import { RoleTransformPipeModule } from 'src/app/pipes/role-transform/role-transform.module';
|
||||
|
||||
import {
|
||||
OrgMemberRolesAutocompleteModule,
|
||||
} from '../../pages/orgs/org-member-roles-autocomplete/org-member-roles-autocomplete.module';
|
||||
import { SearchProjectAutocompleteModule } from '../search-project-autocomplete/search-project-autocomplete.module';
|
||||
import { SearchRolesAutocompleteModule } from '../search-roles-autocomplete/search-roles-autocomplete.module';
|
||||
import { MemberCreateDialogComponent } from './member-create-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [MemberCreateDialogComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatDialogModule,
|
||||
MatButtonModule,
|
||||
MatChipsModule,
|
||||
TranslateModule,
|
||||
InputModule,
|
||||
MatSelectModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
SearchUserAutocompleteModule,
|
||||
SearchRolesAutocompleteModule,
|
||||
SearchProjectAutocompleteModule,
|
||||
OrgMemberRolesAutocompleteModule,
|
||||
],
|
||||
declarations: [MemberCreateDialogComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatDialogModule,
|
||||
MatButtonModule,
|
||||
MatChipsModule,
|
||||
TranslateModule,
|
||||
InputModule,
|
||||
MatSelectModule,
|
||||
RoleTransformPipeModule,
|
||||
FormsModule,
|
||||
MatCheckboxModule,
|
||||
ReactiveFormsModule,
|
||||
SearchUserAutocompleteModule,
|
||||
SearchRolesAutocompleteModule,
|
||||
SearchProjectAutocompleteModule,
|
||||
],
|
||||
})
|
||||
export class MemberCreateDialogModule { }
|
||||
export class MemberCreateDialogModule {}
|
||||
|
@ -0,0 +1,21 @@
|
||||
<span class="title" mat-dialog-title>{{'MEMBER.EDITROLE' | translate}}</span>
|
||||
<div mat-dialog-content>
|
||||
<p class="desc"> {{'MEMBER.EDITFOR' | translate: ({value: data.user})}}</p>
|
||||
<div class="roles-selection">
|
||||
<mat-checkbox class="role-cb" *ngFor="let role of allRoles" color="primary" (change)="toggleRole(role)"
|
||||
[checked]="selectedRoles.includes(role)">
|
||||
<div class="role-cb-content">
|
||||
<div class="cnsl-chip-dot" [style.background]="getColor(role)"></div>
|
||||
<span>{{role | roletransform}}</span>
|
||||
</div>
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div mat-dialog-actions class="action">
|
||||
<button color="primary" mat-stroked-button class="close-button" (click)="closeDialog()">
|
||||
{{'ACTIONS.CLOSE' | translate}}
|
||||
</button>
|
||||
<button color="primary" mat-raised-button class="ok-button" (click)="closeDialogWithRoles()">
|
||||
{{'ACTIONS.CHANGE' | translate}}
|
||||
</button>
|
||||
</div>
|
@ -0,0 +1,29 @@
|
||||
.title {
|
||||
font-size: 1.2rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.roles-selection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.role-cb {
|
||||
padding: 0.25rem 0;
|
||||
|
||||
.role-cb-content {
|
||||
padding-left: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { AddMemberRolesDialogComponent } from './add-member-roles-dialog.component';
|
||||
|
||||
describe('AddMemberRolesDialogComponent', () => {
|
||||
let component: AddMemberRolesDialogComponent;
|
||||
let fixture: ComponentFixture<AddMemberRolesDialogComponent>;
|
||||
|
||||
beforeEach(
|
||||
waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AddMemberRolesDialogComponent],
|
||||
}).compileComponents();
|
||||
}),
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AddMemberRolesDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,39 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { getMembershipColor } from 'src/app/utils/color';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-add-member-roles-dialog',
|
||||
templateUrl: './add-member-roles-dialog.component.html',
|
||||
styleUrls: ['./add-member-roles-dialog.component.scss'],
|
||||
})
|
||||
export class AddMemberRolesDialogComponent {
|
||||
public allRoles: string[] = [];
|
||||
public selectedRoles: string[] = [];
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<AddMemberRolesDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: any) {
|
||||
this.allRoles = Object.assign([], data.allRoles);
|
||||
this.selectedRoles = Object.assign([], data.selectedRoles);
|
||||
}
|
||||
|
||||
public closeDialogWithRoles(): void {
|
||||
this.dialogRef.close(this.selectedRoles);
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
public toggleRole(role: string): void {
|
||||
const index = this.selectedRoles.findIndex((r) => r === role);
|
||||
if (index > -1) {
|
||||
this.selectedRoles.splice(index, 1);
|
||||
} else {
|
||||
this.selectedRoles.push(role);
|
||||
}
|
||||
}
|
||||
|
||||
public getColor(role: string) {
|
||||
return getMembershipColor(role)[500];
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
|
||||
import { RoleTransformPipeModule } from 'src/app/pipes/role-transform/role-transform.module';
|
||||
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
|
||||
|
||||
import { AddMemberRolesDialogComponent } from './add-member-roles-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AddMemberRolesDialogComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
TranslateModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
LocalizedDatePipeModule,
|
||||
RoleTransformPipeModule,
|
||||
TimestampToDatePipeModule,
|
||||
],
|
||||
})
|
||||
export class AddMemberRolesDialogModule {}
|
@ -7,7 +7,7 @@
|
||||
<input cnslInput [matDatepicker]="picker" [min]="startDate" [formControl]="dateControl">
|
||||
<mat-datepicker-toggle style="top: 0;" cnslSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker startView="year" [startAt]="startDate"></mat-datepicker>
|
||||
<span cnsl-error *ngIf="dateControl?.errors?.matDatepickerMin?.min">
|
||||
<span cnslError *ngIf="dateControl?.errors?.matDatepickerMin?.min">
|
||||
{{'USER.PERSONALACCESSTOKEN.ADD.CHOOSEDATEAFTER' | translate}}:
|
||||
{{dateControl?.errors?.matDatepickerMin.min.toDate() | localizedDate: 'EEE dd. MMM'}}
|
||||
</span>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div class="cnsl-app-card" [ngClass]="{'web': type === OIDCAppType.OIDC_APP_TYPE_WEB,
|
||||
<div class="cnsl-app-card" [ngClass]="{'add': type === OIDCAppType.ADD,'web': type === OIDCAppType.OIDC_APP_TYPE_WEB,
|
||||
'useragent': type === OIDCAppType.OIDC_APP_TYPE_USER_AGENT,
|
||||
'native': type === OIDCAppType.OIDC_APP_TYPE_NATIVE, 'api': isApiApp}">
|
||||
<ng-content></ng-content>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
@ -1,21 +1,19 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
@mixin app-card-theme($theme) {
|
||||
/* stylelint-disable */
|
||||
$primary: map-get($theme, primary);
|
||||
$background: map-get($theme, background);
|
||||
$foreground: map-get($theme, foreground);
|
||||
$accent: map-get($theme, accent);
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
$accent-color: mat.get-color-from-palette($primary, 500);
|
||||
$back: map-get($background, background);
|
||||
|
||||
/* stylelint-enable */
|
||||
|
||||
.cnsl-app-card {
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
animation: all .2s;
|
||||
animation: all 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@ -23,35 +21,39 @@
|
||||
font-size: 2rem;
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
margin: 1rem;
|
||||
text-transform: uppercase;
|
||||
border-radius: .5rem;
|
||||
font-weight: 800;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
background-color: $back;
|
||||
transition: background-color box-shadow .3s ease-in;
|
||||
transition: background-color box-shadow 0.3s ease-in;
|
||||
color: mat.get-color-from-palette($primary, default-contrast);
|
||||
|
||||
&.add {
|
||||
background-color: map-get($primary, 500);
|
||||
}
|
||||
|
||||
&.web {
|
||||
background-color: rgb(80, 110, 110);
|
||||
color: white;
|
||||
background: linear-gradient(40deg, #059669 30%, #047857);
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.native {
|
||||
background-color: #595d80;
|
||||
color: white;
|
||||
background: linear-gradient(40deg, #306ccc 30%, #4f46e5);
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.useragent {
|
||||
background-color: #6a506e;
|
||||
color: white;
|
||||
background: linear-gradient(40deg, #dc2626 30%, #db2777);
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.api {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
background: linear-gradient(40deg, #1f2937, #111827);
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,39 +1,39 @@
|
||||
<div class="radio-button-wrapper">
|
||||
<ng-container *ngFor="let method of authMethods; index as i">
|
||||
<input type="radio" [disabled]="method.disabled" (change)="emitChange()" [value]="method.key" [id]="method.key"
|
||||
[(ngModel)]="selected" />
|
||||
<label class="cnsl-radio-button" [ngClass]="{'first': i === 0, 'last': i === authMethods.length - 1}"
|
||||
[for]="method.key">
|
||||
<div class="recommended" [ngClass]="{'not': method.notRecommended}"
|
||||
*ngIf="method.recommended || method.notRecommended">
|
||||
{{(method.recommended ?
|
||||
'APP.OIDC.RECOMMENDED' : 'APP.OIDC.NOTRECOMMENDED') | translate }}</div>
|
||||
<div class="auth-method-radio-button-wrapper">
|
||||
<ng-container *ngFor="let method of authMethods; index as i">
|
||||
<input type="radio" [disabled]="method.disabled" (change)="emitChange()" [value]="method.key" [id]="method.key"
|
||||
[(ngModel)]="selected" />
|
||||
<label class="cnsl-radio-button" [ngClass]="{'first': i === 0, 'last': i === authMethods.length - 1}"
|
||||
[for]="method.key">
|
||||
<div class="recommended" [ngClass]="{'not': method.notRecommended}"
|
||||
*ngIf="method.recommended || method.notRecommended">
|
||||
{{(method.recommended ?
|
||||
'APP.OIDC.RECOMMENDED' : 'APP.OIDC.NOTRECOMMENDED') | translate }}</div>
|
||||
|
||||
<div class="cnsl-radio-header" [ngStyle]="{'background': method.background}">
|
||||
<span>{{method.prefix}}</span>
|
||||
<div class="current" *ngIf="current === method.key">{{'APP.OIDC.CURRENT' | translate}}</div>
|
||||
</div>
|
||||
<p>{{method.titleI18nKey | translate}}</p>
|
||||
<p class="type-desc">{{method.descI18nKey | translate}}</p>
|
||||
<span class="fill-space"></span>
|
||||
<div class="app-specs">
|
||||
<div class="row" *ngIf="isOIDC && method && method.responseType !== undefined">
|
||||
<span>{{'APP.OIDC.RESPONSETYPE' | translate}}</span>
|
||||
<span>{{('APP.OIDC.RESPONSE.'+method.responseType.toString()) | translate}}</span>
|
||||
</div>
|
||||
<div class="row" *ngIf="isOIDC && method.grantType !== undefined">
|
||||
<span>{{'APP.GRANT' | translate}}</span>
|
||||
<span>{{('APP.OIDC.GRANT.'+method.grantType.toString()) | translate}}</span>
|
||||
</div>
|
||||
<div class="row" *ngIf="isOIDC && method.authMethod !== undefined">
|
||||
<span>{{'APP.AUTHMETHOD' | translate}}</span>
|
||||
<span>{{('APP.OIDC.AUTHMETHOD.'+method.authMethod.toString()) | translate}}</span>
|
||||
</div>
|
||||
<div class="row" *ngIf="!isOIDC && method.apiAuthMethod !== undefined">
|
||||
<span>{{'APP.AUTHMETHOD' | translate}}</span>
|
||||
<span>{{('APP.API.AUTHMETHOD.'+method.apiAuthMethod.toString()) | translate}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</ng-container>
|
||||
<div class="cnsl-radio-header" [ngStyle]="{'background': method.background}">
|
||||
<span>{{method.prefix}}</span>
|
||||
<div class="current" *ngIf="current === method.key">{{'APP.OIDC.CURRENT' | translate}}</div>
|
||||
</div>
|
||||
<p>{{method.titleI18nKey | translate}}</p>
|
||||
<p class="type-desc cnsl-secondary-text">{{method.descI18nKey | translate}}</p>
|
||||
<span class="fill-space"></span>
|
||||
<div class="app-specs cnsl-secondary-text">
|
||||
<div class="row" *ngIf="isOIDC && method && method.responseType !== undefined">
|
||||
<span>{{'APP.OIDC.RESPONSETYPE' | translate}}</span>
|
||||
<span>{{('APP.OIDC.RESPONSE.'+method.responseType.toString()) | translate}}</span>
|
||||
</div>
|
||||
<div class="row" *ngIf="isOIDC && method.grantType !== undefined">
|
||||
<span>{{'APP.GRANT' | translate}}</span>
|
||||
<span>{{('APP.OIDC.GRANT.'+method.grantType.toString()) | translate}}</span>
|
||||
</div>
|
||||
<div class="row" *ngIf="isOIDC && method.authMethod !== undefined">
|
||||
<span>{{'APP.AUTHMETHOD' | translate}}</span>
|
||||
<span>{{('APP.OIDC.AUTHMETHOD.'+method.authMethod.toString()) | translate}}</span>
|
||||
</div>
|
||||
<div class="row" *ngIf="!isOIDC && method.apiAuthMethod !== undefined">
|
||||
<span>{{'APP.AUTHMETHOD' | translate}}</span>
|
||||
<span>{{('APP.API.AUTHMETHOD.'+method.apiAuthMethod.toString()) | translate}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</ng-container>
|
||||
</div>
|
@ -1,13 +1,13 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
.radio-button-wrapper {
|
||||
.auth-method-radio-button-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
margin: 0;
|
||||
padding-bottom: .5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
@ -15,15 +15,24 @@
|
||||
$primary: map-get($theme, primary);
|
||||
$primary-color: mat.get-color-from-palette($primary, 500);
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
$background: map-get($theme, background);
|
||||
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
|
||||
|
||||
input[type="radio"] {
|
||||
input[type='radio'] {
|
||||
appearance: none;
|
||||
opacity: 0;
|
||||
display: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
border: 1px solid if($is-dark-theme, white, black);
|
||||
}
|
||||
}
|
||||
|
||||
input:checked + label {
|
||||
border-color: if($is-dark-theme, white, var(--grey));
|
||||
border-color: $primary-color;
|
||||
border-width: 2px;
|
||||
box-shadow: 0 0 6px rgb(0 0 0 / 10%);
|
||||
|
||||
.cnsl-radio-header span {
|
||||
color: if($is-dark-theme, white, white);
|
||||
@ -31,16 +40,17 @@
|
||||
}
|
||||
|
||||
.cnsl-radio-button {
|
||||
margin: .5rem;
|
||||
border-radius: .5rem;
|
||||
border: 1px solid if($is-dark-theme, var(--grey), white);
|
||||
margin: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid $border-color;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: map-get($background, cards);
|
||||
flex: 0 1 230px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding-bottom: 1rem;
|
||||
box-shadow: inset 0 0 6px rgba(0, 0, 0, .1);
|
||||
|
||||
&.first {
|
||||
margin-left: 0;
|
||||
@ -62,6 +72,7 @@
|
||||
padding: 3px 1rem;
|
||||
box-shadow: 0 0 6px rgb(0 0 0 / 10%);
|
||||
white-space: nowrap;
|
||||
border: 1px solid $border-color;
|
||||
|
||||
&.not {
|
||||
background: rgb(144 75 75);
|
||||
@ -81,11 +92,11 @@
|
||||
|
||||
.current {
|
||||
position: absolute;
|
||||
bottom: .5rem;
|
||||
bottom: 0.5rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: block;
|
||||
color: #ffffff60;
|
||||
color: #ffffff90;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
@ -104,7 +115,6 @@
|
||||
|
||||
.type-desc {
|
||||
font-size: 14px;
|
||||
color: var(--grey);
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
@ -121,7 +131,6 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: var(--grey);
|
||||
margin: 3px 0;
|
||||
|
||||
span {
|
||||
|
@ -1,13 +1,13 @@
|
||||
<div class="radio-button-wrapper">
|
||||
<ng-container *ngFor="let type of types">
|
||||
<input class="app" type="radio" (change)="emitChange()" [value]="type" [(ngModel)]="selected" [id]="type.prefix" />
|
||||
<label class="cnsl-type-radio-button" [for]="type.prefix">
|
||||
<div class="cnsl-type-radio-header" [ngStyle]="{'background': type.background}">
|
||||
<span>{{type.prefix}}</span>
|
||||
</div>
|
||||
<p>{{type.titleI18nKey | translate}}</p>
|
||||
<p class="type-desc">{{type.descI18nKey | translate}}</p>
|
||||
<span class="fill-space"></span>
|
||||
</label>
|
||||
</ng-container>
|
||||
<div class="app-type-radio-button-wrapper">
|
||||
<ng-container *ngFor="let type of types">
|
||||
<input class="app" type="radio" (change)="emitChange()" [value]="type" [(ngModel)]="selected" [id]="type.prefix" />
|
||||
<label class="cnsl-type-radio-button" [for]="type.prefix">
|
||||
<div class="cnsl-type-radio-header" [ngStyle]="{'background': type.background}">
|
||||
<span>{{type.prefix}}</span>
|
||||
</div>
|
||||
<p>{{type.titleI18nKey | translate}}</p>
|
||||
<p class="type-desc cnsl-secondary-text">{{type.descI18nKey | translate}}</p>
|
||||
<span class="fill-space"></span>
|
||||
</label>
|
||||
</ng-container>
|
||||
</div>
|
@ -1,25 +1,36 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
.radio-button-wrapper {
|
||||
.app-type-radio-button-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -.5rem;
|
||||
margin: 0 -0.5rem;
|
||||
box-sizing: border-box;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@mixin app-type-radio-theme($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
$primary-color: mat.get-color-from-palette($primary, 500);
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
$background: map-get($theme, background);
|
||||
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
|
||||
|
||||
input[type="radio"].app {
|
||||
input[type='radio'].app {
|
||||
appearance: none;
|
||||
opacity: 0;
|
||||
display: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
border: 1px solid if($is-dark-theme, white, black);
|
||||
}
|
||||
}
|
||||
|
||||
input.app:checked + label {
|
||||
border-color: if($is-dark-theme, white, var(--grey));
|
||||
border-color: $primary-color;
|
||||
border-width: 2px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 0 6px rgb(0 0 0 / 10%);
|
||||
|
||||
.cnsl-type-radio-header span {
|
||||
color: if($is-dark-theme, white, white);
|
||||
@ -27,16 +38,18 @@
|
||||
}
|
||||
|
||||
.cnsl-type-radio-button {
|
||||
margin: .5rem;
|
||||
border-radius: .5rem;
|
||||
border: 1px solid if($is-dark-theme, var(--grey), white);
|
||||
margin: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid $border-color;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 1 240px;
|
||||
background-color: map-get($background, cards);
|
||||
flex: 0 1 230px;
|
||||
min-height: 300px;
|
||||
min-width: 150px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding-bottom: 1rem;
|
||||
box-shadow: inset 0 0 6px rgba(0, 0, 0, .1);
|
||||
|
||||
.cnsl-type-radio-header {
|
||||
display: flex;
|
||||
@ -44,6 +57,7 @@
|
||||
justify-content: center;
|
||||
background: rgb(80, 110, 110);
|
||||
margin-bottom: 1rem;
|
||||
box-sizing: border-box;
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
|
||||
@ -61,7 +75,6 @@
|
||||
|
||||
.type-desc {
|
||||
font-size: 14px;
|
||||
color: var(--grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,12 @@
|
||||
<div class="avatar-circle dontcloseonclick" matRipple [matRippleColor]="'#ffffff20'" [matRippleUnbounded]="true"
|
||||
[matRippleCentered]="true"
|
||||
[ngStyle]="{'height': size+'px', 'width': size+'px', 'fontSize': (fontSize-1)+'px', 'background': color}"
|
||||
<div class="avatar-circle dontcloseonclick" matRipple [matRippleColor]="'#ffffff20'" [matRippleUnbounded]="false"
|
||||
[ngStyle]="{'height': size+'px', 'width': size+'px', 'fontSize': (fontSize-1)+'px','fontWeight': fontWeight, 'background': (themeService.isDarkTheme | async) ? color[900]: color[300], 'color': (themeService.isDarkTheme | async) ? color[200]:color[900]}"
|
||||
[ngClass]="{'active': active}">
|
||||
<img class="dontcloseonclick" *ngIf="avatarUrl; else creds" [src]="avatarUrl"/>
|
||||
<ng-template #creds>{{credentials}}</ng-template>
|
||||
<ng-container *ngIf="isMachine; else human">
|
||||
<i class="machine-icon las la-robot"></i>
|
||||
</ng-container>
|
||||
<ng-template #human>
|
||||
<span class="dontcloseonclick">{{credentials}}</span>
|
||||
<img class="dontcloseonclick" *ngIf="avatarUrl" [src]="avatarUrl"
|
||||
onerror="this.src='./assets/images/transparent.png';this.onerror='';" />
|
||||
</ng-template>
|
||||
</div>
|
@ -12,11 +12,13 @@
|
||||
text-transform: uppercase;
|
||||
background-color: $primary-color;
|
||||
box-sizing: border-box;
|
||||
letter-spacing: .05em;
|
||||
letter-spacing: 0.05em;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-family: 'Lato', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
// box-shadow: 0 0 3px #0000001a;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
@ -24,6 +26,14 @@
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.machine-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ThemeService } from 'src/app/services/theme.service';
|
||||
import { Color, getColorHash } from 'src/app/utils/color';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-avatar',
|
||||
@ -7,87 +9,47 @@ import { Component, Input, OnInit } from '@angular/core';
|
||||
})
|
||||
export class AvatarComponent implements OnInit {
|
||||
@Input() name: string = '';
|
||||
@Input() credentials: string = '';
|
||||
@Input() size: number = 24;
|
||||
@Input() size: number = 32;
|
||||
@Input() fontSize: number = 14;
|
||||
@Input() fontWeight: number = 600;
|
||||
@Input() active: boolean = false;
|
||||
@Input() color: string = '';
|
||||
@Input() forColor: string = '';
|
||||
@Input() avatarUrl: string = '';
|
||||
constructor() { }
|
||||
@Input() isMachine: boolean = false;
|
||||
|
||||
constructor(public themeService: ThemeService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.credentials && this.forColor) {
|
||||
this.credentials = this.getInitials(this.forColor);
|
||||
if (!this.color) {
|
||||
this.color = this.getColor(this.forColor || '');
|
||||
}
|
||||
} else if (!this.credentials && this.name) {
|
||||
this.credentials = this.getInitials(this.name);
|
||||
if (!this.color) {
|
||||
this.color = this.getColor(this.name || '');
|
||||
}
|
||||
}
|
||||
|
||||
if (this.size > 50) {
|
||||
this.fontSize = 32;
|
||||
this.fontWeight = 500;
|
||||
}
|
||||
}
|
||||
|
||||
getInitials(fromName: string): string {
|
||||
const username = fromName.split('@')[0];
|
||||
let separator = '_';
|
||||
if (username.includes('-')) {
|
||||
separator = '-';
|
||||
public get credentials(): string {
|
||||
const toSplit = this.name ? this.name : this.forColor;
|
||||
|
||||
if (this.name) {
|
||||
const split = toSplit.split(' ');
|
||||
const initials = split[0].charAt(0) + (split[1] ? split[1].charAt(0) : '');
|
||||
return initials;
|
||||
} else {
|
||||
const username = toSplit.split('@')[0];
|
||||
let separator = '_';
|
||||
if (username.includes('-')) {
|
||||
separator = '-';
|
||||
}
|
||||
if (username.includes('.')) {
|
||||
separator = '.';
|
||||
}
|
||||
const split = username.split(separator);
|
||||
const initials = split[0].charAt(0) + (split[1] ? split[1].charAt(0) : '');
|
||||
return initials;
|
||||
}
|
||||
if (username.includes('.')) {
|
||||
separator = '.';
|
||||
}
|
||||
const split = username.split(separator);
|
||||
const initials = split[0].charAt(0) + (split[1] ? split[1].charAt(0) : '');
|
||||
return initials;
|
||||
}
|
||||
|
||||
getColor(userName: string): string {
|
||||
const colors = [
|
||||
'linear-gradient(40deg, #B44D51 30%, rgb(241,138,138))',
|
||||
'linear-gradient(40deg, #B75073 30%, rgb(234,96,143))',
|
||||
'linear-gradient(40deg, #84498E 30%, rgb(214,116,230))',
|
||||
'linear-gradient(40deg, #705998 30%, rgb(163,131,220))',
|
||||
'linear-gradient(40deg, #5C6598 30%, rgb(135,148,222))',
|
||||
'linear-gradient(40deg, #7F90D3 30%, rgb(181,196,247))',
|
||||
'linear-gradient(40deg, #3E93B9 30%, rgb(150,215,245))',
|
||||
'linear-gradient(40deg, #3494A0 30%, rgb(71,205,222))',
|
||||
'linear-gradient(40deg, #25716A 30%, rgb(58,185,173))',
|
||||
'linear-gradient(40deg, #427E41 30%, rgb(97,185,96))',
|
||||
'linear-gradient(40deg, #89A568 30%, rgb(176,212,133))',
|
||||
'linear-gradient(40deg, #90924D 30%, rgb(187,189,98))',
|
||||
'linear-gradient(40deg, #E2B032 30%, rgb(245,203,99))',
|
||||
'linear-gradient(40deg, #C97358 30%, rgb(245,148,118))',
|
||||
'linear-gradient(40deg, #6D5B54 30%, rgb(152,121,108))',
|
||||
'linear-gradient(40deg, #6B7980 30%, rgb(134,163,177))',
|
||||
];
|
||||
|
||||
let hash = 0;
|
||||
if (userName.length === 0) {
|
||||
return colors[hash];
|
||||
}
|
||||
|
||||
hash = this.hashCode(userName);
|
||||
return colors[hash % colors.length];
|
||||
public get color(): Color {
|
||||
const toGen = this.forColor || this.name || '';
|
||||
return getColorHash(toGen);
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
private hashCode(str: string, seed: number = 0): number {
|
||||
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
|
||||
for (let i = 0, ch; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i);
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||
}
|
||||
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
||||
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
||||
}
|
||||
/* eslint-enable */
|
||||
}
|
||||
|
@ -4,17 +4,9 @@ import { MatRippleModule } from '@angular/material/core';
|
||||
|
||||
import { AvatarComponent } from './avatar.component';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [AvatarComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatRippleModule,
|
||||
],
|
||||
exports: [
|
||||
AvatarComponent,
|
||||
],
|
||||
declarations: [AvatarComponent],
|
||||
imports: [CommonModule, MatRippleModule],
|
||||
exports: [AvatarComponent],
|
||||
})
|
||||
export class AvatarModule { }
|
||||
|
||||
export class AvatarModule {}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="card" [ngClass]="{'nomargin': nomargin, 'warn': warn}">
|
||||
<div class="card" [ngClass]="{'nomargin': nomargin, 'stretch': stretch, 'warn': warn}" [attr.data-e2e]="'app-card'">
|
||||
<div *ngIf="title || description" class="header" [ngClass]="{'bottom-margin': expanded}">
|
||||
<div *ngIf="title" class="row">
|
||||
<h2 class="title">{{title}}</h2>
|
||||
<h2 class="title" [attr.data-e2e]="'app-card-title'">{{title}}</h2>
|
||||
<span class="fill-space"></span>
|
||||
<ng-content select="[card-actions]"></ng-content>
|
||||
<button class="button" type="button" matTooltip="Expand or collapse" mat-icon-button
|
||||
@ -10,7 +10,7 @@
|
||||
<mat-icon *ngIf="expanded">keyboard_arrow_up</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<p *ngIf="description" class="desc">{{description}}</p>
|
||||
<p *ngIf="description" class="desc cnsl-secondary-text">{{description}}</p>
|
||||
</div>
|
||||
<div class="card-content" *ngIf="expanded" [@openClose]="animate">
|
||||
<ng-content></ng-content>
|
||||
|
@ -1,7 +1,7 @@
|
||||
.card {
|
||||
margin: 1rem 0;
|
||||
padding: 1.5rem;
|
||||
border-radius: .5rem;
|
||||
border-radius: 0.5rem;
|
||||
padding-top: 1rem;
|
||||
min-width: 300px;
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .05em;
|
||||
letter-spacing: 0.05em;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
@ -36,25 +36,28 @@
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-right: -.5rem;
|
||||
margin-right: -0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: .9rem;
|
||||
color: var(--grey);
|
||||
font-size: 14px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.stretch {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
margin: .5rem 0;
|
||||
margin: 0.5rem 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
@ -22,4 +22,5 @@ export class CardComponent {
|
||||
@Input() public description: string = '';
|
||||
@Input() public animate: boolean = false;
|
||||
@Input() public nomargin?: boolean = false;
|
||||
@Input() public stretch: boolean = false;
|
||||
}
|
||||
|
@ -1,15 +1,13 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
@mixin card-theme($theme) {
|
||||
/* stylelint-disable */
|
||||
$primary: map-get($theme, primary);
|
||||
$primary-color: mat.get-color-from-palette($primary, 500);
|
||||
$background: map-get($theme, background);
|
||||
$card-background-color: mat.get-color-from-palette($background, card);
|
||||
$card-background-color: mat.get-color-from-palette($background, cards);
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
|
||||
$border-selected-color: if($is-dark-theme, #ffffff, #000000);
|
||||
/* stylelint-enable */
|
||||
$border-selected-color: if($is-dark-theme, #fff, #000);
|
||||
|
||||
.card {
|
||||
background-color: $card-background-color;
|
||||
@ -18,7 +16,6 @@
|
||||
box-sizing: border-box;
|
||||
border-radius: 0.5rem;
|
||||
outline: none;
|
||||
height: 100%;
|
||||
|
||||
&.warn {
|
||||
border-color: var(--warn);
|
||||
@ -58,4 +55,25 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cnsl-chip {
|
||||
height: auto;
|
||||
border: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.cnsl-chip-list {
|
||||
padding: 0.5rem 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cnsl-chip-dot {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
background-color: #cd5c5c;
|
||||
margin-left: -4px;
|
||||
margin-right: 8px;
|
||||
box-shadow: 0 0 3px #0000001a;
|
||||
}
|
||||
}
|
||||
|
@ -1,45 +1,44 @@
|
||||
<div class="change-header">
|
||||
<span class="ch-header">{{ 'CHANGES.LISTTITLE' | translate }}</span>
|
||||
<button matTooltip="{{'ACTIONS.REFRESH' | translate}}" (click)="init()" mat-icon-button>
|
||||
<mat-icon class="icon">refresh</mat-icon>
|
||||
</button>
|
||||
<span class="ch-header">{{ 'CHANGES.LISTTITLE' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<div class="scroll-container" cnslScrollable (scrollPosition)="scrollHandler($event)">
|
||||
<li class="item change-item-back" *ngFor="let hist of data | async; index as histindex">
|
||||
<span *ngIf="hist.values[0].dates[0]" class="date">
|
||||
{{ hist.values[0].dates[0]| timestampToDate | localizedDate: 'dd. MMMM YYYY' }}
|
||||
</span>
|
||||
<div class="item" *ngFor="let dayelement of hist.values; index as i">
|
||||
<div class="row">
|
||||
<cnsl-avatar matTooltip="{{ dayelement.editorDisplayName }}"
|
||||
*ngIf="dayelement.editorDisplayName; else spacer" class="avatar"
|
||||
[name]="dayelement.editorDisplayName" [size]="32" [forColor]="dayelement?.editorPreferredLoginName ?? 'A'"
|
||||
[avatarUrl]="dayelement.editorAvatarUrl || ''">
|
||||
</cnsl-avatar>
|
||||
<ng-template #spacer>
|
||||
<div class="spacer"></div>
|
||||
</ng-template>
|
||||
<div class="actions">
|
||||
<div class="action" *ngFor="let action of dayelement.eventTypes; index as j">
|
||||
<button disabled mat-icon-button aria-label="Restore history"
|
||||
matTooltip="{{ dayelement.dates[j] | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}">
|
||||
<mat-icon class="icon">schedule</mat-icon>
|
||||
</button>
|
||||
|
||||
<span>
|
||||
<span class="msg">{{ action.localizedMessage }}</span>
|
||||
<span class="block">{{
|
||||
dayelement.dates[j] | timestampToDate | localizedDate: 'HH:mm'
|
||||
}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll-container">
|
||||
<li class="item" *ngFor="let hist of data | async; index as histindex">
|
||||
<span *ngIf="hist.values[0].dates[0]" class="date">
|
||||
{{ hist.values[0].dates[0]| timestampToDate | localizedDate: 'dd. MMM YYYY' }}
|
||||
</span>
|
||||
<div class="item" *ngFor="let dayelement of hist.values; index as i">
|
||||
<div class="row">
|
||||
<cnsl-avatar matTooltip="{{ dayelement.editorDisplayName }}" *ngIf="dayelement.editorDisplayName; else spacer"
|
||||
class="avatar" [name]="dayelement.editorDisplayName" [size]="32"
|
||||
[forColor]="dayelement?.editorPreferredLoginName ?? 'A'" [avatarUrl]="dayelement.editorAvatarUrl || ''">
|
||||
</cnsl-avatar>
|
||||
<ng-template #spacer>
|
||||
<div class="spacer"></div>
|
||||
</ng-template>
|
||||
<div class="change-actions">
|
||||
<div class="change-action" *ngFor="let action of dayelement.eventTypes; index as j">
|
||||
<div>
|
||||
<span class="msg">{{ action.localizedMessage }}</span>
|
||||
<span class="block"
|
||||
matTooltip="{{ dayelement.dates[j] | timestampToDate | localizedDate: 'dd. MM YYYY, HH:mm' }}">{{
|
||||
dayelement.dates[j] | timestampToDate | localizedDate: 'HH:mm'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<div class="sp-wrapper">
|
||||
<mat-spinner *ngIf="loading | async" diameter="25"></mat-spinner>
|
||||
</div>
|
||||
</div>
|
||||
<span class="end-container" *ngIf="bottom">{{'CHANGES.BOTTOM' | translate}}</span>
|
||||
</div>
|
||||
</li>
|
||||
<div class="ch-sp-wrapper">
|
||||
<mat-spinner *ngIf="loading | async" diameter="25"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<button (click)="more()" class="load-more-button" *ngIf="!bottom" mat-stroked-button>
|
||||
{{'CHANGES.LOADMORE' | translate}}
|
||||
</button>
|
||||
|
||||
<div *ngIf="bottom" class="end-container">
|
||||
<span class="cnsl-secondary-text">{{'CHANGES.BOTTOM' | translate}}</span>
|
||||
</div>
|
||||
</div>
|
@ -11,7 +11,7 @@
|
||||
font-weight: 400;
|
||||
margin-top: 1rem;
|
||||
font-size: 14px;
|
||||
letter-spacing: .05em;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@ -23,24 +23,25 @@
|
||||
@mixin changes-theme($theme) {
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
$foreground: map-get($theme, foreground);
|
||||
$secondary-text: map-get($foreground, secondary-text);
|
||||
|
||||
.scroll-container {
|
||||
max-height: 50vh;
|
||||
overflow-y: scroll;
|
||||
border-bottom: 1px solid map-get($foreground, divider);
|
||||
margin-bottom: .5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
.date {
|
||||
font-weight: 500;
|
||||
font-size: .8rem;
|
||||
font-size: 0.8rem;
|
||||
display: block;
|
||||
margin-bottom: .5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
padding: 10px 0;
|
||||
font-size: .8rem;
|
||||
font-size: 0.8rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
@ -50,31 +51,25 @@
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
.change-actions {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: -.5rem;
|
||||
margin-top: -0.25rem;
|
||||
margin-left: 1rem;
|
||||
|
||||
.action {
|
||||
.change-action {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
|
||||
.icon {
|
||||
width: 32px;
|
||||
display: inline-block;
|
||||
height: 1.2rem;
|
||||
line-height: 1.2rem;
|
||||
font-size: 1.2rem;
|
||||
color: var(--grey);
|
||||
}
|
||||
padding: 0.25rem 0;
|
||||
cursor: default;
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
font-size: .8rem;
|
||||
font-size: 0.8rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
@ -84,60 +79,32 @@
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.restore {
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
margin-left: 1rem;
|
||||
transform: opacity .2s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.restore {
|
||||
visibility: visible;
|
||||
display: inline-block;
|
||||
opacity: 1;
|
||||
color: #81868a;
|
||||
|
||||
&[disabled] {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
color: $secondary-text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* stylelint-disable */
|
||||
$primary: map-get($theme, primary);
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
$transparent-color: if($is-dark-theme, rgba(#8795a1, .2), rgba(#8795a1, .2));
|
||||
|
||||
/* stylelint-enable */
|
||||
|
||||
&.change-item-back {
|
||||
background-color: rgba($transparent-color, .93);
|
||||
transition: background-color .3s cubic-bezier(.645, .045, .355, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sp-wrapper {
|
||||
padding: .5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.ch-sp-wrapper {
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.load-more-button {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.end-container {
|
||||
font-size: 14px;
|
||||
margin: 1rem 0 1rem 0;
|
||||
display: block;
|
||||
color: var(--grey);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,16 +32,17 @@ export interface MappedChange {
|
||||
editorDisplayName: string;
|
||||
editorAvatarUrl: string;
|
||||
editorPreferredLoginName: string;
|
||||
eventTypes: Array<{ key: string; localizedMessage: string; }>;
|
||||
eventTypes: Array<{ key: string; localizedMessage: string }>;
|
||||
sequences: number[];
|
||||
}>;
|
||||
}
|
||||
|
||||
type ListChanges = ListMyUserChangesResponse.AsObject |
|
||||
ListUserChangesResponse.AsObject |
|
||||
ListProjectChangesResponse.AsObject |
|
||||
ListOrgChangesResponse.AsObject |
|
||||
ListAppChangesResponse.AsObject;
|
||||
type ListChanges =
|
||||
| ListMyUserChangesResponse.AsObject
|
||||
| ListUserChangesResponse.AsObject
|
||||
| ListProjectChangesResponse.AsObject
|
||||
| ListOrgChangesResponse.AsObject
|
||||
| ListAppChangesResponse.AsObject;
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-changes',
|
||||
@ -64,9 +65,7 @@ export class ChangesComponent implements OnInit, OnDestroy {
|
||||
public data!: Observable<MappedChange[]>;
|
||||
public changes!: ListChanges;
|
||||
private destroyed$: Subject<void> = new Subject();
|
||||
constructor(private mgmtUserService: ManagementService, private authUserService: GrpcAuthService) {
|
||||
|
||||
}
|
||||
constructor(private mgmtUserService: ManagementService, private authUserService: GrpcAuthService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.init();
|
||||
@ -82,24 +81,23 @@ export class ChangesComponent implements OnInit, OnDestroy {
|
||||
this.destroyed$.next();
|
||||
}
|
||||
|
||||
public scrollHandler(e: any): void {
|
||||
if (e === 'bottom') {
|
||||
this.more();
|
||||
}
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
let first: Promise<ListChanges>;
|
||||
switch (this.changeType) {
|
||||
case ChangeType.MYUSER: first = this.authUserService.listMyUserChanges(20, 0);
|
||||
case ChangeType.MYUSER:
|
||||
first = this.authUserService.listMyUserChanges(30, 0);
|
||||
break;
|
||||
case ChangeType.USER: first = this.mgmtUserService.listUserChanges(this.id, 20, 0);
|
||||
case ChangeType.USER:
|
||||
first = this.mgmtUserService.listUserChanges(this.id, 30, 0);
|
||||
break;
|
||||
case ChangeType.PROJECT: first = this.mgmtUserService.listProjectChanges(this.id, 20, 0);
|
||||
case ChangeType.PROJECT:
|
||||
first = this.mgmtUserService.listProjectChanges(this.id, 30, 0);
|
||||
break;
|
||||
case ChangeType.ORG: first = this.mgmtUserService.listOrgChanges(20, 0);
|
||||
case ChangeType.ORG:
|
||||
first = this.mgmtUserService.listOrgChanges(30, 0);
|
||||
break;
|
||||
case ChangeType.APP: first = this.mgmtUserService.listAppChanges(this.id, this.secId, 20, 0);
|
||||
case ChangeType.APP:
|
||||
first = this.mgmtUserService.listAppChanges(this.id, this.secId, 30, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -109,24 +107,30 @@ export class ChangesComponent implements OnInit, OnDestroy {
|
||||
this.data = this._data.asObservable().pipe(
|
||||
scan((acc, val) => {
|
||||
return false ? val.concat(acc) : acc.concat(val);
|
||||
}));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private more(): void {
|
||||
public more(): void {
|
||||
const cursor = this.getCursor();
|
||||
|
||||
let more: Promise<ListChanges>;
|
||||
|
||||
switch (this.changeType) {
|
||||
case ChangeType.MYUSER: more = this.authUserService.listMyUserChanges(20, cursor);
|
||||
case ChangeType.MYUSER:
|
||||
more = this.authUserService.listMyUserChanges(20, cursor);
|
||||
break;
|
||||
case ChangeType.USER: more = this.mgmtUserService.listUserChanges(this.id, 20, cursor);
|
||||
case ChangeType.USER:
|
||||
more = this.mgmtUserService.listUserChanges(this.id, 20, cursor);
|
||||
break;
|
||||
case ChangeType.PROJECT: more = this.mgmtUserService.listProjectChanges(this.id, 20, cursor);
|
||||
case ChangeType.PROJECT:
|
||||
more = this.mgmtUserService.listProjectChanges(this.id, 20, cursor);
|
||||
break;
|
||||
case ChangeType.ORG: more = this.mgmtUserService.listOrgChanges(20, cursor);
|
||||
case ChangeType.ORG:
|
||||
more = this.mgmtUserService.listOrgChanges(20, cursor);
|
||||
break;
|
||||
case ChangeType.APP: more = this.mgmtUserService.listAppChanges(this.id, this.secId, 20, cursor);
|
||||
case ChangeType.APP:
|
||||
more = this.mgmtUserService.listAppChanges(this.id, this.secId, 20, cursor);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -147,42 +151,45 @@ export class ChangesComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Maps the snapshot to usable format the updates source
|
||||
private mapAndUpdate(col: Promise<ListChanges>): any {
|
||||
if (this._done.value || this._loading.value) { return; }
|
||||
if (this._done.value || this._loading.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Map snapshot with doc ref (needed for cursor)
|
||||
if (!this.bottom) {
|
||||
// loading
|
||||
this._loading.next(true);
|
||||
|
||||
return from(col).pipe(
|
||||
take(1),
|
||||
tap((res: ListChanges) => {
|
||||
const values = res.resultList;
|
||||
const mapped = this.mapChanges(values);
|
||||
// update source with new values, done loading
|
||||
// this._data.next(values);
|
||||
this._data.next(mapped);
|
||||
return from(col)
|
||||
.pipe(
|
||||
take(1),
|
||||
tap((res: ListChanges) => {
|
||||
const values = res.resultList;
|
||||
const mapped = this.mapChanges(values);
|
||||
|
||||
this._loading.next(false);
|
||||
this._data.next(mapped);
|
||||
|
||||
// no more values, mark done
|
||||
if (!values.length) {
|
||||
this._done.next(true);
|
||||
}
|
||||
}),
|
||||
catchError(_ => {
|
||||
this._loading.next(false);
|
||||
this.bottom = true;
|
||||
return of([]);
|
||||
}),
|
||||
).subscribe();
|
||||
this._loading.next(false);
|
||||
|
||||
if (!values.length) {
|
||||
this._done.next(true);
|
||||
}
|
||||
}),
|
||||
catchError((_) => {
|
||||
this._loading.next(false);
|
||||
this.bottom = true;
|
||||
return of([]);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
private mapChanges(changes: Change.AsObject[]): {
|
||||
key: string; values: any[];
|
||||
key: string;
|
||||
values: any[];
|
||||
}[] {
|
||||
const splitted: { [editorId: string]: any[]; } = {};
|
||||
const splitted: { [editorId: string]: any[] } = {};
|
||||
changes.forEach((change) => {
|
||||
if (change.changeDate) {
|
||||
const index = `${this.getDateString(change.changeDate)}`;
|
||||
@ -230,7 +237,7 @@ export class ChangesComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
});
|
||||
const arr = Object.keys(splitted).map(key => {
|
||||
const arr = Object.keys(splitted).map((key) => {
|
||||
return { key: key, values: splitted[key] };
|
||||
});
|
||||
|
||||
@ -263,7 +270,7 @@ export class ChangesComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Order by descending property key
|
||||
keyDescOrder = (a: KeyValue<number, string>, b: KeyValue<number, string>): number => {
|
||||
return a.key > b.key ? -1 : (b.key > a.key ? 1 : 0);
|
||||
return a.key > b.key ? -1 : b.key > a.key ? 1 : 0;
|
||||
};
|
||||
/* eslint-enable */
|
||||
}
|
||||
|
@ -1,71 +1,72 @@
|
||||
<cnsl-refresh-table [loading]="loading$ | async" (refreshed)="refreshPage()" [dataSize]="dataSource.data.length"
|
||||
[timestamp]="keyResult?.details?.viewTimestamp" [selection]="selection">
|
||||
<div actions>
|
||||
<a [disabled]="([('project.app.write:' + projectId), 'project.app.write'] | hasRole | async) === false"
|
||||
color="primary" mat-raised-button (click)="openAddKey()">
|
||||
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
|
||||
</a>
|
||||
</div>
|
||||
[timestamp]="keyResult?.details?.viewTimestamp" [selection]="selection">
|
||||
<div actions>
|
||||
<a [disabled]="([('project.app.write:' + projectId), 'project.app.write'] | hasRole | async) === false"
|
||||
color="primary" mat-raised-button (click)="openAddKey()">
|
||||
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table class="table" mat-table [dataSource]="dataSource">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
|
||||
[checked]="selection.hasValue() && isAllSelected()"
|
||||
[indeterminate]="selection.hasValue() && !isAllSelected()">
|
||||
</mat-checkbox>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let key">
|
||||
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
|
||||
(change)="$event ? selection.toggle(key) : null" [checked]="selection.isSelected(key)">
|
||||
</mat-checkbox>
|
||||
</td>
|
||||
</ng-container>
|
||||
<div class="table-wrapper">
|
||||
<table class="table" mat-table [dataSource]="dataSource">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
|
||||
[checked]="selection.hasValue() && isAllSelected()"
|
||||
[indeterminate]="selection.hasValue() && !isAllSelected()">
|
||||
</mat-checkbox>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let key">
|
||||
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
|
||||
(change)="$event ? selection.toggle(key) : null" [checked]="selection.isSelected(key)">
|
||||
</mat-checkbox>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MACHINE.ID' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let key"> {{key?.id}} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MACHINE.ID' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let key"> {{key?.id}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MACHINE.TYPE' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let key"> {{'USER.MACHINE.KEYTYPES.'+key?.type | translate}} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MACHINE.TYPE' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let key"> {{'USER.MACHINE.KEYTYPES.'+key?.type | translate}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="creationDate">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MACHINE.CREATIONDATE' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let key">
|
||||
{{key.details.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM YYYY, HH:mm'}}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="creationDate">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MACHINE.CREATIONDATE' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let key">
|
||||
{{key.details.creationDate | timestampToDate | localizedDate: 'fromNow'}}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="expirationDate">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MACHINE.EXPIRATIONDATE' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let key">
|
||||
{{key.expirationDate | timestampToDate | localizedDate: 'EEE dd. MMM YYYY, HH:mm'}}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="expirationDate">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MACHINE.EXPIRATIONDATE' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let key">
|
||||
{{key.expirationDate | timestampToDate | localizedDate: 'fromNow'}}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let key">
|
||||
<button
|
||||
[disabled]="([('project.app.write:' + projectId), 'project.app.write'] | hasRole | async) === false"
|
||||
mat-icon-button color="warn" matTooltip="{{'ACTIONS.DELETE' | translate}}"
|
||||
(click)="deleteKey(key)">
|
||||
<i class="las la-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let key">
|
||||
<cnsl-table-actions>
|
||||
<button actions
|
||||
[disabled]="([('project.app.write:' + projectId), 'project.app.write'] | hasRole | async) === false"
|
||||
mat-icon-button color="warn" matTooltip="{{'ACTIONS.DELETE' | translate}}" (click)="deleteKey(key)">
|
||||
<i class="las la-trash"></i>
|
||||
</button>
|
||||
</cnsl-table-actions>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr class="highlight" mat-row *matRowDef="let key; columns: displayedColumns;"
|
||||
(click)="selection.toggle(key);">
|
||||
</tr>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr class="highlight" mat-row *matRowDef="let key; columns: displayedColumns;" (click)="selection.toggle(key);">
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</table>
|
||||
<cnsl-paginator #paginator class="paginator" [timestamp]="keyResult?.details?.viewTimestamp" [length]="keyResult?.details?.totalResult || 0" [pageSize]="10"
|
||||
[pageSizeOptions]="[5, 10, 20]" (page)="changePage($event)"></cnsl-paginator>
|
||||
</div>
|
||||
<cnsl-paginator #paginator class="paginator" [timestamp]="keyResult?.details?.viewTimestamp"
|
||||
[length]="keyResult?.details?.totalResult || 0" [pageSize]="20" [pageSizeOptions]="[10, 20, 50, 100]"
|
||||
(page)="changePage($event)"></cnsl-paginator>
|
||||
</cnsl-refresh-table>
|
@ -1,27 +1,3 @@
|
||||
|
||||
.table-wrapper {
|
||||
overflow: auto;
|
||||
|
||||
.table,
|
||||
.paginator {
|
||||
width: 100%;
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0 1rem;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
outline: none;
|
||||
|
||||
|
@ -21,38 +21,35 @@ import { InputModule } from '../input/input.module';
|
||||
import { PaginatorModule } from '../paginator/paginator.module';
|
||||
import { RefreshTableModule } from '../refresh-table/refresh-table.module';
|
||||
import { ShowKeyDialogModule } from '../show-key-dialog/show-key-dialog.module';
|
||||
import { TableActionsModule } from '../table-actions/table-actions.module';
|
||||
import { ClientKeysComponent } from './client-keys.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ClientKeysComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
HasRoleModule,
|
||||
CardModule,
|
||||
MatTableModule,
|
||||
PaginatorModule,
|
||||
MatIconModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatCheckboxModule,
|
||||
MatTooltipModule,
|
||||
HasRolePipeModule,
|
||||
TimestampToDatePipeModule,
|
||||
LocalizedDatePipeModule,
|
||||
TranslateModule,
|
||||
RefreshTableModule,
|
||||
InputModule,
|
||||
ShowKeyDialogModule,
|
||||
AddKeyDialogModule,
|
||||
],
|
||||
exports: [
|
||||
ClientKeysComponent,
|
||||
],
|
||||
declarations: [ClientKeysComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
HasRoleModule,
|
||||
CardModule,
|
||||
MatTableModule,
|
||||
PaginatorModule,
|
||||
MatIconModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatCheckboxModule,
|
||||
TableActionsModule,
|
||||
MatTooltipModule,
|
||||
HasRolePipeModule,
|
||||
TimestampToDatePipeModule,
|
||||
LocalizedDatePipeModule,
|
||||
TranslateModule,
|
||||
RefreshTableModule,
|
||||
InputModule,
|
||||
ShowKeyDialogModule,
|
||||
AddKeyDialogModule,
|
||||
],
|
||||
exports: [ClientKeysComponent],
|
||||
})
|
||||
export class ClientKeysModule { }
|
||||
export class ClientKeysModule {}
|
||||
|
@ -1,42 +1,34 @@
|
||||
<div class="groups">
|
||||
<span class="co-header">{{ title }}</span>
|
||||
<span class="sub-header">{{ description }} {{'MEMBER.DOCSINFO' | translate}} <a
|
||||
href="https://docs.zitadel.ch/docs/manuals/admin-managers" target="_blank">ZITADEL Managers</a>.
|
||||
</span>
|
||||
<div class="people">
|
||||
<div class="img-list" [@cardAnimation]="totalResult">
|
||||
<div class="contributor-groups">
|
||||
<div class="contributor-people">
|
||||
<div class="contributor-img-list" [ngClass]="{'padd-left': totalResult > 0}" [@cardAnimation]="totalResult">
|
||||
<mat-spinner class="spinner" diameter="20" *ngIf="loading"></mat-spinner>
|
||||
<ng-container *ngIf="totalResult < 10; else compact">
|
||||
<ng-container *ngFor="let member of membersSubject | async; index as i">
|
||||
<div @animate (click)="emitShowDetail()" class="avatar-circle"
|
||||
matTooltip="{{ member.displayName }} | {{member.rolesList?.join(' ')}}" [ngStyle]="{'z-index': 100 - i}">
|
||||
<div @animate (click)="emitShowDetail()" class="contributor-avatar-circle"
|
||||
matTooltip="{{ member.displayName }} | {{member.rolesList | roletransform}}"
|
||||
[ngStyle]="{'z-index': 20 - i}">
|
||||
<cnsl-avatar *ngIf="member && member.displayName && member.firstName && member.lastName; else cog"
|
||||
class="avatar dontcloseonclick" [avatarUrl]="member.avatarUrl|| ''"
|
||||
[forColor]="member.preferredLoginName ?? 'A'"
|
||||
[name]="member.displayName ? member.displayName : (member.firstName + ' '+ member.lastName)" [size]="32">
|
||||
class="contributor-avatar dontcloseonclick" [avatarUrl]="member.avatarUrl|| ''"
|
||||
[name]="member.displayName ? member.displayName : (member.firstName + ' '+ member.lastName)"
|
||||
[forColor]="member.preferredLoginName" [size]="32">
|
||||
</cnsl-avatar>
|
||||
<ng-template #cog>
|
||||
<div class="sa-icon">
|
||||
<i class="las la-user-cog"></i>
|
||||
</div>
|
||||
<cnsl-avatar [forColor]="member.preferredLoginName" [isMachine]="true">
|
||||
<i class="las la-robot"></i>
|
||||
</cnsl-avatar>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-template #compact>
|
||||
<div (click)="emitShowDetail()" class="avatar-circle" matTooltip="Click to show detail">
|
||||
<div (click)="emitShowDetail()" class="contributor-avatar-circle" matTooltip="Click to show detail">
|
||||
<span>{{totalResult}}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
<button class="add-img" (click)="emitAddMember()" [disabled]="disabled" mat-icon-button
|
||||
matTooltip="{{'ACTIONS.ADD' | translate}}" aria-label="Edit contributors">
|
||||
<button class="add-img" [ngClass]="{'no-margin': totalResult === 0}" (click)="emitAddMember()"
|
||||
[disabled]="disabled" mat-icon-button aria-label="Add member">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
<span class="fill-space"></span>
|
||||
<button matTooltip="{{'ACTIONS.REFRESH' | translate}}" class="refresh-img" (click)="emitRefresh()" mat-icon-button
|
||||
aria-label="refresh contributors">
|
||||
<mat-icon class="icon">refresh</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,92 +1,81 @@
|
||||
.groups {
|
||||
padding-top: 1rem;
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
.co-header {
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
letter-spacing: .05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@mixin contributors-theme($theme) {
|
||||
$foreground: map-get($theme, foreground);
|
||||
$background: map-get($theme, background);
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
|
||||
.sub-header {
|
||||
font-size: .8rem;
|
||||
color: var(--grey);
|
||||
}
|
||||
.contributor-groups {
|
||||
padding: 0.5rem 0;
|
||||
|
||||
.people {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.owner {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.img-list {
|
||||
width: 100%;
|
||||
margin-top: .5rem;
|
||||
margin-left: 1rem;
|
||||
.contributor-people {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
border-radius: 50vw;
|
||||
padding: 6px 9px;
|
||||
background-color: map-get($background, contributor);
|
||||
padding-left: 9px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
.spinner {
|
||||
margin-left: -15px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.add-img {
|
||||
float: left;
|
||||
margin: 0 8px 0 -15px;
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.refresh-img {
|
||||
float: left;
|
||||
margin: 0 0 0 -15px;
|
||||
|
||||
.icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-circle {
|
||||
float: left;
|
||||
margin: 0 8px 0 -15px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
border-radius: 50%;
|
||||
-webkit-box-shadow: 2px 0 7px -1px rgba(33, 34, 36, .5);
|
||||
-moz-box-shadow: 2px 0 7px -1px rgba(33, 34, 36, .5);
|
||||
box-shadow: 2px 0 7px -1px rgba(33, 34, 36, .5);
|
||||
cursor: pointer;
|
||||
.contributor-img-list {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.avatar {
|
||||
pointer-events: none;
|
||||
&.padd-left {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.sa-icon {
|
||||
display: block;
|
||||
width: 32px;
|
||||
margin: 0 .5rem;
|
||||
.spinner {
|
||||
margin: -10px 20px -10px -15px;
|
||||
}
|
||||
|
||||
i {
|
||||
margin: auto;
|
||||
font-size: 1.2rem;
|
||||
.add-img {
|
||||
float: left;
|
||||
margin: 0 0 0 -8px;
|
||||
|
||||
&.no-margin {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.margin-neg {
|
||||
margin-left: -1rem;
|
||||
.contributor-avatar-circle {
|
||||
float: left;
|
||||
margin: 0 8px 0 -15px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
line-height: 1rem;
|
||||
border-radius: 50%;
|
||||
-webkit-box-shadow: 2px 0 7px -1px rgba(33, 34, 36, 0.5);
|
||||
-moz-box-shadow: 2px 0 7px -1px rgba(33, 34, 36, 0.5);
|
||||
box-shadow: 2px 0 7px -1px rgba(33, 34, 36, 0.5);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: map-get($background, contributor-avatar);
|
||||
color: white;
|
||||
|
||||
.contributor-avatar {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sa-icon {
|
||||
display: block;
|
||||
width: 32px;
|
||||
margin: 0 0.5rem;
|
||||
|
||||
i {
|
||||
margin: auto;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.margin-neg {
|
||||
margin-left: -1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,25 +5,23 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RoleTransformPipeModule } from 'src/app/pipes/role-transform/role-transform.module';
|
||||
|
||||
import { AvatarModule } from '../avatar/avatar.module';
|
||||
import { ContributorsComponent } from './contributors.component';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [ContributorsComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
AvatarModule,
|
||||
MatIconModule,
|
||||
MatTooltipModule,
|
||||
MatButtonModule,
|
||||
MatProgressSpinnerModule,
|
||||
TranslateModule,
|
||||
],
|
||||
exports: [
|
||||
ContributorsComponent,
|
||||
],
|
||||
declarations: [ContributorsComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
AvatarModule,
|
||||
MatIconModule,
|
||||
MatTooltipModule,
|
||||
MatButtonModule,
|
||||
MatProgressSpinnerModule,
|
||||
RoleTransformPipeModule,
|
||||
TranslateModule,
|
||||
],
|
||||
exports: [ContributorsComponent],
|
||||
})
|
||||
export class ContributorsModule { }
|
||||
export class ContributorsModule {}
|
||||
|
@ -1,23 +1,25 @@
|
||||
<div [ngClass]="{'max-width-container': maxWidth, 'enlarged-container': !maxWidth}">
|
||||
<div class="detail-container">
|
||||
<div class="detail-left">
|
||||
<a *ngIf="backRouterLink" [routerLink]="backRouterLink" mat-icon-button>
|
||||
<mat-icon class="icon">arrow_back</mat-icon>
|
||||
<div class="max-width-container">
|
||||
<div class="enlarged-container">
|
||||
<div class="detail-layout-head">
|
||||
<div class="detail-layout-top-view">
|
||||
<div>
|
||||
<div class="back-row">
|
||||
<a *ngIf="hasBackButton" cnslBack mat-icon-button>
|
||||
<mat-icon class="icon">arrow_back</mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
<div class="detail-right">
|
||||
<div class="head">
|
||||
<div class="top-view">
|
||||
<div>
|
||||
<h1>{{ title }}</h1>
|
||||
<p class="head-desc">{{ description }}</p>
|
||||
</div>
|
||||
<div class="actions-wrap">
|
||||
<ng-content select="[actions]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
<ng-content></ng-content>
|
||||
<div>
|
||||
<h1>{{ title }}</h1>
|
||||
<ng-content select="[sub]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
<p class="head-desc cnsl-secondary-text max-width-description">{{ description }}</p>
|
||||
</div>
|
||||
<div class="actions-wrap">
|
||||
<ng-content select="[actions]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
@ -1,73 +1,44 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
@mixin detail-layout-theme($theme) {
|
||||
/* stylelint-disable */
|
||||
$primary: map-get($theme, primary);
|
||||
$primary-color: mat.get-color-from-palette($primary, 500);
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
/* stylelint-enable */
|
||||
$lighter-color: rgba(mat.get-color-from-palette($primary, 300), 0.5);
|
||||
|
||||
$lighter-color: rgba(mat.get-color-from-palette($primary, 300), .5);
|
||||
.detail-layout-head {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.detail-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 3rem;
|
||||
|
||||
@media only screen and (min-width: 550px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.detail-left {
|
||||
align-self: flex-start;
|
||||
.detail-layout-top-view {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
padding-top: 0;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 1rem;
|
||||
|
||||
@media only screen and (min-width: 550px) {
|
||||
width: 100px;
|
||||
}
|
||||
.back-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
a {
|
||||
margin-top: 13px;
|
||||
color: inherit;
|
||||
a {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-right {
|
||||
flex: 1;
|
||||
padding-left: 1rem;
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
flex-basis: 100%;
|
||||
div {
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.head {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.top-view {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
div {
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.head-desc {
|
||||
display: block;
|
||||
font-size: .9rem;
|
||||
color: var(--grey);
|
||||
}
|
||||
}
|
||||
|
||||
.actions-wrap {
|
||||
padding-top: .5rem;
|
||||
}
|
||||
.head-desc {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.actions-wrap {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,7 @@ import { Component, Input } from '@angular/core';
|
||||
styleUrls: ['./detail-layout.component.scss'],
|
||||
})
|
||||
export class DetailLayoutComponent {
|
||||
@Input() backRouterLink: any = undefined;
|
||||
@Input() hasBackButton: boolean = true;
|
||||
@Input() title: string | null = '';
|
||||
@Input() description: string | null = '';
|
||||
@Input() maxWidth: boolean = true;
|
||||
}
|
||||
|
@ -1,21 +1,15 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { BackModule } from 'src/app/directives/back/back.module';
|
||||
|
||||
import { DetailLayoutComponent } from './detail-layout.component';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [DetailLayoutComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatIconModule,
|
||||
RouterModule,
|
||||
],
|
||||
exports: [
|
||||
DetailLayoutComponent,
|
||||
],
|
||||
declarations: [DetailLayoutComponent],
|
||||
imports: [CommonModule, MatIconModule, BackModule, MatButtonModule, RouterModule],
|
||||
exports: [DetailLayoutComponent],
|
||||
})
|
||||
export class DetailLayoutModule { }
|
||||
export class DetailLayoutModule {}
|
||||
|
@ -1,15 +1,14 @@
|
||||
<div *ngIf="currentMap">
|
||||
|
||||
|
||||
<form [formGroup]="form">
|
||||
<ng-container *ngFor="let key of (current$ | async) | keyvalue">
|
||||
<div class="block">
|
||||
<div class="flex" *ngIf="(default$ | async) as defaultmap">
|
||||
<cnsl-form-field class="formfield">
|
||||
<div *ngIf="key.key !== 'isDefault'" class="edit-text-block">
|
||||
<div class="edit-text-flex" *ngIf="(default$ | async) as defaultmap">
|
||||
<cnsl-form-field class="edit-text-formfield">
|
||||
<cnsl-label>{{key.key}}</cnsl-label>
|
||||
<textarea class="text" cnslInput [formControlName]="key.key" [placeholder]="defaultmap[key.key]"
|
||||
[name]="key.key" [ngClass]="{'defaulttext': form.get(key.key)?.value === ''}"></textarea>
|
||||
<div class="chips" *ngIf="warnText[key.key] === undefined">
|
||||
<textarea class="edit-text-area" cnslInput [formControlName]="key.key"
|
||||
[placeholder]="$any(defaultmap[key.key])" [name]="key.key"
|
||||
[ngClass]="{'defaulttext': form.get(key.key)?.value === ''}"></textarea>
|
||||
<div class="edit-text-chips" *ngIf="warnText[key.key] === undefined">
|
||||
<ng-container *ngFor="let chip of chips">
|
||||
<div class="chip" cnslCopyToClipboard [valueToCopy]="chip.value" (copiedValue)="copied = $event"
|
||||
(click)="addChip(key.key, chip.value)">
|
||||
@ -21,22 +20,22 @@
|
||||
</ng-container>
|
||||
</div>
|
||||
</cnsl-form-field>
|
||||
<div class="actions">
|
||||
<div class="edit-text-actions">
|
||||
<button matTooltip="{{'ACTIONS.RESETDEFAULT'| translate }}" mat-icon-button
|
||||
[disabled]="form.get(key.key)?.value === defaultmap[key.key] || disabled"
|
||||
(click)="form.get(key.key)?.setValue(defaultmap[key.key])"
|
||||
(mouseenter)="form.get(key.key)?.value !== defaultmap[key.key] && setWarnText(key.key, defaultmap[key.key])"
|
||||
(mouseenter)="form.get(key.key)?.value !== defaultmap[key.key] && setWarnText(key.key, $any(defaultmap[key.key]))"
|
||||
(mouseleave)="setWarnText(key.key, undefined)"><i class="las la-history"></i></button>
|
||||
<button matTooltip="{{'ACTIONS.RESETCURRENT'| translate }}" mat-icon-button
|
||||
[disabled]="form.get(key.key)?.value === currentMap[key.key] || disabled"
|
||||
(click)="form.get(key.key)?.setValue(currentMap[key.key])"
|
||||
(mouseenter)="form.get(key.key)?.value !== currentMap[key.key] && setWarnText(key.key, currentMap[key.key])"
|
||||
(mouseenter)="form.get(key.key)?.value !== currentMap[key.key] && setWarnText(key.key, $any(currentMap[key.key]))"
|
||||
(mouseleave)="setWarnText(key.key, undefined)"><i class="las la-undo"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<cnsl-info-section *ngIf="warnText[key.key] !== undefined" class="info" [type]="InfoSectionType.WARN">
|
||||
<cnsl-info-section *ngIf="warnText[key.key] !== undefined" class="edit-text-info" [type]="InfoSectionType.WARN">
|
||||
{{'ACTIONS.RESETTO'| translate }} <cite>'{{warnText[key.key]}}'</cite></cnsl-info-section>
|
||||
</ng-container>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -1,96 +1,111 @@
|
||||
.block {
|
||||
display: block;
|
||||
@mixin edit-text-theme($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
$warn: map-get($theme, warn);
|
||||
$background: map-get($theme, background);
|
||||
$accent: map-get($theme, accent);
|
||||
$primary-color: mat.get-color-from-palette($primary, 500);
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
$warn-color: mat.get-color-from-palette($warn, 500);
|
||||
$accent-color: mat.get-color-from-palette($accent, 500);
|
||||
$foreground: map-get($theme, foreground);
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
$back: map-get($background, background);
|
||||
|
||||
.formfield {
|
||||
flex: 1;
|
||||
.edit-text-block {
|
||||
display: block;
|
||||
|
||||
.text {
|
||||
min-height: 80px;
|
||||
}
|
||||
.edit-text-flex {
|
||||
display: flex;
|
||||
|
||||
&.hovering {
|
||||
background-color: red;
|
||||
}
|
||||
.edit-text-formfield {
|
||||
flex: 1;
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
opacity: 0;
|
||||
margin: 0 -.25rem;
|
||||
transition: all .2s ease;
|
||||
.edit-text-area {
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
border-radius: 50vw;
|
||||
padding: 4px .5rem;
|
||||
font-size: 12px;
|
||||
background: #5282c1;
|
||||
color: white;
|
||||
margin: .25rem;
|
||||
&.hovering {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.edit-text-chips {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
flex-wrap: wrap;
|
||||
opacity: 0;
|
||||
margin: 0 -0.25rem;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
* {
|
||||
transition: all .2s ease;
|
||||
}
|
||||
.chip {
|
||||
border-radius: 50vw;
|
||||
padding: 4px 0.5rem;
|
||||
font-size: 12px;
|
||||
background: $primary-color;
|
||||
color: mat.get-color-from-palette($primary, default-contrast);
|
||||
margin: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
|
||||
i {
|
||||
opacity: .5;
|
||||
font-size: 1.1rem;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
* {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.key {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.value {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
i {
|
||||
opacity: 1;
|
||||
opacity: 0.5;
|
||||
font-size: 1.1rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.key {
|
||||
display: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.value {
|
||||
display: inline-block;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
i {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.key {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.value {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.cnsl-focused {
|
||||
.chips {
|
||||
opacity: 1;
|
||||
cursor: copy;
|
||||
&.cnsl-focused {
|
||||
.edit-text-chips {
|
||||
opacity: 1;
|
||||
cursor: copy;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-text-chips:hover {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.chips:hover {
|
||||
visibility: visible;
|
||||
.edit-text-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: flex-start;
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: flex-start;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.edit-text-info {
|
||||
display: block;
|
||||
margin-right: 40px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
display: block;
|
||||
margin-right: 40px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
@ -12,14 +12,14 @@ import { InfoSectionType } from '../info-section/info-section.component';
|
||||
})
|
||||
export class EditTextComponent implements OnInit, OnDestroy {
|
||||
@Input() label: string = '';
|
||||
@Input() current$!: Observable<{ [key: string]: any | string }>;
|
||||
@Input() default$!: Observable<{ [key: string]: any | string }>;
|
||||
@Input() current$!: Observable<{ [key: string]: string | boolean }>;
|
||||
@Input() default$!: Observable<{ [key: string]: string | boolean }>;
|
||||
@Input() currentlyDragged: string = '';
|
||||
@Output() changedValues: EventEmitter<{ [key: string]: string }> = new EventEmitter();
|
||||
public currentMap: { [key: string]: string } = {};
|
||||
public currentMap: { [key: string]: string | boolean } = {}; // boolean because of isDefault
|
||||
private destroy$: Subject<void> = new Subject();
|
||||
public form!: FormGroup;
|
||||
public warnText: { [key: string]: string | undefined } = {};
|
||||
public warnText: { [key: string]: string | boolean | undefined } = {};
|
||||
|
||||
@Input() public chips: any[] = [];
|
||||
@Input() public disabled: boolean = true;
|
||||
@ -47,7 +47,7 @@ export class EditTextComponent implements OnInit, OnDestroy {
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
public setWarnText(key: string, text: string | undefined): void {
|
||||
public setWarnText(key: string, text: string | boolean | undefined): void {
|
||||
this.warnText[key] = text;
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,17 @@
|
||||
<cnsl-detail-layout [backRouterLink]="[ serviceType === FeatureServiceType.ADMIN ? '/iam/policies' : '/org']"
|
||||
[title]="('FEATURES.TITLE' | translate)" [description]="'FEATURES.DESCRIPTION' | translate">
|
||||
|
||||
<cnsl-detail-layout [hasBackButton]="true" [title]="('FEATURES.TITLE' | translate)">
|
||||
<p class="subinfo" sub>
|
||||
<span class="cnsl-secondary-text">{{'FEATURES.DESCRIPTION' | translate}}</span>
|
||||
<a mat-icon-button href="https://docs.zitadel.ch/docs/legal/introduction" target="_blank">
|
||||
<i class="las la-info-circle"></i>
|
||||
</a>
|
||||
</p>
|
||||
<h2>{{'FEATURES.TIER.TITLE' | translate}}</h2>
|
||||
<p *ngIf="serviceType === FeatureServiceType.MGMT" class="tier-desc">{{'FEATURES.TIER.DESCRIPTION' | translate}}
|
||||
<p *ngIf="serviceType === FeatureServiceType.MGMT" class="tier-desc cnsl-secondary-text">{{'FEATURES.TIER.DESCRIPTION'
|
||||
| translate}}
|
||||
{{'FEATURES.TIER.QUESTIONS' | translate}} <a href="mailto:support@zitadel.ch">support@zitadel.ch</a>.</p>
|
||||
|
||||
<div class="detail">
|
||||
<p class="title">{{'FEATURES.TIER.NAME' | translate}}</p>
|
||||
<p class="title cnsl-secondary-text">{{'FEATURES.TIER.NAME' | translate}}</p>
|
||||
<p class="center">{{features?.tier?.name}}
|
||||
<a class="ext" href="https://zitadel.ch/pricing" target="_blank">
|
||||
<i class="las la-external-link-alt"></i>
|
||||
@ -17,7 +22,7 @@
|
||||
<ng-container *ngIf="serviceType === FeatureServiceType.MGMT">
|
||||
<mat-spinner class="spinner" diameter="20" *ngIf="customerLoading || stripeLoading"></mat-spinner>
|
||||
<div class="detail" *ngIf="stripeCustomer || stripeCustomer === null">
|
||||
<p class="title">{{'FEATURES.TIER.DETAILS' | translate}}
|
||||
<p class="title cnsl-secondary-text">{{'FEATURES.TIER.DETAILS' | translate}}
|
||||
<a (click)="setCustomer()">{{'ACTIONS.EDIT' | translate}}</a>
|
||||
</p>
|
||||
<p>{{stripeCustomer?.contact}}</p>
|
||||
@ -31,7 +36,7 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="error" *ngIf="(stripeCustomer || stripeCustomer === null) && !customerValid">
|
||||
<p class="error-tier-message" *ngIf="(stripeCustomer || stripeCustomer === null) && !customerValid">
|
||||
{{'FEATURES.TIER.CUSTOMERINVALID' | translate}}</p>
|
||||
|
||||
<div class="current-tier">
|
||||
@ -47,8 +52,6 @@
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<cnsl-info-section *ngIf="isDefault"> {{'POLICY.DEFAULTLABEL' | translate}}</cnsl-info-section>
|
||||
<div class="content" *ngIf="features">
|
||||
<div class="row">
|
||||
@ -58,7 +61,7 @@
|
||||
translate}}</span>
|
||||
</div>
|
||||
|
||||
<p class="feature-section">{{'FEATURES.HEADERS.LOGINPOLICY' | translate}}</p>
|
||||
<p class="feature-section cnsl-secondary-text">{{'FEATURES.HEADERS.LOGINPOLICY' | translate}}</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="featureavatar green">
|
||||
@ -138,7 +141,7 @@
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
<p class="feature-section">{{'FEATURES.HEADERS.PASSWORD' | translate}}</p>
|
||||
<p class="feature-section cnsl-secondary-text">{{'FEATURES.HEADERS.PASSWORD' | translate}}</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="featureavatar yellow">
|
||||
@ -168,7 +171,7 @@
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
<p class="feature-section">{{'FEATURES.HEADERS.LABELPOLICY' | translate}}</p>
|
||||
<p class="feature-section cnsl-secondary-text">{{'FEATURES.HEADERS.LABELPOLICY' | translate}}</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="featureavatar blue">
|
||||
@ -196,7 +199,7 @@
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
<p class="feature-section">{{'FEATURES.HEADERS.DOMAIN' | translate}}</p>
|
||||
<p class="feature-section cnsl-secondary-text">{{'FEATURES.HEADERS.DOMAIN' | translate}}</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="featureavatar purple">
|
||||
@ -210,7 +213,7 @@
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
<p class="feature-section">{{'FEATURES.HEADERS.TEXTSANDLINKS' | translate}}</p>
|
||||
<p class="feature-section cnsl-secondary-text">{{'FEATURES.HEADERS.TEXTSANDLINKS' | translate}}</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="featureavatar red">
|
||||
@ -251,7 +254,7 @@
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
<p class="feature-section">{{'FEATURES.HEADERS.METADATA' | translate}}</p>
|
||||
<p class="feature-section cnsl-secondary-text">{{'FEATURES.HEADERS.METADATA' | translate}}</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="featureavatar blue">
|
||||
|
@ -1,5 +1,17 @@
|
||||
.subinfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
margin: -1.5rem 0 0 0;
|
||||
|
||||
i {
|
||||
font-size: 1.2rem;
|
||||
height: 1.2rem;
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.tier-desc {
|
||||
color: var(--grey);
|
||||
font-size: 14px;
|
||||
margin-top: 0;
|
||||
}
|
||||
@ -16,7 +28,6 @@
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
color: var(--grey);
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
a {
|
||||
@ -45,7 +56,7 @@
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
.error-tier-message {
|
||||
color: var(--warn);
|
||||
font-size: 14px;
|
||||
}
|
||||
@ -56,15 +67,6 @@
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-color: var(--grey);
|
||||
opacity: 0.5;
|
||||
margin: 0.5rem 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1rem;
|
||||
display: flex;
|
||||
@ -73,7 +75,6 @@
|
||||
|
||||
.feature-section {
|
||||
font-size: 14px;
|
||||
color: var(--grey);
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ import { Component, Injector, OnDestroy, Type } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import {
|
||||
GetOrgFeaturesResponse,
|
||||
SetDefaultFeaturesRequest,
|
||||
@ -12,6 +11,7 @@ import { ActionsAllowed, Features } from 'src/app/proto/generated/zitadel/featur
|
||||
import { GetFeaturesResponse } from 'src/app/proto/generated/zitadel/management_pb';
|
||||
import { Org } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { StorageKey, StorageLocation, StorageService } from 'src/app/services/storage.service';
|
||||
import { StripeCustomer, SubscriptionService } from 'src/app/services/subscription.service';
|
||||
@ -61,42 +61,51 @@ export class FeaturesComponent implements OnDestroy {
|
||||
private adminService: AdminService,
|
||||
private subService: SubscriptionService,
|
||||
private dialog: MatDialog,
|
||||
breadcrumbService: BreadcrumbService,
|
||||
) {
|
||||
const iamBread = new Breadcrumb({
|
||||
type: BreadcrumbType.IAM,
|
||||
name: 'IAM',
|
||||
routerLink: ['/system'],
|
||||
});
|
||||
const bread: Breadcrumb = {
|
||||
type: BreadcrumbType.ORG,
|
||||
routerLink: ['/org'],
|
||||
};
|
||||
breadcrumbService.setBreadcrumb([iamBread, bread]);
|
||||
|
||||
const temporg: Org.AsObject | null = this.storage.getItem(StorageKey.organization, StorageLocation.session);
|
||||
|
||||
if (temporg) {
|
||||
this.org = temporg;
|
||||
}
|
||||
this.sub = this.route.data
|
||||
.pipe(
|
||||
switchMap((data) => {
|
||||
this.serviceType = data.serviceType;
|
||||
if (this.serviceType === FeatureServiceType.MGMT) {
|
||||
this.managementService = this.injector.get(ManagementService as Type<ManagementService>);
|
||||
}
|
||||
return this.route.params;
|
||||
}),
|
||||
)
|
||||
.subscribe((_) => {
|
||||
this.fetchData();
|
||||
});
|
||||
|
||||
if (this.serviceType === FeatureServiceType.MGMT) {
|
||||
this.customerLoading = true;
|
||||
this.subService
|
||||
.getCustomer(this.org.id)
|
||||
.then((payload) => {
|
||||
this.customerLoading = false;
|
||||
this.stripeCustomer = payload;
|
||||
if (this.customerValid) {
|
||||
this.getLinkToStripe();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.customerLoading = false;
|
||||
console.error(error);
|
||||
});
|
||||
const serviceType = this.route.snapshot.data.serviceType;
|
||||
if (serviceType !== undefined) {
|
||||
this.serviceType = serviceType;
|
||||
if (this.serviceType === FeatureServiceType.MGMT) {
|
||||
this.managementService = this.injector.get(ManagementService as Type<ManagementService>);
|
||||
}
|
||||
|
||||
if (this.serviceType === FeatureServiceType.MGMT) {
|
||||
this.customerLoading = true;
|
||||
this.subService
|
||||
.getCustomer(this.org.id)
|
||||
.then((payload) => {
|
||||
this.customerLoading = false;
|
||||
this.stripeCustomer = payload;
|
||||
if (this.customerValid) {
|
||||
this.getLinkToStripe();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.customerLoading = false;
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
@ -113,7 +122,6 @@ export class FeaturesComponent implements OnDestroy {
|
||||
|
||||
dialogRefPhone.afterClosed().subscribe((customer) => {
|
||||
if (customer) {
|
||||
console.log(customer);
|
||||
this.stripeCustomer = customer;
|
||||
this.subService
|
||||
.setCustomer(this.org.id, customer)
|
||||
@ -183,7 +191,6 @@ export class FeaturesComponent implements OnDestroy {
|
||||
req.setPrivacyPolicy(this.features.privacyPolicy);
|
||||
req.setMetadataUser(this.features.metadataUser);
|
||||
req.setLockoutPolicy(this.features.lockoutPolicy);
|
||||
// req.setActions(this.features.actions);
|
||||
req.setActionsAllowed(this.features.actionsAllowed);
|
||||
req.setMaxActions(this.features.maxActions);
|
||||
|
||||
@ -213,7 +220,6 @@ export class FeaturesComponent implements OnDestroy {
|
||||
dreq.setCustomTextMessage(this.features.customTextMessage);
|
||||
dreq.setMetadataUser(this.features.metadataUser);
|
||||
dreq.setLockoutPolicy(this.features.lockoutPolicy);
|
||||
// dreq.setActions(this.features.actions);
|
||||
dreq.setActionsAllowed(this.features.actionsAllowed);
|
||||
dreq.setMaxActions(this.features.maxActions);
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user