rehaul havigation

This commit is contained in:
Max Peintner
2025-07-17 13:39:01 +02:00
parent caca43a07f
commit d85c813e3c
32 changed files with 716 additions and 588 deletions

View File

@@ -75,7 +75,7 @@ const routes: Routes = [
loadChildren: () => import('./pages/actions/actions.module'), loadChildren: () => import('./pages/actions/actions.module'),
canActivate: [authGuard, roleGuard], canActivate: [authGuard, roleGuard],
data: { data: {
roles: ['org.action.read', 'org.flow.read'], roles: ['iam.read', 'iam.read'],
}, },
}, },
{ {

View File

@@ -5,13 +5,12 @@
*ngIf=" *ngIf="
breadc[breadc.length - 1] && breadc[breadc.length - 1] &&
!breadc[breadc.length - 1].hideNav && !breadc[breadc.length - 1].hideNav &&
breadc[breadc.length - 1].type !== BreadcrumbType.AUTHUSER && breadc[breadc.length - 1].type !== BreadcrumbType.AUTHUSER
breadc[breadc.length - 1].type !== BreadcrumbType.INSTANCE
" "
[ngSwitch]="breadc[0].type" [ngSwitch]="breadc[0].type"
> >
<div class="nav-row" @navrow> <div class="nav-row" @navrow>
<ng-container *ngSwitchCase="BreadcrumbType.ORG"> <ng-container *ngSwitchCase="BreadcrumbType.INSTANCE">
<div class="nav-row-abs" @navrowproject> <div class="nav-row-abs" @navrowproject>
<a <a
class="nav-item" class="nav-item"
@@ -22,6 +21,48 @@
<span class="label">{{ 'MENU.DASHBOARD' | translate }}</span> <span class="label">{{ 'MENU.DASHBOARD' | translate }}</span>
</a> </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-container class="org-list" *ngIf="org">
<ng-template cnslHasRole [hasRole]="['org.read']"> <ng-template cnslHasRole [hasRole]="['org.read']">
<a <a

View File

@@ -23,7 +23,8 @@
.nav-row { .nav-row {
position: relative; position: relative;
width: 100%; width: 100%;
height: 36px; height: 40px; // Increased height to accommodate the line
overflow: visible; // Allow the indicator line to show below
.nav-row-abs { .nav-row-abs {
padding: 0 2rem; padding: 0 2rem;
@@ -32,7 +33,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: visible; // Allow the indicator line to show
align-self: stretch; align-self: stretch;
position: absolute; position: absolute;
top: 0; top: 0;
@@ -51,19 +52,46 @@
.nav-item { .nav-item {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 14px;
line-height: 14px;
padding: 0.4rem 12px;
color: map-get($foreground, text);
transition: all 0.2s ease; transition: all 0.2s ease;
text-decoration: none; text-decoration: none;
border-radius: 50vw; border-radius: 50vw;
font-weight: 500; font-weight: 500;
margin: 0.25rem 2px; margin: 0.25rem 2px;
font-size: 14px;
white-space: nowrap; white-space: nowrap;
position: relative; position: relative;
height: 36px;
box-sizing: border-box; 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 { .c_label {
display: flex; display: flex;
@@ -97,14 +125,14 @@
} }
} }
&:hover {
background: if($is-dark-theme, #ffffff40, #00000010);
}
&.active { &.active {
background-color: $primary-color; opacity: 1;
color: map-get($primary, default-contrast); color: map-get($primary, default-contrast);
&::after {
width: 100%; // Full width for testing
}
.c_label { .c_label {
.count { .count {
display: inline-block; display: inline-block;

View File

@@ -1,7 +1,7 @@
<div class="new-header-wrapper"> <div class="new-header-wrapper">
<ng-container *ngIf="myInstanceQuery.data()?.instance as instance"> <ng-container *ngIf="myInstanceQuery.data()?.instance as instance">
<ng-container *ngTemplateOutlet="slash"></ng-container> <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 }} {{ instance.name }}
</a> </a>
<cnsl-header-button <cnsl-header-button
@@ -37,7 +37,7 @@
class="new-header-breadcrumb" class="new-header-breadcrumb"
matRipple matRipple
[matRippleUnbounded]="false" [matRippleUnbounded]="false"
[routerLink]="['/']" [routerLink]="['/org']"
> >
{{ org.name }} {{ org.name }}
</a> </a>

View File

@@ -1,10 +1,4 @@
<cnsl-sidenav [indented]="true" [setting]="setting()" (settingChange)="setting.set($event)" [settingsList]="settingsList"> <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'"> <ng-container *ngIf="setting()?.id === 'features'">
<cnsl-features></cnsl-features> <cnsl-features></cnsl-features>
</ng-container> </ng-container>
@@ -74,12 +68,6 @@
<ng-container *ngIf="setting()?.id === 'failedevents' && serviceType === PolicyComponentServiceType.ADMIN"> <ng-container *ngIf="setting()?.id === 'failedevents' && serviceType === PolicyComponentServiceType.ADMIN">
<cnsl-iam-failed-events></cnsl-iam-failed-events> <cnsl-iam-failed-events></cnsl-iam-failed-events>
</ng-container> </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> <ng-content></ng-content>
</cnsl-sidenav> </cnsl-sidenav>

View File

@@ -1,15 +1,6 @@
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum'; import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
import { SidenavSetting } from '../sidenav/sidenav.component'; 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 = { export const FEATURESETTINGS: SidenavSetting = {
id: 'features', id: 'features',
i18nKey: 'SETTINGS.LIST.FEATURESETTINGS', i18nKey: 'SETTINGS.LIST.FEATURESETTINGS',
@@ -222,23 +213,3 @@ export const BRANDING: SidenavSetting = {
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'], [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,
};

View File

@@ -1,107 +1,14 @@
<div class="max-width-container"> <div class="max-width-container">
<div class="enlarged-container actions-enlarged-container"> <div class="enlarged-container">
<div class="actions-title-row"> <h1>{{ 'ORG.PAGES.LIST' | translate }}</h1>
<h1>{{ 'DESCRIPTIONS.ACTIONS.TITLE' | translate }}</h1> <p class="org-desc cnsl-secondary-text">{{ 'ORG.PAGES.LISTDESCRIPTION' | translate }}</p>
<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" <cnsl-meta-layout>
>{{ 'FLOWS.ACTIONSMAX' | translate: { value: maxActions } }} <cnsl-sidenav [(setting)]="currentSetting" [settingsList]="settingsList">
</cnsl-info-section> <ng-container *ngIf="currentSetting.id === 'actions'"> actions </ng-container>
<ng-template cnslHasRole [hasRole]="['org.action.read']"> <ng-container *ngIf="currentSetting.id === 'targets'"> targets </ng-container>
<cnsl-card </cnsl-sidenav>
title="{{ 'DESCRIPTIONS.ACTIONS.SCRIPTS.TITLE' | translate }}" </cnsl-meta-layout>
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">&nbsp;({{ selection.length }})</span>
</div>
</button>
</div>
</div>
</ng-template>
</div> </div>
</div> </div>

View File

@@ -1,186 +1,7 @@
.actions-title-row { h1 {
display: flex; margin: 0;
align-items: center;
h1 {
margin: 0;
}
a {
.icon {
font-size: 1.2rem;
height: 1.2rem;
width: 1.2rem;
}
}
} }
@mixin actions-theme($theme) { .org-desc {
$foreground: map-get($theme, foreground); font-size: 14px;
$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);
}
}
} }

View File

@@ -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', () => { describe('OrgListComponent', () => {
let component: ActionsComponent; let component: OrgListComponent;
let fixture: ComponentFixture<ActionsComponent>; let fixture: ComponentFixture<OrgListComponent>;
beforeEach(async () => { beforeEach(waitForAsync(() => {
await TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ActionsComponent], declarations: [OrgListComponent],
}).compileComponents(); }).compileComponents();
}); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ActionsComponent); fixture = TestBed.createComponent(OrgListComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -1,180 +1,28 @@
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { Component } from '@angular/core';
import { Component, DestroyRef } from '@angular/core'; import { enterAnimations } from 'src/app/animations';
import { UntypedFormControl } from '@angular/forms'; import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component';
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 { 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'; const ACTIONS: SidenavSetting = { id: 'general', i18nKey: 'MENU.ACTIONS' };
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; const TARGETS: SidenavSetting = { id: 'roles', i18nKey: 'MENU.TARGETS' };
@Component({ @Component({
selector: 'cnsl-actions', selector: 'cnsl-actions',
templateUrl: './actions.component.html', templateUrl: './actions.component.html',
styleUrls: ['./actions.component.scss'], styleUrls: ['./actions.component.scss'],
animations: [enterAnimations],
}) })
export class ActionsComponent { export class ActionsComponent {
protected flow!: Flow.AsObject; public settingsList: SidenavSetting[] = [ACTIONS, TARGETS];
public currentSetting = this.settingsList[0];
protected typeControl: UntypedFormControl = new UntypedFormControl(); constructor(breadcrumbService: BreadcrumbService) {
const iamBread = new Breadcrumb({
protected typesForSelection: FlowType.AsObject[] = []; type: BreadcrumbType.INSTANCE,
name: 'Instance',
protected selection: Action.AsObject[] = []; routerLink: ['/instance'],
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) => { breadcrumbService.setBreadcrumb([iamBread]);
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);
});
}
});
}
} }
} }

View File

@@ -1,68 +1,16 @@
import { DragDropModule } from '@angular/cdk/drag-drop';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; 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 { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { OrgTableModule } from 'src/app/modules/org-table/org-table.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 { ActionsRoutingModule } from './actions-routing.module';
import { ActionsComponent } from './actions.component'; import { ActionsComponent } from './actions.component';
import { AddActionDialogComponent } from './add-action-dialog/add-action-dialog.component'; import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module';
import { AddFlowDialogComponent } from './add-flow-dialog/add-flow-dialog.component'; import { SidenavModule } from 'src/app/modules/sidenav/sidenav.module';
@NgModule({ @NgModule({
declarations: [ActionsComponent, ActionTableComponent, AddActionDialogComponent, AddFlowDialogComponent], declarations: [ActionsComponent],
imports: [ imports: [CommonModule, ActionsRoutingModule, OrgTableModule, TranslateModule, MetaLayoutModule, SidenavModule],
CommonModule, exports: [ActionsComponent],
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 {} export default class ActionsModule {}

View File

@@ -27,8 +27,8 @@ export class HomeComponent {
public themeService: ThemeService, public themeService: ThemeService,
) { ) {
const bread: Breadcrumb = { const bread: Breadcrumb = {
type: BreadcrumbType.ORG, type: BreadcrumbType.INSTANCE,
routerLink: ['/org'], routerLink: ['/'],
}; };
breadcrumbService.setBreadcrumb([bread]); breadcrumbService.setBreadcrumb([bread]);

View File

@@ -33,10 +33,7 @@ import {
VIEWS, VIEWS,
FAILEDEVENTS, FAILEDEVENTS,
EVENTS, EVENTS,
ORGANIZATIONS,
FEATURESETTINGS, FEATURESETTINGS,
ACTIONS,
ACTIONS_TARGETS,
} from 'src/app/modules/settings-list/settings'; } from 'src/app/modules/settings-list/settings';
import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component'; import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
@@ -58,10 +55,7 @@ export class InstanceComponent {
protected id: string = ''; protected id: string = '';
protected readonly defaultSettingsList: SidenavSetting[] = [ protected readonly defaultSettingsList: SidenavSetting[] = [
ORGANIZATIONS,
FEATURESETTINGS, FEATURESETTINGS,
ACTIONS,
ACTIONS_TARGETS,
// notifications // notifications
// { showWarn: true, ...NOTIFICATIONS }, // { showWarn: true, ...NOTIFICATIONS },
NOTIFICATIONS, NOTIFICATIONS,

View 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 {}

View 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">&nbsp;({{ selection.length }})</span>
</div>
</button>
</div>
</div>
</ng-template>
</div>
</div>

View 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);
}
}
}

View 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();
});
});

View 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);
});
}
});
}
}
}

View 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 {}

View File

@@ -37,7 +37,7 @@
@import 'src/app/pages/users/user-list/user-table/user-table.component'; @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/users/user-detail/contact/contact.component';
@import 'src/app/pages/projects/project-grid/project-grid.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 'src/app/app.component.scss';
@import './styles/color.scss'; @import './styles/color.scss';
@import 'src/app/pages/instance/instance.component.scss'; @import 'src/app/pages/instance/instance.component.scss';