mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 15:49:35 +00:00
rehaul havigation
This commit is contained in:
@@ -75,7 +75,7 @@ const routes: Routes = [
|
||||
loadChildren: () => import('./pages/actions/actions.module'),
|
||||
canActivate: [authGuard, roleGuard],
|
||||
data: {
|
||||
roles: ['org.action.read', 'org.flow.read'],
|
||||
roles: ['iam.read', 'iam.read'],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@@ -5,13 +5,12 @@
|
||||
*ngIf="
|
||||
breadc[breadc.length - 1] &&
|
||||
!breadc[breadc.length - 1].hideNav &&
|
||||
breadc[breadc.length - 1].type !== BreadcrumbType.AUTHUSER &&
|
||||
breadc[breadc.length - 1].type !== BreadcrumbType.INSTANCE
|
||||
breadc[breadc.length - 1].type !== BreadcrumbType.AUTHUSER
|
||||
"
|
||||
[ngSwitch]="breadc[0].type"
|
||||
>
|
||||
<div class="nav-row" @navrow>
|
||||
<ng-container *ngSwitchCase="BreadcrumbType.ORG">
|
||||
<ng-container *ngSwitchCase="BreadcrumbType.INSTANCE">
|
||||
<div class="nav-row-abs" @navrowproject>
|
||||
<a
|
||||
class="nav-item"
|
||||
@@ -22,6 +21,48 @@
|
||||
<span class="label">{{ 'MENU.DASHBOARD' | translate }}</span>
|
||||
</a>
|
||||
|
||||
<ng-container class="org-list" *ngIf="org">
|
||||
<ng-template cnslHasRole [hasRole]="['org.read']">
|
||||
<a
|
||||
class="nav-item"
|
||||
[routerLinkActive]="['active']"
|
||||
[routerLinkActiveOptions]="{ exact: false }"
|
||||
[routerLink]="['/orgs']"
|
||||
>
|
||||
<span class="label">{{ 'MENU.ORGS' | translate }}</span>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['org.action.read']">
|
||||
<a
|
||||
class="nav-item"
|
||||
[routerLinkActive]="['active']"
|
||||
[routerLink]="['/actions']"
|
||||
[routerLinkActiveOptions]="{ exact: false }"
|
||||
>
|
||||
<span class="label">{{ 'MENU.ACTIONS' | translate }}</span>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['org.read']">
|
||||
<a
|
||||
class="nav-item"
|
||||
[routerLinkActive]="['active']"
|
||||
[routerLinkActiveOptions]="{ exact: false }"
|
||||
[routerLink]="['/instance']"
|
||||
*ngIf="['policy.read'] | hasRole | async"
|
||||
>
|
||||
<span class="label">{{ 'MENU.SETTINGS' | translate }}</span>
|
||||
</a>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<template [ngTemplateOutlet]="shortcutKeyRef"></template>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="BreadcrumbType.ORG">
|
||||
<div class="nav-row-abs" @navrowproject>
|
||||
<ng-container class="org-list" *ngIf="org">
|
||||
<ng-template cnslHasRole [hasRole]="['org.read']">
|
||||
<a
|
||||
|
@@ -23,7 +23,8 @@
|
||||
.nav-row {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
height: 40px; // Increased height to accommodate the line
|
||||
overflow: visible; // Allow the indicator line to show below
|
||||
|
||||
.nav-row-abs {
|
||||
padding: 0 2rem;
|
||||
@@ -32,7 +33,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
overflow-y: visible; // Allow the indicator line to show
|
||||
align-self: stretch;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -51,19 +52,46 @@
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
padding: 0.4rem 12px;
|
||||
color: map-get($foreground, text);
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
border-radius: 50vw;
|
||||
font-weight: 500;
|
||||
margin: 0.25rem 2px;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
height: 36px;
|
||||
box-sizing: border-box;
|
||||
height: 27px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
padding: 0 0.5rem;
|
||||
cursor: pointer;
|
||||
color: map-get($foreground, text);
|
||||
opacity: 0.8;
|
||||
|
||||
// Active indicator line
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 0;
|
||||
height: 1px;
|
||||
background-color: map-get($foreground, text);
|
||||
transition: width 0.2s ease;
|
||||
border-radius: 2px;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: map-get($foreground, text);
|
||||
background-color: if($is-dark-theme, #ffffff10, #00000010);
|
||||
}
|
||||
|
||||
.c_label {
|
||||
display: flex;
|
||||
@@ -97,14 +125,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: if($is-dark-theme, #ffffff40, #00000010);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $primary-color;
|
||||
opacity: 1;
|
||||
color: map-get($primary, default-contrast);
|
||||
|
||||
&::after {
|
||||
width: 100%; // Full width for testing
|
||||
}
|
||||
|
||||
.c_label {
|
||||
.count {
|
||||
display: inline-block;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div class="new-header-wrapper">
|
||||
<ng-container *ngIf="myInstanceQuery.data()?.instance as instance">
|
||||
<ng-container *ngTemplateOutlet="slash"></ng-container>
|
||||
<a class="new-header-breadcrumb" matRipple [matRippleUnbounded]="false" [routerLink]="['/instance']">
|
||||
<a class="new-header-breadcrumb" matRipple [matRippleUnbounded]="false" [routerLink]="['/']">
|
||||
{{ instance.name }}
|
||||
</a>
|
||||
<cnsl-header-button
|
||||
@@ -37,7 +37,7 @@
|
||||
class="new-header-breadcrumb"
|
||||
matRipple
|
||||
[matRippleUnbounded]="false"
|
||||
[routerLink]="['/']"
|
||||
[routerLink]="['/org']"
|
||||
>
|
||||
{{ org.name }}
|
||||
</a>
|
||||
|
@@ -1,10 +1,4 @@
|
||||
<cnsl-sidenav [indented]="true" [setting]="setting()" (settingChange)="setting.set($event)" [settingsList]="settingsList">
|
||||
<ng-container *ngIf="setting()?.id === 'organizations'">
|
||||
<h2>{{ 'ORG.PAGES.LIST' | translate }}</h2>
|
||||
<p class="org-desc cnsl-secondary-text">{{ 'ORG.PAGES.LISTDESCRIPTION' | translate }}</p>
|
||||
|
||||
<cnsl-org-table></cnsl-org-table>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="setting()?.id === 'features'">
|
||||
<cnsl-features></cnsl-features>
|
||||
</ng-container>
|
||||
@@ -74,12 +68,6 @@
|
||||
<ng-container *ngIf="setting()?.id === 'failedevents' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||
<cnsl-iam-failed-events></cnsl-iam-failed-events>
|
||||
</ng-container>
|
||||
<!-- todo: figure out permissions -->
|
||||
<ng-container *ngIf="setting()?.id === 'actions'">
|
||||
<cnsl-actions-two-actions />
|
||||
</ng-container>
|
||||
<ng-container *ngIf="setting()?.id === 'actions_targets'">
|
||||
<cnsl-actions-two-targets />
|
||||
</ng-container>
|
||||
|
||||
<ng-content></ng-content>
|
||||
</cnsl-sidenav>
|
||||
|
@@ -1,15 +1,6 @@
|
||||
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
|
||||
import { SidenavSetting } from '../sidenav/sidenav.component';
|
||||
|
||||
export const ORGANIZATIONS: SidenavSetting = {
|
||||
id: 'organizations',
|
||||
i18nKey: 'SETTINGS.LIST.ORGS',
|
||||
groupI18nKey: 'SETTINGS.GROUPS.GENERAL',
|
||||
requiredRoles: {
|
||||
[PolicyComponentServiceType.ADMIN]: ['iam.read'],
|
||||
},
|
||||
};
|
||||
|
||||
export const FEATURESETTINGS: SidenavSetting = {
|
||||
id: 'features',
|
||||
i18nKey: 'SETTINGS.LIST.FEATURESETTINGS',
|
||||
@@ -222,23 +213,3 @@ export const BRANDING: SidenavSetting = {
|
||||
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
|
||||
},
|
||||
};
|
||||
|
||||
export const ACTIONS: SidenavSetting = {
|
||||
id: 'actions',
|
||||
i18nKey: 'SETTINGS.LIST.ACTIONS',
|
||||
groupI18nKey: 'SETTINGS.GROUPS.ACTIONS',
|
||||
requiredRoles: {
|
||||
[PolicyComponentServiceType.ADMIN]: ['action.execution.write', 'action.target.write'],
|
||||
},
|
||||
beta: true,
|
||||
};
|
||||
|
||||
export const ACTIONS_TARGETS: SidenavSetting = {
|
||||
id: 'actions_targets',
|
||||
i18nKey: 'SETTINGS.LIST.TARGETS',
|
||||
groupI18nKey: 'SETTINGS.GROUPS.ACTIONS',
|
||||
requiredRoles: {
|
||||
[PolicyComponentServiceType.ADMIN]: ['action.execution.write', 'action.target.write'],
|
||||
},
|
||||
beta: true,
|
||||
};
|
||||
|
@@ -1,107 +1,14 @@
|
||||
<div class="max-width-container">
|
||||
<div class="enlarged-container actions-enlarged-container">
|
||||
<div class="actions-title-row">
|
||||
<h1>{{ 'DESCRIPTIONS.ACTIONS.TITLE' | translate }}</h1>
|
||||
<a mat-icon-button href="https://zitadel.com/docs/concepts/features/actions" rel="noreferrer" target="_blank">
|
||||
<mat-icon class="icon">info_outline</mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
<cnsl-info-section [type]="InfoSectionType.ALERT">
|
||||
{{ 'DESCRIPTIONS.ACTIONS.ACTIONSTWO_NOTE' | translate }}
|
||||
</cnsl-info-section>
|
||||
<p class="desc cnsl-secondary-text">{{ 'DESCRIPTIONS.ACTIONS.DESCRIPTION' | translate }}</p>
|
||||
<div class="enlarged-container">
|
||||
<h1>{{ 'ORG.PAGES.LIST' | translate }}</h1>
|
||||
<p class="org-desc cnsl-secondary-text">{{ 'ORG.PAGES.LISTDESCRIPTION' | translate }}</p>
|
||||
|
||||
<cnsl-info-section class="max-actions" *ngIf="maxActions"
|
||||
>{{ 'FLOWS.ACTIONSMAX' | translate: { value: maxActions } }}
|
||||
</cnsl-info-section>
|
||||
<cnsl-meta-layout>
|
||||
<cnsl-sidenav [(setting)]="currentSetting" [settingsList]="settingsList">
|
||||
<ng-container *ngIf="currentSetting.id === 'actions'"> actions </ng-container>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['org.action.read']">
|
||||
<cnsl-card
|
||||
title="{{ 'DESCRIPTIONS.ACTIONS.SCRIPTS.TITLE' | translate }}"
|
||||
description="{{ 'DESCRIPTIONS.ACTIONS.SCRIPTS.DESCRIPTION' | translate }}"
|
||||
>
|
||||
<cnsl-action-table (changedSelection)="selection = $event"></cnsl-action-table>
|
||||
</cnsl-card>
|
||||
</ng-template>
|
||||
|
||||
<div class="title-section">
|
||||
<h2>{{ 'DESCRIPTIONS.ACTIONS.FLOWS.TITLE' | translate }}</h2>
|
||||
<i class="las la-exchange-alt"></i>
|
||||
</div>
|
||||
|
||||
<p class="desc cnsl-secondary-text">{{ 'DESCRIPTIONS.ACTIONS.FLOWS.DESCRIPTION' | translate }}</p>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['org.flow.read']">
|
||||
<div class="actions-flow">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'FLOWS.FLOWTYPE' | translate }}</cnsl-label>
|
||||
<mat-select [formControl]="typeControl">
|
||||
<mat-option *ngFor="let type of typesForSelection" [value]="type">
|
||||
{{ type.name?.localizedMessage }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</cnsl-form-field>
|
||||
|
||||
<div *ngIf="flow" class="trigger-wrapper">
|
||||
<div class="actions-topbottomline"></div>
|
||||
|
||||
<div class="flow-type">
|
||||
<i class="type-icon las la-dot-circle"></i>
|
||||
<span>{{ flow.type?.name?.localizedMessage }}</span>
|
||||
<button
|
||||
*ngIf="flow.type && (flow.triggerActionsList?.length ?? 0) > 0"
|
||||
matTooltip="{{ 'ACTIONS.CLEAR' | translate }}"
|
||||
mat-icon-button
|
||||
color="warn"
|
||||
(click)="clearFlow(flow.type.id)"
|
||||
>
|
||||
<i class="type-button-icon las la-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<cnsl-card *ngFor="let trigger of flow.triggerActionsList; index as i" class="trigger">
|
||||
<div class="trigger-top">
|
||||
<mat-icon svgIcon="mdi_arrow_right_bottom" class="icon"></mat-icon>
|
||||
<span>{{ trigger.triggerType?.name?.localizedMessage }}</span>
|
||||
<span class="fill-space"></span>
|
||||
<button color="warn" mat-icon-button (click)="removeTriggerActionsList(i)">
|
||||
<i class="las la-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<span class="fill-space"></span>
|
||||
<div class="flow-action-wrapper" cdkDropList (cdkDropListDropped)="drop(i, trigger.actionsList, $event)">
|
||||
<div
|
||||
cdkDrag
|
||||
cdkDragLockAxis="y"
|
||||
cdkDragBoundary=".action-wrapper"
|
||||
class="flow-action"
|
||||
*ngFor="let action of trigger.actionsList"
|
||||
>
|
||||
<i class="las la-code"></i>
|
||||
<span class="flow-action-name">{{ action.name }}</span>
|
||||
<span class="fill-space"></span>
|
||||
<span
|
||||
class="state"
|
||||
[ngClass]="{
|
||||
active: action.state === ActionState.ACTION_STATE_ACTIVE,
|
||||
inactive: action.state === ActionState.ACTION_STATE_INACTIVE,
|
||||
}"
|
||||
>
|
||||
{{ 'FLOWS.STATES.' + action.state | translate }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</cnsl-card>
|
||||
|
||||
<button *ngIf="flow.type" class="add-btn" mat-raised-button color="primary" (click)="openAddTrigger(flow.type)">
|
||||
<div class="cnsl-action-button">
|
||||
<mat-icon>add</mat-icon>
|
||||
<span>{{ 'FLOWS.ADDTRIGGER' | translate }}</span>
|
||||
<span *ngIf="selection && selection.length"> ({{ selection.length }})</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="currentSetting.id === 'targets'"> targets </ng-container>
|
||||
</cnsl-sidenav>
|
||||
</cnsl-meta-layout>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,186 +1,7 @@
|
||||
.actions-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
.icon {
|
||||
font-size: 1.2rem;
|
||||
height: 1.2rem;
|
||||
width: 1.2rem;
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@mixin actions-theme($theme) {
|
||||
$foreground: map-get($theme, foreground);
|
||||
$background: map-get($theme, background);
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
$primary: map-get($theme, primary);
|
||||
$primary-color: map-get($primary, 500);
|
||||
|
||||
.actions-enlarged-container {
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-bottom: 2rem;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
i {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.actions-flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 1000px;
|
||||
position: relative;
|
||||
|
||||
.formfield {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.flow-type {
|
||||
margin: 0.5rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 0 1.5rem;
|
||||
|
||||
.type-icon {
|
||||
color: $primary-color;
|
||||
}
|
||||
|
||||
.type-button-icon,
|
||||
.type-icon,
|
||||
span {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.type-icon,
|
||||
.type-button-icon {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.trigger-wrapper {
|
||||
position: relative;
|
||||
|
||||
.trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
.trigger-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-left: 7px;
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 1rem;
|
||||
color: $primary-color;
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flow-action-wrapper {
|
||||
padding: 0 0.5rem;
|
||||
margin: 0;
|
||||
|
||||
.flow-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
padding: 0.5rem 0;
|
||||
cursor: move;
|
||||
|
||||
.flow-action-name {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
i {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.state {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions-topbottomline {
|
||||
position: absolute;
|
||||
top: 26px;
|
||||
bottom: 1.5rem;
|
||||
left: 35px;
|
||||
width: 2px;
|
||||
z-index: 0;
|
||||
background-color: $primary-color;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cdk-drag-preview {
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0 0.5rem;
|
||||
background-color: $primary-color;
|
||||
box-shadow:
|
||||
0 5px 5px -3px rgba(0, 0, 0, 0.2),
|
||||
0 8px 10px 1px rgba(0, 0, 0, 0.14),
|
||||
0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||
|
||||
i {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.cdk-drag-placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cdk-drag-animating {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
.org-desc {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { ActionsComponent } from './actions.component';
|
||||
import { OrgListComponent } from './actions.component';
|
||||
|
||||
describe('ActionsComponent', () => {
|
||||
let component: ActionsComponent;
|
||||
let fixture: ComponentFixture<ActionsComponent>;
|
||||
describe('OrgListComponent', () => {
|
||||
let component: OrgListComponent;
|
||||
let fixture: ComponentFixture<OrgListComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ActionsComponent],
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [OrgListComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ActionsComponent);
|
||||
fixture = TestBed.createComponent(OrgListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -1,180 +1,28 @@
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { Component, DestroyRef } from '@angular/core';
|
||||
import { UntypedFormControl } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActionKeysType } from 'src/app/modules/action-keys/action-keys.component';
|
||||
import { InfoSectionType } from 'src/app/modules/info-section/info-section.component';
|
||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
import { Action, ActionState, Flow, FlowType, TriggerType } from 'src/app/proto/generated/zitadel/action_pb';
|
||||
import { SetTriggerActionsRequest } from 'src/app/proto/generated/zitadel/management_pb';
|
||||
import { Component } from '@angular/core';
|
||||
import { enterAnimations } from 'src/app/animations';
|
||||
import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component';
|
||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { AddFlowDialogComponent } from './add-flow-dialog/add-flow-dialog.component';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
const ACTIONS: SidenavSetting = { id: 'general', i18nKey: 'MENU.ACTIONS' };
|
||||
const TARGETS: SidenavSetting = { id: 'roles', i18nKey: 'MENU.TARGETS' };
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-actions',
|
||||
templateUrl: './actions.component.html',
|
||||
styleUrls: ['./actions.component.scss'],
|
||||
animations: [enterAnimations],
|
||||
})
|
||||
export class ActionsComponent {
|
||||
protected flow!: Flow.AsObject;
|
||||
public settingsList: SidenavSetting[] = [ACTIONS, TARGETS];
|
||||
public currentSetting = this.settingsList[0];
|
||||
|
||||
protected typeControl: UntypedFormControl = new UntypedFormControl();
|
||||
|
||||
protected typesForSelection: FlowType.AsObject[] = [];
|
||||
|
||||
protected selection: Action.AsObject[] = [];
|
||||
protected InfoSectionType = InfoSectionType;
|
||||
protected ActionKeysType = ActionKeysType;
|
||||
|
||||
protected maxActions: number | null = null;
|
||||
protected ActionState = ActionState;
|
||||
constructor(
|
||||
private mgmtService: ManagementService,
|
||||
breadcrumbService: BreadcrumbService,
|
||||
private dialog: MatDialog,
|
||||
private toast: ToastService,
|
||||
destroyRef: DestroyRef,
|
||||
) {
|
||||
const bread: Breadcrumb = {
|
||||
type: BreadcrumbType.ORG,
|
||||
routerLink: ['/org'],
|
||||
};
|
||||
breadcrumbService.setBreadcrumb([bread]);
|
||||
|
||||
this.getFlowTypes().then();
|
||||
|
||||
this.typeControl.valueChanges.pipe(takeUntilDestroyed(destroyRef)).subscribe((value) => {
|
||||
this.loadFlow((value as FlowType.AsObject).id);
|
||||
});
|
||||
}
|
||||
|
||||
private async getFlowTypes(): Promise<void> {
|
||||
try {
|
||||
let resp = await this.mgmtService.listFlowTypes();
|
||||
this.typesForSelection = resp.resultList;
|
||||
if (!this.flow && resp.resultList[0]) {
|
||||
const type = resp.resultList[0];
|
||||
this.typeControl.setValue(type);
|
||||
}
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
private loadFlow(id: string) {
|
||||
this.mgmtService.getFlow(id).then((flowResponse) => {
|
||||
if (flowResponse.flow) {
|
||||
this.flow = flowResponse.flow;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public clearFlow(id: string): void {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.CLEAR',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
titleKey: 'FLOWS.DIALOG.CLEAR.TITLE',
|
||||
descriptionKey: 'FLOWS.DIALOG.CLEAR.DESCRIPTION',
|
||||
},
|
||||
width: '400px',
|
||||
constructor(breadcrumbService: BreadcrumbService) {
|
||||
const iamBread = new Breadcrumb({
|
||||
type: BreadcrumbType.INSTANCE,
|
||||
name: 'Instance',
|
||||
routerLink: ['/instance'],
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
this.mgmtService
|
||||
.clearFlow(id)
|
||||
.then(() => {
|
||||
this.toast.showInfo('FLOWS.FLOWCLEARED', true);
|
||||
this.loadFlow(id);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected openAddTrigger(flow: FlowType.AsObject, trigger?: TriggerType.AsObject): void {
|
||||
const dialogRef = this.dialog.open(AddFlowDialogComponent, {
|
||||
data: {
|
||||
flowType: flow,
|
||||
actions: this.selection && this.selection.length ? this.selection : [],
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((req: SetTriggerActionsRequest) => {
|
||||
if (req) {
|
||||
this.mgmtService
|
||||
.setTriggerActions(req.getActionIdsList(), req.getFlowType(), req.getTriggerType())
|
||||
.then(() => {
|
||||
this.toast.showInfo('FLOWS.FLOWCHANGED', true);
|
||||
this.loadFlow(flow.id);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
drop(triggerActionsListIndex: number, array: any[], event: CdkDragDrop<Action.AsObject[]>) {
|
||||
moveItemInArray(array, event.previousIndex, event.currentIndex);
|
||||
this.saveFlow(triggerActionsListIndex);
|
||||
}
|
||||
|
||||
saveFlow(index: number) {
|
||||
if (
|
||||
this.flow.type &&
|
||||
this.flow.triggerActionsList &&
|
||||
this.flow.triggerActionsList[index] &&
|
||||
this.flow.triggerActionsList[index]?.triggerType
|
||||
) {
|
||||
this.mgmtService
|
||||
.setTriggerActions(
|
||||
this.flow.triggerActionsList[index].actionsList.map((action) => action.id),
|
||||
this.flow.type.id,
|
||||
this.flow.triggerActionsList[index].triggerType?.id ?? '',
|
||||
)
|
||||
.then(() => {
|
||||
this.toast.showInfo('FLOWS.TOAST.ACTIONSSET', true);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected removeTriggerActionsList(index: number) {
|
||||
if (this.flow.type && this.flow.triggerActionsList && this.flow.triggerActionsList[index]) {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.CLEAR',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
titleKey: 'FLOWS.DIALOG.REMOVEACTIONSLIST.TITLE',
|
||||
descriptionKey: 'FLOWS.DIALOG.REMOVEACTIONSLIST.DESCRIPTION',
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
this.mgmtService
|
||||
.setTriggerActions([], this.flow?.type?.id ?? '', this.flow.triggerActionsList[index].triggerType?.id ?? '')
|
||||
.then(() => {
|
||||
this.toast.showInfo('FLOWS.TOAST.ACTIONSSET', true);
|
||||
this.loadFlow(this.flow?.type?.id ?? '');
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
breadcrumbService.setBreadcrumb([iamBread]);
|
||||
}
|
||||
}
|
||||
|
@@ -1,68 +1,16 @@
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
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 { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { CodemirrorModule } from '@ctrl/ngx-codemirror';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
|
||||
import { ActionKeysModule } from 'src/app/modules/action-keys/action-keys.module';
|
||||
import { CardModule } from 'src/app/modules/card/card.module';
|
||||
import { FormFieldModule } from 'src/app/modules/form-field/form-field.module';
|
||||
import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module';
|
||||
import { InputModule } from 'src/app/modules/input/input.module';
|
||||
import { PaginatorModule } from 'src/app/modules/paginator/paginator.module';
|
||||
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
|
||||
import { TableActionsModule } from 'src/app/modules/table-actions/table-actions.module';
|
||||
import { WarnDialogModule } from 'src/app/modules/warn-dialog/warn-dialog.module';
|
||||
import { DurationToSecondsPipeModule } from 'src/app/pipes/duration-to-seconds-pipe/duration-to-seconds-pipe.module';
|
||||
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
|
||||
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
|
||||
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
|
||||
import { OrgTableModule } from 'src/app/modules/org-table/org-table.module';
|
||||
|
||||
import { ActionTableComponent } from './action-table/action-table.component';
|
||||
import { ActionsRoutingModule } from './actions-routing.module';
|
||||
import { ActionsComponent } from './actions.component';
|
||||
import { AddActionDialogComponent } from './add-action-dialog/add-action-dialog.component';
|
||||
import { AddFlowDialogComponent } from './add-flow-dialog/add-flow-dialog.component';
|
||||
import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module';
|
||||
import { SidenavModule } from 'src/app/modules/sidenav/sidenav.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ActionsComponent, ActionTableComponent, AddActionDialogComponent, AddFlowDialogComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ActionsRoutingModule,
|
||||
TranslateModule,
|
||||
MatDialogModule,
|
||||
RefreshTableModule,
|
||||
MatTableModule,
|
||||
PaginatorModule,
|
||||
MatButtonModule,
|
||||
ReactiveFormsModule,
|
||||
MatIconModule,
|
||||
DurationToSecondsPipeModule,
|
||||
TimestampToDatePipeModule,
|
||||
LocalizedDatePipeModule,
|
||||
HasRoleModule,
|
||||
ActionKeysModule,
|
||||
MatTooltipModule,
|
||||
CardModule,
|
||||
MatCheckboxModule,
|
||||
InputModule,
|
||||
FormFieldModule,
|
||||
MatSelectModule,
|
||||
WarnDialogModule,
|
||||
DragDropModule,
|
||||
InfoSectionModule,
|
||||
HasRolePipeModule,
|
||||
TableActionsModule,
|
||||
CodemirrorModule,
|
||||
],
|
||||
declarations: [ActionsComponent],
|
||||
imports: [CommonModule, ActionsRoutingModule, OrgTableModule, TranslateModule, MetaLayoutModule, SidenavModule],
|
||||
exports: [ActionsComponent],
|
||||
})
|
||||
export default class ActionsModule {}
|
||||
|
@@ -27,8 +27,8 @@ export class HomeComponent {
|
||||
public themeService: ThemeService,
|
||||
) {
|
||||
const bread: Breadcrumb = {
|
||||
type: BreadcrumbType.ORG,
|
||||
routerLink: ['/org'],
|
||||
type: BreadcrumbType.INSTANCE,
|
||||
routerLink: ['/'],
|
||||
};
|
||||
|
||||
breadcrumbService.setBreadcrumb([bread]);
|
||||
|
@@ -33,10 +33,7 @@ import {
|
||||
VIEWS,
|
||||
FAILEDEVENTS,
|
||||
EVENTS,
|
||||
ORGANIZATIONS,
|
||||
FEATURESETTINGS,
|
||||
ACTIONS,
|
||||
ACTIONS_TARGETS,
|
||||
} from 'src/app/modules/settings-list/settings';
|
||||
import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
@@ -58,10 +55,7 @@ export class InstanceComponent {
|
||||
|
||||
protected id: string = '';
|
||||
protected readonly defaultSettingsList: SidenavSetting[] = [
|
||||
ORGANIZATIONS,
|
||||
FEATURESETTINGS,
|
||||
ACTIONS,
|
||||
ACTIONS_TARGETS,
|
||||
// notifications
|
||||
// { showWarn: true, ...NOTIFICATIONS },
|
||||
NOTIFICATIONS,
|
||||
|
17
console/src/app/pages/org-actions/actions-routing.module.ts
Normal file
17
console/src/app/pages/org-actions/actions-routing.module.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { ActionsComponent } from './actions.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ActionsComponent,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ActionsRoutingModule {}
|
107
console/src/app/pages/org-actions/actions.component.html
Normal file
107
console/src/app/pages/org-actions/actions.component.html
Normal file
@@ -0,0 +1,107 @@
|
||||
<div class="max-width-container">
|
||||
<div class="enlarged-container actions-enlarged-container">
|
||||
<div class="actions-title-row">
|
||||
<h1>{{ 'DESCRIPTIONS.ACTIONS.TITLE' | translate }}</h1>
|
||||
<a mat-icon-button href="https://zitadel.com/docs/concepts/features/actions" rel="noreferrer" target="_blank">
|
||||
<mat-icon class="icon">info_outline</mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
<cnsl-info-section [type]="InfoSectionType.ALERT">
|
||||
{{ 'DESCRIPTIONS.ACTIONS.ACTIONSTWO_NOTE' | translate }}
|
||||
</cnsl-info-section>
|
||||
<p class="desc cnsl-secondary-text">{{ 'DESCRIPTIONS.ACTIONS.DESCRIPTION' | translate }}</p>
|
||||
|
||||
<cnsl-info-section class="max-actions" *ngIf="maxActions"
|
||||
>{{ 'FLOWS.ACTIONSMAX' | translate: { value: maxActions } }}
|
||||
</cnsl-info-section>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['org.action.read']">
|
||||
<cnsl-card
|
||||
title="{{ 'DESCRIPTIONS.ACTIONS.SCRIPTS.TITLE' | translate }}"
|
||||
description="{{ 'DESCRIPTIONS.ACTIONS.SCRIPTS.DESCRIPTION' | translate }}"
|
||||
>
|
||||
<cnsl-action-table (changedSelection)="selection = $event"></cnsl-action-table>
|
||||
</cnsl-card>
|
||||
</ng-template>
|
||||
|
||||
<div class="title-section">
|
||||
<h2>{{ 'DESCRIPTIONS.ACTIONS.FLOWS.TITLE' | translate }}</h2>
|
||||
<i class="las la-exchange-alt"></i>
|
||||
</div>
|
||||
|
||||
<p class="desc cnsl-secondary-text">{{ 'DESCRIPTIONS.ACTIONS.FLOWS.DESCRIPTION' | translate }}</p>
|
||||
|
||||
<ng-template cnslHasRole [hasRole]="['org.flow.read']">
|
||||
<div class="actions-flow">
|
||||
<cnsl-form-field class="formfield">
|
||||
<cnsl-label>{{ 'FLOWS.FLOWTYPE' | translate }}</cnsl-label>
|
||||
<mat-select [formControl]="typeControl">
|
||||
<mat-option *ngFor="let type of typesForSelection" [value]="type">
|
||||
{{ type.name?.localizedMessage }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</cnsl-form-field>
|
||||
|
||||
<div *ngIf="flow" class="trigger-wrapper">
|
||||
<div class="actions-topbottomline"></div>
|
||||
|
||||
<div class="flow-type">
|
||||
<i class="type-icon las la-dot-circle"></i>
|
||||
<span>{{ flow.type?.name?.localizedMessage }}</span>
|
||||
<button
|
||||
*ngIf="flow.type && (flow.triggerActionsList?.length ?? 0) > 0"
|
||||
matTooltip="{{ 'ACTIONS.CLEAR' | translate }}"
|
||||
mat-icon-button
|
||||
color="warn"
|
||||
(click)="clearFlow(flow.type.id)"
|
||||
>
|
||||
<i class="type-button-icon las la-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<cnsl-card *ngFor="let trigger of flow.triggerActionsList; index as i" class="trigger">
|
||||
<div class="trigger-top">
|
||||
<mat-icon svgIcon="mdi_arrow_right_bottom" class="icon"></mat-icon>
|
||||
<span>{{ trigger.triggerType?.name?.localizedMessage }}</span>
|
||||
<span class="fill-space"></span>
|
||||
<button color="warn" mat-icon-button (click)="removeTriggerActionsList(i)">
|
||||
<i class="las la-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<span class="fill-space"></span>
|
||||
<div class="flow-action-wrapper" cdkDropList (cdkDropListDropped)="drop(i, trigger.actionsList, $event)">
|
||||
<div
|
||||
cdkDrag
|
||||
cdkDragLockAxis="y"
|
||||
cdkDragBoundary=".action-wrapper"
|
||||
class="flow-action"
|
||||
*ngFor="let action of trigger.actionsList"
|
||||
>
|
||||
<i class="las la-code"></i>
|
||||
<span class="flow-action-name">{{ action.name }}</span>
|
||||
<span class="fill-space"></span>
|
||||
<span
|
||||
class="state"
|
||||
[ngClass]="{
|
||||
active: action.state === ActionState.ACTION_STATE_ACTIVE,
|
||||
inactive: action.state === ActionState.ACTION_STATE_INACTIVE,
|
||||
}"
|
||||
>
|
||||
{{ 'FLOWS.STATES.' + action.state | translate }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</cnsl-card>
|
||||
|
||||
<button *ngIf="flow.type" class="add-btn" mat-raised-button color="primary" (click)="openAddTrigger(flow.type)">
|
||||
<div class="cnsl-action-button">
|
||||
<mat-icon>add</mat-icon>
|
||||
<span>{{ 'FLOWS.ADDTRIGGER' | translate }}</span>
|
||||
<span *ngIf="selection && selection.length"> ({{ selection.length }})</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
186
console/src/app/pages/org-actions/actions.component.scss
Normal file
186
console/src/app/pages/org-actions/actions.component.scss
Normal file
@@ -0,0 +1,186 @@
|
||||
.actions-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
.icon {
|
||||
font-size: 1.2rem;
|
||||
height: 1.2rem;
|
||||
width: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin actions-theme($theme) {
|
||||
$foreground: map-get($theme, foreground);
|
||||
$background: map-get($theme, background);
|
||||
$is-dark-theme: map-get($theme, is-dark);
|
||||
$primary: map-get($theme, primary);
|
||||
$primary-color: map-get($primary, 500);
|
||||
|
||||
.actions-enlarged-container {
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-bottom: 2rem;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
i {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.actions-flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 1000px;
|
||||
position: relative;
|
||||
|
||||
.formfield {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.flow-type {
|
||||
margin: 0.5rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 0 1.5rem;
|
||||
|
||||
.type-icon {
|
||||
color: $primary-color;
|
||||
}
|
||||
|
||||
.type-button-icon,
|
||||
.type-icon,
|
||||
span {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.type-icon,
|
||||
.type-button-icon {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.trigger-wrapper {
|
||||
position: relative;
|
||||
|
||||
.trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
.trigger-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-left: 7px;
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 1rem;
|
||||
color: $primary-color;
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flow-action-wrapper {
|
||||
padding: 0 0.5rem;
|
||||
margin: 0;
|
||||
|
||||
.flow-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
padding: 0.5rem 0;
|
||||
cursor: move;
|
||||
|
||||
.flow-action-name {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
i {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.state {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions-topbottomline {
|
||||
position: absolute;
|
||||
top: 26px;
|
||||
bottom: 1.5rem;
|
||||
left: 35px;
|
||||
width: 2px;
|
||||
z-index: 0;
|
||||
background-color: $primary-color;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cdk-drag-preview {
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0 0.5rem;
|
||||
background-color: $primary-color;
|
||||
box-shadow:
|
||||
0 5px 5px -3px rgba(0, 0, 0, 0.2),
|
||||
0 8px 10px 1px rgba(0, 0, 0, 0.14),
|
||||
0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||
|
||||
i {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.cdk-drag-placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cdk-drag-animating {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
}
|
24
console/src/app/pages/org-actions/actions.component.spec.ts
Normal file
24
console/src/app/pages/org-actions/actions.component.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ActionsComponent } from './actions.component';
|
||||
|
||||
describe('ActionsComponent', () => {
|
||||
let component: ActionsComponent;
|
||||
let fixture: ComponentFixture<ActionsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ActionsComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ActionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
180
console/src/app/pages/org-actions/actions.component.ts
Normal file
180
console/src/app/pages/org-actions/actions.component.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { Component, DestroyRef } from '@angular/core';
|
||||
import { UntypedFormControl } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActionKeysType } from 'src/app/modules/action-keys/action-keys.component';
|
||||
import { InfoSectionType } from 'src/app/modules/info-section/info-section.component';
|
||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
import { Action, ActionState, Flow, FlowType, TriggerType } from 'src/app/proto/generated/zitadel/action_pb';
|
||||
import { SetTriggerActionsRequest } from 'src/app/proto/generated/zitadel/management_pb';
|
||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { AddFlowDialogComponent } from './add-flow-dialog/add-flow-dialog.component';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-actions',
|
||||
templateUrl: './actions.component.html',
|
||||
styleUrls: ['./actions.component.scss'],
|
||||
})
|
||||
export class ActionsComponent {
|
||||
protected flow!: Flow.AsObject;
|
||||
|
||||
protected typeControl: UntypedFormControl = new UntypedFormControl();
|
||||
|
||||
protected typesForSelection: FlowType.AsObject[] = [];
|
||||
|
||||
protected selection: Action.AsObject[] = [];
|
||||
protected InfoSectionType = InfoSectionType;
|
||||
protected ActionKeysType = ActionKeysType;
|
||||
|
||||
protected maxActions: number | null = null;
|
||||
protected ActionState = ActionState;
|
||||
constructor(
|
||||
private mgmtService: ManagementService,
|
||||
breadcrumbService: BreadcrumbService,
|
||||
private dialog: MatDialog,
|
||||
private toast: ToastService,
|
||||
destroyRef: DestroyRef,
|
||||
) {
|
||||
const bread: Breadcrumb = {
|
||||
type: BreadcrumbType.ORG,
|
||||
routerLink: ['/org'],
|
||||
};
|
||||
breadcrumbService.setBreadcrumb([bread]);
|
||||
|
||||
this.getFlowTypes().then();
|
||||
|
||||
this.typeControl.valueChanges.pipe(takeUntilDestroyed(destroyRef)).subscribe((value) => {
|
||||
this.loadFlow((value as FlowType.AsObject).id);
|
||||
});
|
||||
}
|
||||
|
||||
private async getFlowTypes(): Promise<void> {
|
||||
try {
|
||||
let resp = await this.mgmtService.listFlowTypes();
|
||||
this.typesForSelection = resp.resultList;
|
||||
if (!this.flow && resp.resultList[0]) {
|
||||
const type = resp.resultList[0];
|
||||
this.typeControl.setValue(type);
|
||||
}
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
private loadFlow(id: string) {
|
||||
this.mgmtService.getFlow(id).then((flowResponse) => {
|
||||
if (flowResponse.flow) {
|
||||
this.flow = flowResponse.flow;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public clearFlow(id: string): void {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.CLEAR',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
titleKey: 'FLOWS.DIALOG.CLEAR.TITLE',
|
||||
descriptionKey: 'FLOWS.DIALOG.CLEAR.DESCRIPTION',
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
this.mgmtService
|
||||
.clearFlow(id)
|
||||
.then(() => {
|
||||
this.toast.showInfo('FLOWS.FLOWCLEARED', true);
|
||||
this.loadFlow(id);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected openAddTrigger(flow: FlowType.AsObject, trigger?: TriggerType.AsObject): void {
|
||||
const dialogRef = this.dialog.open(AddFlowDialogComponent, {
|
||||
data: {
|
||||
flowType: flow,
|
||||
actions: this.selection && this.selection.length ? this.selection : [],
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((req: SetTriggerActionsRequest) => {
|
||||
if (req) {
|
||||
this.mgmtService
|
||||
.setTriggerActions(req.getActionIdsList(), req.getFlowType(), req.getTriggerType())
|
||||
.then(() => {
|
||||
this.toast.showInfo('FLOWS.FLOWCHANGED', true);
|
||||
this.loadFlow(flow.id);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
drop(triggerActionsListIndex: number, array: any[], event: CdkDragDrop<Action.AsObject[]>) {
|
||||
moveItemInArray(array, event.previousIndex, event.currentIndex);
|
||||
this.saveFlow(triggerActionsListIndex);
|
||||
}
|
||||
|
||||
saveFlow(index: number) {
|
||||
if (
|
||||
this.flow.type &&
|
||||
this.flow.triggerActionsList &&
|
||||
this.flow.triggerActionsList[index] &&
|
||||
this.flow.triggerActionsList[index]?.triggerType
|
||||
) {
|
||||
this.mgmtService
|
||||
.setTriggerActions(
|
||||
this.flow.triggerActionsList[index].actionsList.map((action) => action.id),
|
||||
this.flow.type.id,
|
||||
this.flow.triggerActionsList[index].triggerType?.id ?? '',
|
||||
)
|
||||
.then(() => {
|
||||
this.toast.showInfo('FLOWS.TOAST.ACTIONSSET', true);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected removeTriggerActionsList(index: number) {
|
||||
if (this.flow.type && this.flow.triggerActionsList && this.flow.triggerActionsList[index]) {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.CLEAR',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
titleKey: 'FLOWS.DIALOG.REMOVEACTIONSLIST.TITLE',
|
||||
descriptionKey: 'FLOWS.DIALOG.REMOVEACTIONSLIST.DESCRIPTION',
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
this.mgmtService
|
||||
.setTriggerActions([], this.flow?.type?.id ?? '', this.flow.triggerActionsList[index].triggerType?.id ?? '')
|
||||
.then(() => {
|
||||
this.toast.showInfo('FLOWS.TOAST.ACTIONSSET', true);
|
||||
this.loadFlow(this.flow?.type?.id ?? '');
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
68
console/src/app/pages/org-actions/actions.module.ts
Normal file
68
console/src/app/pages/org-actions/actions.module.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
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 { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { CodemirrorModule } from '@ctrl/ngx-codemirror';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
|
||||
import { ActionKeysModule } from 'src/app/modules/action-keys/action-keys.module';
|
||||
import { CardModule } from 'src/app/modules/card/card.module';
|
||||
import { FormFieldModule } from 'src/app/modules/form-field/form-field.module';
|
||||
import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module';
|
||||
import { InputModule } from 'src/app/modules/input/input.module';
|
||||
import { PaginatorModule } from 'src/app/modules/paginator/paginator.module';
|
||||
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
|
||||
import { TableActionsModule } from 'src/app/modules/table-actions/table-actions.module';
|
||||
import { WarnDialogModule } from 'src/app/modules/warn-dialog/warn-dialog.module';
|
||||
import { DurationToSecondsPipeModule } from 'src/app/pipes/duration-to-seconds-pipe/duration-to-seconds-pipe.module';
|
||||
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
|
||||
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
|
||||
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
|
||||
|
||||
import { ActionTableComponent } from './action-table/action-table.component';
|
||||
import { ActionsRoutingModule } from './actions-routing.module';
|
||||
import { ActionsComponent } from './actions.component';
|
||||
import { AddActionDialogComponent } from './add-action-dialog/add-action-dialog.component';
|
||||
import { AddFlowDialogComponent } from './add-flow-dialog/add-flow-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ActionsComponent, ActionTableComponent, AddActionDialogComponent, AddFlowDialogComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ActionsRoutingModule,
|
||||
TranslateModule,
|
||||
MatDialogModule,
|
||||
RefreshTableModule,
|
||||
MatTableModule,
|
||||
PaginatorModule,
|
||||
MatButtonModule,
|
||||
ReactiveFormsModule,
|
||||
MatIconModule,
|
||||
DurationToSecondsPipeModule,
|
||||
TimestampToDatePipeModule,
|
||||
LocalizedDatePipeModule,
|
||||
HasRoleModule,
|
||||
ActionKeysModule,
|
||||
MatTooltipModule,
|
||||
CardModule,
|
||||
MatCheckboxModule,
|
||||
InputModule,
|
||||
FormFieldModule,
|
||||
MatSelectModule,
|
||||
WarnDialogModule,
|
||||
DragDropModule,
|
||||
InfoSectionModule,
|
||||
HasRolePipeModule,
|
||||
TableActionsModule,
|
||||
CodemirrorModule,
|
||||
],
|
||||
})
|
||||
export default class ActionsModule {}
|
@@ -37,7 +37,7 @@
|
||||
@import 'src/app/pages/users/user-list/user-table/user-table.component';
|
||||
@import 'src/app/pages/users/user-detail/contact/contact.component';
|
||||
@import 'src/app/pages/projects/project-grid/project-grid.component';
|
||||
@import 'src/app/pages/actions/actions.component';
|
||||
@import 'src/app/pages/org-actions/actions.component';
|
||||
@import 'src/app/app.component.scss';
|
||||
@import './styles/color.scss';
|
||||
@import 'src/app/pages/instance/instance.component.scss';
|
||||
|
Reference in New Issue
Block a user