Merge pull request 'feature/2-secure-configurations' (#2) from feature/2-secure-configurations into develop
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
Reviewed-on: #2
This commit is contained in:
commit
18415059c6
|
@ -0,0 +1,6 @@
|
||||||
|
**/node_modules
|
||||||
|
.gitignore
|
||||||
|
.dockerignore
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
package-lock.json
|
|
@ -0,0 +1,77 @@
|
||||||
|
---
|
||||||
|
global-variables:
|
||||||
|
release: &release ${DRONE_BRANCH##/**}
|
||||||
|
environment: &environment
|
||||||
|
CRE_REGISTRY_IMAGE: registry.fyloz.dev:5443/colorrecipesexplorer/frontend
|
||||||
|
CRE_PORT: 9102
|
||||||
|
CRE_RELEASE: *release
|
||||||
|
alpine-image: &alpine-image alpine:latest
|
||||||
|
docker-registry-repo: &docker-registry-repo registry.fyloz.dev:5443/colorrecipesexplorer/frontend
|
||||||
|
|
||||||
|
kind: pipeline
|
||||||
|
name: default
|
||||||
|
type: docker
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: set-docker-tags-latest
|
||||||
|
image: *alpine-image
|
||||||
|
environment:
|
||||||
|
<<: *environment
|
||||||
|
commands:
|
||||||
|
- echo -n "latest" > .tags
|
||||||
|
when:
|
||||||
|
branch: develop
|
||||||
|
|
||||||
|
- name: set-docker-tags-release
|
||||||
|
image: *alpine-image
|
||||||
|
environment:
|
||||||
|
<<: *environment
|
||||||
|
commands:
|
||||||
|
- echo -n "latest-release,$CRE_RELEASE" > .tags
|
||||||
|
when:
|
||||||
|
branch: release/**
|
||||||
|
|
||||||
|
- name: containerize-dev
|
||||||
|
image: plugins/docker
|
||||||
|
environment:
|
||||||
|
<<: *environment
|
||||||
|
settings:
|
||||||
|
repo: *docker-registry-repo
|
||||||
|
when:
|
||||||
|
branch:
|
||||||
|
- develop
|
||||||
|
- release/**
|
||||||
|
|
||||||
|
- name: deploy
|
||||||
|
image: alpine:latest
|
||||||
|
environment:
|
||||||
|
<<: *environment
|
||||||
|
CRE_REGISTRY_IMAGE: *docker-registry-repo
|
||||||
|
DEPLOY_SERVER:
|
||||||
|
from_secret: deploy_server
|
||||||
|
DEPLOY_SERVER_USERNAME:
|
||||||
|
from_secret: deploy_server_username
|
||||||
|
DEPLOY_SERVER_SSH_PORT:
|
||||||
|
from_secret: deploy_server_ssh_port
|
||||||
|
DEPLOY_SERVER_SSH_KEY:
|
||||||
|
from_secret: deploy_server_ssh_key
|
||||||
|
DEPLOY_CONTAINER_NAME: cre_frontend-${DRONE_BRANCH}
|
||||||
|
commands:
|
||||||
|
- apk update
|
||||||
|
- apk add --no-cache openssh-client
|
||||||
|
- mkdir -p ~/.ssh
|
||||||
|
- echo "$DEPLOY_SERVER_SSH_KEY" | tr -d '\r' > ~/.ssh/id_rsa
|
||||||
|
- chmod 700 ~/.ssh/id_rsa
|
||||||
|
- eval $(ssh-agent -s)
|
||||||
|
- ssh-add ~/.ssh/id_rsa
|
||||||
|
- ssh-keyscan -p $DEPLOY_SERVER_SSH_PORT -H $DEPLOY_SERVER >> ~/.ssh/known_hosts
|
||||||
|
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
|
||||||
|
- ssh -p $DEPLOY_SERVER_SSH_PORT $DEPLOY_SERVER_USERNAME@$DEPLOY_SERVER "docker stop $DEPLOY_CONTAINER_NAME || true && docker rm $DEPLOY_CONTAINER_NAME || true"
|
||||||
|
- ssh -p $DEPLOY_SERVER_SSH_PORT $DEPLOY_SERVER_USERNAME@$DEPLOY_SERVER "docker pull $CRE_REGISTRY_IMAGE:$CRE_RELEASE"
|
||||||
|
- ssh -p $DEPLOY_SERVER_SSH_PORT $DEPLOY_SERVER_USERNAME@$DEPLOY_SERVER "docker run -d -p $CRE_PORT:80 --name=$DEPLOY_CONTAINER_NAME $CRE_REGISTRY_IMAGE:$CRE_RELEASE"
|
||||||
|
when:
|
||||||
|
branch: release/**
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- develop
|
||||||
|
- master
|
|
@ -1,72 +0,0 @@
|
||||||
variables:
|
|
||||||
CI_REGISTRY_IMAGE_NG: "$CI_REGISTRY_IMAGE:latest-ng"
|
|
||||||
CI_REGISTRY_IMAGE_FRONTEND: "$CI_REGISTRY_IMAGE:latest"
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- docker info
|
|
||||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- build
|
|
||||||
- package
|
|
||||||
- deploy
|
|
||||||
|
|
||||||
.only-master:
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
|
|
||||||
build:
|
|
||||||
stage: build
|
|
||||||
extends: .only-master
|
|
||||||
script:
|
|
||||||
- docker pull $CI_REGISTRY_IMAGE_NG || true
|
|
||||||
- docker build --cache-from $CI_REGISTRY_IMAGE_NG -f ng.Dockerfile -t $CI_REGISTRY_IMAGE_NG .
|
|
||||||
- docker push $CI_REGISTRY_IMAGE_NG
|
|
||||||
|
|
||||||
package:
|
|
||||||
stage: package
|
|
||||||
needs: ['build']
|
|
||||||
extends: .only-master
|
|
||||||
variables:
|
|
||||||
PACKAGE_CONTAINER_NAME: "cre_frontend_package"
|
|
||||||
ARTIFACT_NAME: "ColorRecipesExplorer-frontend-$CI_PIPELINE_IID"
|
|
||||||
script:
|
|
||||||
- apk update
|
|
||||||
- apk add --no-cache zip
|
|
||||||
- mkdir dist
|
|
||||||
- docker run --name $PACKAGE_CONTAINER_NAME $CI_REGISTRY_IMAGE_NG ng build --configuration=$ANGULAR_CONFIGURATION --output-hashing=none --stats-json --source-map=false
|
|
||||||
- docker cp $PACKAGE_CONTAINER_NAME:/usr/src/cre/dist/color-recipes-explorer-frontend/ dist/
|
|
||||||
- zip -r $ARTIFACT_NAME.zip dist/
|
|
||||||
- docker build -t $CI_REGISTRY_IMAGE_FRONTEND --build-arg ARTIFACT_NAME=$ARTIFACT_NAME .
|
|
||||||
- docker push $CI_REGISTRY_IMAGE_FRONTEND
|
|
||||||
after_script:
|
|
||||||
- docker stop $PACKAGE_CONTAINER_NAME || true
|
|
||||||
- docker rm $PACKAGE_CONTAINER_NAME || true
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- $ARTIFACT_NAME.zip
|
|
||||||
expire_in: 1 week
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
stage: deploy
|
|
||||||
image: alpine:latest
|
|
||||||
needs: ['package']
|
|
||||||
extends: .only-master
|
|
||||||
variables:
|
|
||||||
DEPLOYED_CONTAINER_NAME: "cre_frontend"
|
|
||||||
before_script:
|
|
||||||
- apk update
|
|
||||||
- apk add --no-cache openssh-client
|
|
||||||
- mkdir -p ~/.ssh
|
|
||||||
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
|
|
||||||
- chmod 700 ~/.ssh/id_rsa
|
|
||||||
- eval $(ssh-agent -s)
|
|
||||||
- ssh-add ~/.ssh/id_rsa
|
|
||||||
- ssh-keyscan -p $DEPLOYMENT_SERVER_SSH_PORT -H $DEPLOYMENT_SERVER >> ~/.ssh/known_hosts
|
|
||||||
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
|
|
||||||
script:
|
|
||||||
- ssh -p $DEPLOYMENT_SERVER_SSH_PORT $DEPLOYMENT_SERVER_USERNAME@$DEPLOYMENT_SERVER "docker stop $DEPLOYED_CONTAINER_NAME || true && docker rm $DEPLOYED_CONTAINER_NAME || true"
|
|
||||||
- ssh -p $DEPLOYMENT_SERVER_SSH_PORT $DEPLOYMENT_SERVER_USERNAME@$DEPLOYMENT_SERVER "docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY && docker pull $CI_REGISTRY_IMAGE_FRONTEND"
|
|
||||||
- ssh -p $DEPLOYMENT_SERVER_SSH_PORT $DEPLOYMENT_SERVER_USERNAME@$DEPLOYMENT_SERVER "docker run -d -p $PORT:80 --name=$DEPLOYED_CONTAINER_NAME $CI_REGISTRY_IMAGE_FRONTEND"
|
|
||||||
|
|
||||||
|
|
34
Dockerfile
34
Dockerfile
|
@ -1,17 +1,29 @@
|
||||||
FROM nginx:mainline-alpine
|
FROM alpine:latest AS build
|
||||||
|
WORKDIR /usr/src/
|
||||||
WORKDIR /usr/bin/cre/
|
|
||||||
|
|
||||||
ARG ARTIFACT_NAME=ColorRecipesExplorer-ng
|
|
||||||
COPY $ARTIFACT_NAME.zip .
|
|
||||||
COPY nginx.conf /etc/nginx/nginx.conf
|
|
||||||
|
|
||||||
RUN apk update
|
RUN apk update
|
||||||
RUN apk add --no-cache zip
|
RUN apk add --no-cache nodejs npm
|
||||||
|
|
||||||
RUN unzip $ARTIFACT_NAME.zip
|
RUN npm install -g typescript@4.0.7 && \
|
||||||
RUN rm $ARTIFACT_NAME.zip
|
npm install -g @angular/cli@11.2.9 || true --fo
|
||||||
|
|
||||||
EXPOSE 80
|
ENV NG_CLI_ANALYTICS=ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ARG ANGULAR_CONFIGURATION=production
|
||||||
|
|
||||||
|
RUN npm install --force
|
||||||
|
RUN ng build --configuration=$ANGULAR_CONFIGURATION --stats-json --source-map=false
|
||||||
|
|
||||||
|
FROM nginx:mainline-alpine
|
||||||
|
|
||||||
|
WORKDIR /usr/bin/
|
||||||
|
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
COPY --from=build /usr/src/dist/color-recipes-explorer-frontend/ .
|
||||||
|
|
||||||
|
ARG CRE_PORT=80
|
||||||
|
EXPOSE $CRE_PORT
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|
|
@ -8,18 +8,18 @@ services:
|
||||||
MYSQL_ROOT_PASSWORD: "pass"
|
MYSQL_ROOT_PASSWORD: "pass"
|
||||||
MYSQL_DATABASE: "cre"
|
MYSQL_DATABASE: "cre"
|
||||||
ports:
|
ports:
|
||||||
- 3306:3306
|
- "3306:3306"
|
||||||
backend:
|
backend:
|
||||||
image: fyloz.dev:5443/color-recipes-explorer/backend:master
|
image: registry.fyloz.dev:5443/colorrecipesexplorer/backend:latest
|
||||||
environment:
|
environment:
|
||||||
spring_profiles_active: "mysql,debug"
|
spring_profiles_active: "mysql,debug"
|
||||||
cre_database_url: "mysql://database:3306/cre"
|
cre_database_url: "mysql://database:3306/cre"
|
||||||
cre_database_username: "root"
|
cre_database_username: "root"
|
||||||
cre_database_password: "pass"
|
cre_database_password: "pass"
|
||||||
CRE_ENABLE_DB_UPDATE: 0
|
CRE_ENABLE_DB_UPDATE: 1
|
||||||
server_port: 9090
|
server_port: 9090
|
||||||
ports:
|
ports:
|
||||||
- 9090:9090
|
- "9090:9090"
|
||||||
volumes:
|
volumes:
|
||||||
- cre_data:/usr/bin/cre/data
|
- cre_data:/usr/bin/cre/data
|
||||||
- cre_config:/usr/bin/cre/config
|
- cre_config:/usr/bin/cre/config
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
FROM alpine:latest
|
|
||||||
WORKDIR /usr/src/cre/
|
|
||||||
|
|
||||||
RUN apk update
|
|
||||||
RUN apk add --no-cache nodejs
|
|
||||||
RUN apk add --no-cache npm
|
|
||||||
|
|
||||||
RUN npm install -g typescript@4.0.7
|
|
||||||
RUN npm install -g @angular/cli@11.2.9 || true
|
|
||||||
|
|
||||||
ENV NG_CLI_ANALYTICS=ci
|
|
||||||
|
|
||||||
COPY package.json .
|
|
||||||
|
|
||||||
RUN npm install --force
|
|
||||||
|
|
||||||
COPY . .
|
|
|
@ -5,7 +5,7 @@ events { worker_connections 1024; }
|
||||||
http {
|
http {
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
root /usr/bin/cre/dist/color-recipes-explorer-frontend;
|
root /usr/bin/;
|
||||||
include /etc/nginx/mime.types;
|
include /etc/nginx/mime.types;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
@import "assets/sass/modules/fonts";
|
||||||
|
@import "custom-theme";
|
||||||
|
@import "~material-design-icons/iconfont/material-icons.css";
|
||||||
|
|
||||||
|
// Spacing
|
||||||
|
$spacer: 1rem;
|
||||||
|
$spacers: (
|
||||||
|
1: $spacer * 0.5,
|
||||||
|
2: $spacer * 0.75,
|
||||||
|
3: $spacer,
|
||||||
|
4: $spacer * 1.5,
|
||||||
|
5: $spacer * 2
|
||||||
|
);
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
$color-primary: map-get($theme-primary, 500);
|
||||||
|
$text-color-primary: white;
|
||||||
|
|
||||||
|
$color-accent: map-get($theme-accent, 500);
|
||||||
|
$color-warn: map-get($theme-error, 500);
|
|
@ -3,8 +3,7 @@ import {Routes, RouterModule} from '@angular/router'
|
||||||
import {CatalogComponent} from './pages/catalog/catalog.component'
|
import {CatalogComponent} from './pages/catalog/catalog.component'
|
||||||
import {AdministrationComponent} from './pages/administration/administration.component'
|
import {AdministrationComponent} from './pages/administration/administration.component'
|
||||||
import {MiscComponent} from './pages/others/misc.component'
|
import {MiscComponent} from './pages/others/misc.component'
|
||||||
import {CreConfigEditor} from './modules/configuration/config'
|
import {CreConfigEditor} from './modules/configuration/config-editor'
|
||||||
|
|
||||||
|
|
||||||
const routes: Routes = [{
|
const routes: Routes = [{
|
||||||
path: 'color',
|
path: 'color',
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@import '../../../../../custom-theme'
|
@import "~src/variables"
|
||||||
|
|
||||||
mat-expansion-panel
|
mat-expansion-panel
|
||||||
width: 48rem
|
width: 48rem
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
<div *ngIf="configuration" [attr.title]="tooltip?.content" class="d-flex flex-row justify-content-between align-items-center">
|
|
||||||
<cre-checkbox-input [label]="label.content" [control]="config.control"></cre-checkbox-input>
|
|
||||||
<mat-hint>{{lastUpdated}}</mat-hint>
|
|
||||||
</div>
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<cre-config-container [configuration]="config" [tooltip]="tooltip">
|
||||||
|
<div class="d-flex flex-row justify-content-between align-items-center">
|
||||||
|
<cre-checkbox-input [label]="label" [control]="control"></cre-checkbox-input>
|
||||||
|
<mat-hint>{{inputHint}}</mat-hint>
|
||||||
|
</div>
|
||||||
|
</cre-config-container>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<div
|
||||||
|
class="cre-config"
|
||||||
|
[class.cre-readonly-config]="readOnly"
|
||||||
|
[attr.title]="tooltip">
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<cre-config-container [configuration]="config" [tooltip]="tooltip">
|
||||||
|
<cre-input
|
||||||
|
class="w-100"
|
||||||
|
type="text"
|
||||||
|
[label]="label"
|
||||||
|
[hint]="inputHint"
|
||||||
|
[control]="control"
|
||||||
|
[icon]="inputIcon"
|
||||||
|
[iconTitle]="inputIconTitle"
|
||||||
|
iconColor="warning">
|
||||||
|
</cre-input>
|
||||||
|
</cre-config-container>
|
|
@ -0,0 +1,120 @@
|
||||||
|
<form *ngIf="form" [formGroup]="form">
|
||||||
|
<cre-action-bar>
|
||||||
|
<cre-action-group>
|
||||||
|
<cre-primary-button routerLink="/color">Retour</cre-primary-button>
|
||||||
|
</cre-action-group>
|
||||||
|
<cre-action-group>
|
||||||
|
<cre-accent-button [disabled]="!(form.dirty && form.valid)" (click)="onSubmit()">Enregistrer</cre-accent-button>
|
||||||
|
</cre-action-group>
|
||||||
|
</cre-action-bar>
|
||||||
|
|
||||||
|
<div class="d-flex flex-column" style="gap: 1.5rem">
|
||||||
|
<cre-config-section *ngIf="!emergencyMode" label="Apparence">
|
||||||
|
<cre-config-list>
|
||||||
|
<cre-image-config
|
||||||
|
label="Logo"
|
||||||
|
tooltip="Affiché dans la bannière de l'application web. Il peut être nécessaire de forcer le
|
||||||
|
rafraîchissement du cache du navigateur pour que ce changement prenne effet (généralement avec les touches
|
||||||
|
'Ctrl+F5')."
|
||||||
|
[configControl]="getConfigControl(keys.INSTANCE_LOGO_PATH)" previewWidth="170px"
|
||||||
|
(invalidFormat)="invalidFormatConfirmBox.show()">
|
||||||
|
</cre-image-config>
|
||||||
|
|
||||||
|
<cre-image-config
|
||||||
|
label="Icône"
|
||||||
|
tooltip="Affiché dans l'onglet de la page dans le navigateur. Il peut être nécessaire de forcer le
|
||||||
|
rafraîchissement du cache du navigateur pour que ce changement prenne effet (généralement avec les touches
|
||||||
|
'Ctrl+F5')."
|
||||||
|
[configControl]="getConfigControl(keys.INSTANCE_ICON_PATH)" previewWidth="32px"
|
||||||
|
(invalidFormat)="invalidFormatConfirmBox.show()">
|
||||||
|
</cre-image-config>
|
||||||
|
</cre-config-list>
|
||||||
|
</cre-config-section>
|
||||||
|
|
||||||
|
<cre-config-section *ngIf="!emergencyMode" label="Données">
|
||||||
|
<cre-config-list class="pt-2">
|
||||||
|
<cre-period-config
|
||||||
|
label="Période d'expiration de l'approbation de l'échantillon des recettes"
|
||||||
|
[configControl]="getConfigControl(keys.RECIPE_APPROBATION_EXPIRATION)">
|
||||||
|
</cre-period-config>
|
||||||
|
|
||||||
|
<cre-period-config
|
||||||
|
label="Période d'expiration des kits de retouches complets"
|
||||||
|
tooltip="Les kits de retouche complétés expirent après la période configurée. Les kits de retouche expirés seront
|
||||||
|
supprimés automatiquement."
|
||||||
|
[configControl]="getConfigControl(keys.TOUCH_UP_KIT_EXPIRATION)">
|
||||||
|
</cre-period-config>
|
||||||
|
|
||||||
|
<cre-bool-config
|
||||||
|
label="Activer le cache des PDFs générés"
|
||||||
|
tooltip="Cette option permet de stocker les PDFs générés sur le disque, ce qui permet d'accélérer
|
||||||
|
l'accès aux PDFs si la lecture des fichiers cachés sur le disque est plus rapide que la génération d'un
|
||||||
|
nouveau PDF."
|
||||||
|
[configControl]="getConfigControl(keys.TOUCH_UP_KIT_CACHE_PDF)">
|
||||||
|
</cre-bool-config>
|
||||||
|
</cre-config-list>
|
||||||
|
</cre-config-section>
|
||||||
|
|
||||||
|
<cre-config-section label="Système">
|
||||||
|
<cre-config-list>
|
||||||
|
<cre-text-config
|
||||||
|
*ngIf="!emergencyMode"
|
||||||
|
label="URL de l'instance"
|
||||||
|
tooltip="Utilisé pour générer l'URL de certaines ressources, comme les images et les fiches signalitiques."
|
||||||
|
[configControl]="getConfigControl(keys.INSTANCE_URL)">
|
||||||
|
</cre-text-config>
|
||||||
|
|
||||||
|
<cre-text-config
|
||||||
|
label="URL de la base de données"
|
||||||
|
[configControl]="getConfigControl(keys.DATABASE_URL)">
|
||||||
|
</cre-text-config>
|
||||||
|
|
||||||
|
<cre-text-config
|
||||||
|
label="Utilisateur de la base de données"
|
||||||
|
[configControl]="getConfigControl(keys.DATABASE_USER)">
|
||||||
|
</cre-text-config>
|
||||||
|
|
||||||
|
<cre-secure-config
|
||||||
|
label="Mot de passe de la base de données"
|
||||||
|
buttonLabel="Modifier le mot de passe de la base de données"
|
||||||
|
[configControl]="getConfigControl(keys.DATABASE_PASSWORD)">
|
||||||
|
</cre-secure-config>
|
||||||
|
|
||||||
|
<cre-text-config
|
||||||
|
label="Version de la base de données"
|
||||||
|
[configControl]="getConfigControl(keys.DATABASE_VERSION)">
|
||||||
|
</cre-text-config>
|
||||||
|
|
||||||
|
<cre-text-config
|
||||||
|
label="Version de Color Recipes Explorer"
|
||||||
|
[configControl]="getConfigControl(keys.BACKEND_BUILD_VERSION)">
|
||||||
|
</cre-text-config>
|
||||||
|
|
||||||
|
<cre-date-config
|
||||||
|
label="Date de compilation de Color Recipes Explorer"
|
||||||
|
[configControl]="getConfigControl(keys.BACKEND_BUILD_TIME)">
|
||||||
|
</cre-date-config>
|
||||||
|
|
||||||
|
<cre-text-config
|
||||||
|
label="Version de Java"
|
||||||
|
[configControl]="getConfigControl(keys.JAVA_VERSION)">
|
||||||
|
</cre-text-config>
|
||||||
|
|
||||||
|
<cre-text-config
|
||||||
|
label="Système d'exploitation"
|
||||||
|
[configControl]="getConfigControl(keys.OPERATING_SYSTEM)">
|
||||||
|
</cre-text-config>
|
||||||
|
</cre-config-list>
|
||||||
|
<cre-config-actions>
|
||||||
|
<cre-warn-button (click)="restartConfirmBox.show()">Redémarrer le serveur</cre-warn-button>
|
||||||
|
</cre-config-actions>
|
||||||
|
</cre-config-section>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<cre-confirm-box #invalidFormatConfirmBox message="Le format du fichier choisi n'est pas valide"></cre-confirm-box>
|
||||||
|
<cre-confirm-box #restartConfirmBox
|
||||||
|
message="Voulez-vous vraiment redémarrer le serveur? Les changements nécessitant un redémarrage seront appliqués."
|
||||||
|
(confirm)="restart()"></cre-confirm-box>
|
||||||
|
<cre-confirm-box #restartingConfirmBox message="Le serveur est en cours de redémarrage" (cancel)="reload()"
|
||||||
|
(confirm)="reload()"></cre-confirm-box>
|
|
@ -0,0 +1,98 @@
|
||||||
|
import {Component, ViewChild} from '@angular/core'
|
||||||
|
import {ErrorHandlingComponent} from '../shared/components/subscribing.component'
|
||||||
|
import {ConfirmBoxComponent} from '../shared/components/confirm-box/confirm-box.component'
|
||||||
|
import {buildFormControl, Config, ConfigControl} from '../shared/model/config.model'
|
||||||
|
import {FormBuilder, FormControl, FormGroup} from '@angular/forms'
|
||||||
|
import {ConfigService} from '../shared/service/config.service'
|
||||||
|
import {ErrorService} from '../shared/service/error.service'
|
||||||
|
import {ActivatedRoute, Router} from '@angular/router'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'cre-config-editor',
|
||||||
|
templateUrl: 'config-editor.html'
|
||||||
|
})
|
||||||
|
export class CreConfigEditor extends ErrorHandlingComponent {
|
||||||
|
@ViewChild('restartingConfirmBox', {static: true}) restartConfirmBox: ConfirmBoxComponent
|
||||||
|
|
||||||
|
keys = {
|
||||||
|
INSTANCE_NAME: Config.INSTANCE_NAME,
|
||||||
|
INSTANCE_LOGO_PATH: Config.INSTANCE_LOGO_PATH,
|
||||||
|
INSTANCE_ICON_PATH: Config.INSTANCE_ICON_PATH,
|
||||||
|
INSTANCE_URL: Config.INSTANCE_URL,
|
||||||
|
DATABASE_URL: Config.DATABASE_URL,
|
||||||
|
DATABASE_USER: Config.DATABASE_USER,
|
||||||
|
DATABASE_PASSWORD: Config.DATABASE_PASSWORD,
|
||||||
|
DATABASE_VERSION: Config.DATABASE_VERSION,
|
||||||
|
RECIPE_APPROBATION_EXPIRATION: Config.RECIPE_APPROBATION_EXPIRATION,
|
||||||
|
TOUCH_UP_KIT_CACHE_PDF: Config.TOUCH_UP_KIT_CACHE_PDF,
|
||||||
|
TOUCH_UP_KIT_EXPIRATION: Config.TOUCH_UP_KIT_EXPIRATION,
|
||||||
|
BACKEND_BUILD_VERSION: Config.BACKEND_BUILD_VERSION,
|
||||||
|
BACKEND_BUILD_TIME: Config.BACKEND_BUILD_TIME,
|
||||||
|
JAVA_VERSION: Config.JAVA_VERSION,
|
||||||
|
OPERATING_SYSTEM: Config.OPERATING_SYSTEM
|
||||||
|
}
|
||||||
|
|
||||||
|
configs = new Map<string, Config>()
|
||||||
|
form: FormGroup | null
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private configService: ConfigService,
|
||||||
|
formBuilder: FormBuilder,
|
||||||
|
errorService: ErrorService,
|
||||||
|
activatedRoute: ActivatedRoute,
|
||||||
|
router: Router
|
||||||
|
) {
|
||||||
|
super(errorService, activatedRoute, router)
|
||||||
|
|
||||||
|
this.fetchConfigurations(formBuilder)
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
super.ngOnInit()
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfigControl(key: string): ConfigControl {
|
||||||
|
return {
|
||||||
|
config: this.configs.get(key),
|
||||||
|
control: this.form.controls[key] as FormControl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit() {
|
||||||
|
this.subscribe(
|
||||||
|
this.configService.setFromForm(this.form),
|
||||||
|
() => this.reload()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
restart() {
|
||||||
|
this.subscribe(
|
||||||
|
this.configService.restart(),
|
||||||
|
() => this.restartConfirmBox.show()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
reload() {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
get emergencyMode(): boolean {
|
||||||
|
return this.configs.get(Config.EMERGENCY_MODE).content === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchConfigurations(formBuilder: FormBuilder) {
|
||||||
|
this.subscribe(
|
||||||
|
this.configService.all,
|
||||||
|
configurations => this.buildForm(formBuilder, configurations)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildForm(formBuilder: FormBuilder, configurations: Config[]) {
|
||||||
|
const group = {}
|
||||||
|
configurations.forEach(config => {
|
||||||
|
group[config.key] = buildFormControl(config)
|
||||||
|
this.configs.set(config.key, config)
|
||||||
|
})
|
||||||
|
this.form = formBuilder.group(group)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
<div class="cre-image-config-label">
|
||||||
|
<p>
|
||||||
|
{{label}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<cre-config-container
|
||||||
|
[configuration]="config"
|
||||||
|
[tooltip]="tooltip">
|
||||||
|
<div class="d-flex flex-row justify-content-between align-items-center">
|
||||||
|
<cre-file-input
|
||||||
|
class="w-100"
|
||||||
|
accept="image/png,image/jpeg,image/x-icon,image/svg+xml"
|
||||||
|
[control]="control"
|
||||||
|
(selection)="updateImage($event)"
|
||||||
|
(invalidFormat)="invalidFormat.emit()">
|
||||||
|
</cre-file-input>
|
||||||
|
<div class="image-wrapper d-flex flex-column justify-content-end">
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
[src]="updatedImage ? updatedImage : configuredImageUrl"
|
||||||
|
[attr.width]="previewWidth ? previewWidth : null"
|
||||||
|
class="mat-elevation-z3"/>
|
||||||
|
</div>
|
||||||
|
<mat-hint>{{lastUpdated}}</mat-hint>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</cre-config-container>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<cre-config-container [configuration]="config" [tooltip]="tooltip">
|
||||||
|
<cre-period-input
|
||||||
|
[control]="control"
|
||||||
|
[label]="label"
|
||||||
|
[hint]="inputHint">
|
||||||
|
</cre-period-input>
|
||||||
|
</cre-config-container>
|
|
@ -1,8 +1,6 @@
|
||||||
<mat-card class="w-50 x-centered">
|
<mat-card class="w-50 x-centered">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title>
|
<mat-card-title>{{label}}</mat-card-title>
|
||||||
<ng-content select="cre-config-label"></ng-content>
|
|
||||||
</mat-card-title>
|
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content [class.no-action]="!hasActions">
|
<mat-card-content [class.no-action]="!hasActions">
|
||||||
<ng-content select="cre-config-list"></ng-content>
|
<ng-content select="cre-config-list"></ng-content>
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
<cre-config-container [configuration]="config" [tooltip]="tooltip">
|
||||||
|
<cre-primary-button
|
||||||
|
class="w-100 mb-3"
|
||||||
|
(click)="onOpen()">
|
||||||
|
{{buttonLabel}}
|
||||||
|
</cre-primary-button>
|
||||||
|
|
||||||
|
<cre-prompt-dialog
|
||||||
|
[title]="label"
|
||||||
|
(cancel)="onCancel()">
|
||||||
|
<cre-dialog-body>
|
||||||
|
<cre-input
|
||||||
|
class="w-100"
|
||||||
|
type="password"
|
||||||
|
label="Nouvelle valeur"
|
||||||
|
[hint]="inputHint"
|
||||||
|
[control]="control"
|
||||||
|
[icon]="inputIcon"
|
||||||
|
[iconTitle]="inputIconTitle"
|
||||||
|
iconColor="warning">
|
||||||
|
</cre-input>
|
||||||
|
</cre-dialog-body>
|
||||||
|
</cre-prompt-dialog>
|
||||||
|
</cre-config-container>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<cre-config-container [tooltip]="tooltip">
|
||||||
|
<cre-input
|
||||||
|
class="w-100"
|
||||||
|
[control]="control"
|
||||||
|
[label]="label"
|
||||||
|
[hint]="inputHint"
|
||||||
|
[icon]="inputIcon"
|
||||||
|
[iconTitle]="inputIconTitle"
|
||||||
|
iconColor="warning">
|
||||||
|
</cre-input>
|
||||||
|
</cre-config-container>
|
|
@ -1,12 +0,0 @@
|
||||||
<div *ngIf="configuration" [attr.title]="tooltip?.content">
|
|
||||||
<cre-input [class.has-hint]="configuration.editable"
|
|
||||||
class="w-100"
|
|
||||||
[type]="config.key === 'database.password' ? 'password' : 'text'"
|
|
||||||
[label]="label.content"
|
|
||||||
[hint]="configuration.editable ? lastUpdated : null"
|
|
||||||
[control]="config.control"
|
|
||||||
[icon]="configuration.requireRestart ? 'alert' : null"
|
|
||||||
[iconTitle]="configuration.requireRestart ? 'Requiert un redémarrage' : null"
|
|
||||||
iconColor="warning">
|
|
||||||
</cre-input>
|
|
||||||
</div>
|
|
|
@ -1,32 +1,35 @@
|
||||||
import {NgModule} from '@angular/core'
|
import {NgModule} from '@angular/core'
|
||||||
import {
|
import {
|
||||||
CreConfig,
|
CreBoolConfig,
|
||||||
CreConfigLabel,
|
|
||||||
CreConfigEditor,
|
|
||||||
CreConfigSection,
|
|
||||||
CreImageConfig,
|
|
||||||
CreConfigList,
|
|
||||||
CreConfigActions,
|
CreConfigActions,
|
||||||
CreConfigTooltip, CrePeriodConfig, CreBoolConfig, CreDateConfig
|
CreConfigContainer,
|
||||||
|
CreConfigList,
|
||||||
|
CreConfigSection,
|
||||||
|
CreDateConfig,
|
||||||
|
CreImageConfig,
|
||||||
|
CrePeriodConfig,
|
||||||
|
CreSecureConfig,
|
||||||
|
CreTextConfig
|
||||||
} from './config'
|
} from './config'
|
||||||
import {SharedModule} from '../shared/shared.module'
|
import {SharedModule} from '../shared/shared.module'
|
||||||
import {CreInputsModule} from '../shared/components/inputs/inputs.module'
|
import {CreInputsModule} from '../shared/components/inputs/inputs.module'
|
||||||
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module'
|
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module'
|
||||||
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
|
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
|
||||||
|
import {CreConfigEditor} from './config-editor'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
CreConfigLabel,
|
|
||||||
CreConfigTooltip,
|
|
||||||
CreConfigEditor,
|
|
||||||
CreConfig,
|
|
||||||
CreImageConfig,
|
|
||||||
CreConfigSection,
|
|
||||||
CreConfigList,
|
CreConfigList,
|
||||||
CreConfigActions,
|
CreConfigActions,
|
||||||
|
CreConfigSection,
|
||||||
|
CreConfigContainer,
|
||||||
|
CreTextConfig,
|
||||||
|
CreImageConfig,
|
||||||
CreBoolConfig,
|
CreBoolConfig,
|
||||||
CrePeriodConfig,
|
CrePeriodConfig,
|
||||||
CreDateConfig
|
CreDateConfig,
|
||||||
|
CreSecureConfig,
|
||||||
|
CreConfigEditor
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
SharedModule,
|
SharedModule,
|
||||||
|
@ -35,4 +38,5 @@ import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
|
||||||
CreButtonsModule
|
CreButtonsModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ConfigModule { }
|
export class ConfigModule {
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
mat-hint
|
mat-hint
|
||||||
font-size: .8em
|
font-size: .8em
|
||||||
|
|
||||||
cre-config
|
cre-config-container
|
||||||
display: block
|
display: block
|
||||||
|
|
||||||
cre-input.has-hint
|
.cre-config:not(.cre-editable-config)
|
||||||
margin-bottom: 1em
|
margin-bottom: 1em
|
||||||
|
|
||||||
mat-hint
|
mat-hint
|
||||||
|
|
|
@ -1,56 +1,12 @@
|
||||||
import {
|
import {AfterViewInit, Component, ContentChild, Directive, EventEmitter, Input, Output, ViewChild, ViewEncapsulation} from '@angular/core'
|
||||||
AfterViewInit,
|
|
||||||
Component,
|
|
||||||
ContentChild,
|
|
||||||
Directive,
|
|
||||||
ElementRef,
|
|
||||||
EventEmitter,
|
|
||||||
Input,
|
|
||||||
Output,
|
|
||||||
ViewChild,
|
|
||||||
ViewEncapsulation
|
|
||||||
} from '@angular/core'
|
|
||||||
import {ConfigService} from '../shared/service/config.service'
|
import {ConfigService} from '../shared/service/config.service'
|
||||||
import {Config} from '../shared/model/config.model'
|
import {Config, ConfigControl} from '../shared/model/config.model'
|
||||||
import {ErrorHandlingComponent, SubscribingComponent} from '../shared/components/subscribing.component'
|
import {SubscribingComponent} from '../shared/components/subscribing.component'
|
||||||
import {ErrorService} from '../shared/service/error.service'
|
import {ErrorService} from '../shared/service/error.service'
|
||||||
import {ActivatedRoute, Router} from '@angular/router'
|
import {ActivatedRoute, Router} from '@angular/router'
|
||||||
import {formatDate, formatDateTime, getFileUrl, readFile} from '../shared/utils/utils'
|
import {formatDate, formatDateTime, getFileUrl, readFile} from '../shared/utils/utils'
|
||||||
import {FormControl, Validators} from '@angular/forms'
|
import {AbstractControl} from '@angular/forms'
|
||||||
import {ConfirmBoxComponent} from '../shared/components/confirm-box/confirm-box.component'
|
import {CrePromptDialog} from '../shared/components/dialogs/dialogs'
|
||||||
import {environment} from '../../../environments/environment'
|
|
||||||
|
|
||||||
@Directive({
|
|
||||||
selector: 'cre-config-label'
|
|
||||||
})
|
|
||||||
export class CreConfigLabel implements AfterViewInit {
|
|
||||||
content: string
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private element: ElementRef
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
|
||||||
this.content = this.element.nativeElement.innerHTML
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Directive({
|
|
||||||
selector: 'cre-config-tooltip'
|
|
||||||
})
|
|
||||||
export class CreConfigTooltip implements AfterViewInit {
|
|
||||||
content: string
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private element: ElementRef
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
|
||||||
this.content = this.element.nativeElement.innerHTML
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: 'cre-config-list'
|
selector: 'cre-config-list'
|
||||||
|
@ -70,6 +26,8 @@ export class CreConfigActions {
|
||||||
templateUrl: 'config-section.html'
|
templateUrl: 'config-section.html'
|
||||||
})
|
})
|
||||||
export class CreConfigSection {
|
export class CreConfigSection {
|
||||||
|
@Input() label: string
|
||||||
|
|
||||||
@ContentChild(CreConfigActions) actions: CreConfigActions
|
@ContentChild(CreConfigActions) actions: CreConfigActions
|
||||||
|
|
||||||
get hasActions(): boolean {
|
get hasActions(): boolean {
|
||||||
|
@ -78,17 +36,25 @@ export class CreConfigSection {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cre-config',
|
selector: 'cre-config-container',
|
||||||
templateUrl: 'config.html',
|
templateUrl: 'config-container.html',
|
||||||
styleUrls: ['config.sass']
|
styleUrls: ['config.sass'],
|
||||||
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class CreConfig extends SubscribingComponent {
|
export class CreConfigContainer {
|
||||||
@Input() config: { key: string, control: FormControl }
|
@Input() configuration?: Config
|
||||||
|
@Input() tooltip: string
|
||||||
|
|
||||||
@ContentChild(CreConfigLabel, {static: true}) label: CreConfigLabel
|
get readOnly(): boolean {
|
||||||
@ContentChild(CreConfigTooltip, {static: true}) tooltip: CreConfigTooltip
|
return !this.configuration?.editable ?? true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
configuration: Config | null
|
@Directive()
|
||||||
|
abstract class _CreConfigBase extends SubscribingComponent {
|
||||||
|
@Input() configControl: ConfigControl
|
||||||
|
@Input() label: string
|
||||||
|
@Input() tooltip?: string
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
@ -101,39 +67,105 @@ export class CreConfig extends SubscribingComponent {
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
super.ngOnInit()
|
super.ngOnInit()
|
||||||
|
|
||||||
this.subscribe(
|
|
||||||
this.configService.get(this.config.key),
|
|
||||||
config => this.setConfig(config)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfig(config: Config) {
|
get config(): Config {
|
||||||
this.configuration = config
|
return this.configControl.config
|
||||||
this.config.control.setValue(config.content)
|
}
|
||||||
if (!config.editable) {
|
|
||||||
this.config.control.disable()
|
get control(): AbstractControl {
|
||||||
}
|
return this.configControl.control
|
||||||
}
|
}
|
||||||
|
|
||||||
get lastUpdated(): string {
|
get lastUpdated(): string {
|
||||||
return 'Dernière mise à jour: ' + formatDateTime(this.configuration.lastUpdated)
|
return 'Dernière mise à jour: ' + formatDateTime(this.config.lastUpdated)
|
||||||
|
}
|
||||||
|
|
||||||
|
get inputHint(): string {
|
||||||
|
return this.config?.editable ? this.lastUpdated : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
abstract class _CreTextConfigBase extends _CreConfigBase {
|
||||||
|
private static readonly REQUIRE_RESTART_ICON = 'alert'
|
||||||
|
private static readonly REQUIRE_RESTART_ICON_TITLE = 'Requiert un redémarrage'
|
||||||
|
|
||||||
|
get inputIcon(): string {
|
||||||
|
return this.config?.requireRestart ? _CreTextConfigBase.REQUIRE_RESTART_ICON : null
|
||||||
|
}
|
||||||
|
|
||||||
|
get inputIconTitle(): string {
|
||||||
|
return this.config?.requireRestart ? _CreTextConfigBase.REQUIRE_RESTART_ICON_TITLE : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'cre-text-config',
|
||||||
|
templateUrl: 'config-text.html',
|
||||||
|
styleUrls: ['config.sass']
|
||||||
|
})
|
||||||
|
export class CreTextConfig extends _CreTextConfigBase {
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cre-image-config',
|
selector: 'cre-image-config',
|
||||||
templateUrl: 'image.html',
|
templateUrl: 'config-image.html',
|
||||||
styleUrls: ['config.sass'],
|
styleUrls: ['config.sass'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class CreImageConfig extends CreConfig {
|
export class CreImageConfig extends _CreConfigBase {
|
||||||
@Input() previewWidth: string | null
|
@Input() previewWidth: string | null
|
||||||
|
|
||||||
@Output() invalidFormat = new EventEmitter<void>()
|
@Output() invalidFormat = new EventEmitter<void>()
|
||||||
|
|
||||||
updatedImage: any | null
|
updatedImage: any | null
|
||||||
|
|
||||||
|
updateImage(file: File): any {
|
||||||
|
readFile(file, (content) => this.updatedImage = content)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
get configuredImageUrl(): string {
|
||||||
|
return getFileUrl(this.config.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'cre-bool-config',
|
||||||
|
templateUrl: 'config-bool.html'
|
||||||
|
})
|
||||||
|
export class CreBoolConfig extends _CreConfigBase {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'cre-period-config',
|
||||||
|
templateUrl: 'config-period.html'
|
||||||
|
})
|
||||||
|
export class CrePeriodConfig extends _CreConfigBase {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'cre-date-config',
|
||||||
|
templateUrl: 'config-date.html'
|
||||||
|
})
|
||||||
|
export class CreDateConfig extends _CreTextConfigBase implements AfterViewInit {
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.control.setValue(formatDate(this.config.content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'cre-secure-config',
|
||||||
|
templateUrl: 'config-secure.html'
|
||||||
|
})
|
||||||
|
export class CreSecureConfig extends _CreTextConfigBase {
|
||||||
|
@ViewChild(CrePromptDialog) dialog: CrePromptDialog
|
||||||
|
|
||||||
|
@Input() buttonLabel: string
|
||||||
|
|
||||||
|
private initialValue: string | null
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
errorService: ErrorService,
|
errorService: ErrorService,
|
||||||
|
@ -143,113 +175,12 @@ export class CreImageConfig extends CreConfig {
|
||||||
super(configService, errorService, activatedRoute, router)
|
super(configService, errorService, activatedRoute, router)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateImage(file: File): any {
|
onOpen() {
|
||||||
readFile(file, (content) => this.updatedImage = content)
|
this.initialValue = this.control.value
|
||||||
|
this.dialog.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
get configuredImageUrl(): string {
|
this.control.setValue(this.initialValue)
|
||||||
return getFileUrl(this.configuration.content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'cre-bool-config',
|
|
||||||
templateUrl: 'bool.html'
|
|
||||||
})
|
|
||||||
export class CreBoolConfig extends CreConfig {
|
|
||||||
setConfig(config: Config) {
|
|
||||||
super.setConfig(config)
|
|
||||||
this.config.control.setValue(config.content === 'true')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'cre-period-config',
|
|
||||||
templateUrl: 'period.html'
|
|
||||||
})
|
|
||||||
export class CrePeriodConfig extends CreConfig {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'cre-date-config',
|
|
||||||
templateUrl: 'date.html'
|
|
||||||
})
|
|
||||||
export class CreDateConfig extends CreConfig {
|
|
||||||
setConfig(config: Config) {
|
|
||||||
super.setConfig(config);
|
|
||||||
this.config.control.setValue(formatDate(config.content))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'cre-config-editor',
|
|
||||||
templateUrl: 'editor.html'
|
|
||||||
})
|
|
||||||
export class CreConfigEditor extends ErrorHandlingComponent {
|
|
||||||
@ViewChild('restartingConfirmBox', {static: true}) restartConfirmBox: ConfirmBoxComponent
|
|
||||||
|
|
||||||
keys = {
|
|
||||||
INSTANCE_NAME: Config.INSTANCE_NAME,
|
|
||||||
INSTANCE_LOGO_PATH: Config.INSTANCE_LOGO_PATH,
|
|
||||||
INSTANCE_ICON_PATH: Config.INSTANCE_ICON_PATH,
|
|
||||||
INSTANCE_URL: Config.INSTANCE_URL,
|
|
||||||
DATABASE_URL: Config.DATABASE_URL,
|
|
||||||
DATABASE_USER: Config.DATABASE_USER,
|
|
||||||
DATABASE_PASSWORD: Config.DATABASE_PASSWORD,
|
|
||||||
DATABASE_VERSION: Config.DATABASE_VERSION,
|
|
||||||
RECIPE_APPROBATION_EXPIRATION: Config.RECIPE_APPROBATION_EXPIRATION,
|
|
||||||
TOUCH_UP_KIT_CACHE_PDF: Config.TOUCH_UP_KIT_CACHE_PDF,
|
|
||||||
TOUCH_UP_KIT_EXPIRATION: Config.TOUCH_UP_KIT_EXPIRATION,
|
|
||||||
BACKEND_BUILD_VERSION: Config.BACKEND_BUILD_VERSION,
|
|
||||||
BACKEND_BUILD_TIME: Config.BACKEND_BUILD_TIME,
|
|
||||||
JAVA_VERSION: Config.JAVA_VERSION,
|
|
||||||
OPERATING_SYSTEM: Config.OPERATING_SYSTEM
|
|
||||||
}
|
|
||||||
controls = new Map<string, FormControl>()
|
|
||||||
emergencyMode: string | null
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private configService: ConfigService,
|
|
||||||
errorService: ErrorService,
|
|
||||||
activatedRoute: ActivatedRoute,
|
|
||||||
router: Router
|
|
||||||
) {
|
|
||||||
super(errorService, activatedRoute, router)
|
|
||||||
|
|
||||||
for (let key in this.keys) {
|
|
||||||
this.controls[this.keys[key]] = new FormControl(null, Validators.required)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.subscribe(
|
|
||||||
this.configService.get(Config.EMERGENCY_MODE),
|
|
||||||
config => {
|
|
||||||
this.emergencyMode = config.content
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
getConfig(key: string) {
|
|
||||||
return {key, control: this.controls[key]}
|
|
||||||
}
|
|
||||||
|
|
||||||
save() {
|
|
||||||
this.subscribe(
|
|
||||||
this.configService.set(this.controls),
|
|
||||||
() => this.reload()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
restart() {
|
|
||||||
this.subscribe(
|
|
||||||
this.configService.restart(),
|
|
||||||
() => this.restartConfirmBox.show()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
reload() {
|
|
||||||
window.location.reload()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
<div *ngIf="configuration" [attr.title]="tooltip?.content">
|
|
||||||
<cre-input [class.has-hint]="configuration.editable"
|
|
||||||
class="w-100"
|
|
||||||
type="text"
|
|
||||||
[label]="label.content"
|
|
||||||
[hint]="configuration.editable ? lastUpdated : null"
|
|
||||||
[control]="config.control"
|
|
||||||
[icon]="configuration.requireRestart ? 'alert' : null"
|
|
||||||
[iconTitle]="configuration.requireRestart ? 'Requiert un redémarrage' : null"
|
|
||||||
iconColor="warning">
|
|
||||||
</cre-input>
|
|
||||||
</div>
|
|
|
@ -1,120 +0,0 @@
|
||||||
<cre-action-bar>
|
|
||||||
<cre-action-group>
|
|
||||||
<cre-primary-button routerLink="/color">Retour</cre-primary-button>
|
|
||||||
</cre-action-group>
|
|
||||||
<cre-action-group>
|
|
||||||
<cre-accent-button (click)="save()">Enregistrer</cre-accent-button>
|
|
||||||
</cre-action-group>
|
|
||||||
</cre-action-bar>
|
|
||||||
|
|
||||||
<div *ngIf="emergencyMode" class="d-flex flex-column" style="gap: 1.5rem">
|
|
||||||
<cre-config-section *ngIf="emergencyMode === 'false'">
|
|
||||||
<cre-config-label>Apparence</cre-config-label>
|
|
||||||
<cre-config-list>
|
|
||||||
<!-- <cre-config [config]="getConfig(keys.INSTANCE_NAME)">-->
|
|
||||||
<!-- <cre-config-label>Nom de l'instance</cre-config-label>-->
|
|
||||||
<!-- <cre-config-tooltip>-->
|
|
||||||
<!-- Affiché dans la barre de titre du navigateur ou en survolant l'onglet de la page dans le navigateur.-->
|
|
||||||
<!-- </cre-config-tooltip>-->
|
|
||||||
<!-- </cre-config>-->
|
|
||||||
|
|
||||||
<cre-image-config [config]="getConfig(keys.INSTANCE_LOGO_PATH)" previewWidth="170px"
|
|
||||||
(invalidFormat)="invalidFormatConfirmBox.show()">
|
|
||||||
<cre-config-label>Logo</cre-config-label>
|
|
||||||
<cre-config-tooltip>
|
|
||||||
Affiché dans la bannière de l'application web. Il peut être nécessaire de forcer le
|
|
||||||
rafraîchissement du cache du navigateur pour que ce changement prenne effet (généralement avec les touches
|
|
||||||
'Ctrl+F5').
|
|
||||||
</cre-config-tooltip>
|
|
||||||
</cre-image-config>
|
|
||||||
|
|
||||||
<cre-image-config [config]="getConfig(keys.INSTANCE_ICON_PATH)" previewWidth="32px"
|
|
||||||
(invalidFormat)="invalidFormatConfirmBox.show()">
|
|
||||||
<cre-config-label>Icône</cre-config-label>
|
|
||||||
<cre-config-tooltip>
|
|
||||||
Affiché dans l'onglet de la page dans le navigateur. Il peut être nécessaire de forcer le
|
|
||||||
rafraîchissement du cache du navigateur pour que ce changement prenne effet (généralement avec les touches
|
|
||||||
'Ctrl+F5').
|
|
||||||
</cre-config-tooltip>
|
|
||||||
</cre-image-config>
|
|
||||||
</cre-config-list>
|
|
||||||
</cre-config-section>
|
|
||||||
|
|
||||||
<cre-config-section>
|
|
||||||
<cre-config-label>Données</cre-config-label>
|
|
||||||
<cre-config-list class="pt-2">
|
|
||||||
<cre-period-config [config]="getConfig(keys.RECIPE_APPROBATION_EXPIRATION)">
|
|
||||||
<cre-config-label>Période d'expiration de l'approbation de l'échantillon des recettes</cre-config-label>
|
|
||||||
</cre-period-config>
|
|
||||||
|
|
||||||
<cre-period-config [config]="getConfig(keys.TOUCH_UP_KIT_EXPIRATION)">
|
|
||||||
<cre-config-label>Période d'expiration des kits de retouches complets</cre-config-label>
|
|
||||||
<cre-config-tooltip>
|
|
||||||
Les kits de retouche complétés expirent après la période configurée. Les kits de retouche expirés seront supprimés automatiquement.
|
|
||||||
</cre-config-tooltip>
|
|
||||||
</cre-period-config>
|
|
||||||
|
|
||||||
<cre-bool-config [config]="getConfig(keys.TOUCH_UP_KIT_CACHE_PDF)">
|
|
||||||
<cre-config-label>Activer le cache des PDFs générés</cre-config-label>
|
|
||||||
<cre-config-tooltip>
|
|
||||||
Cette option permet de stocker les PDFs générés sur le disque, ce qui permet d'accélérer
|
|
||||||
l'accès aux PDFs si la lecture des fichiers cachés sur le disque est plus rapide que la génération d'un
|
|
||||||
nouveau PDF.
|
|
||||||
</cre-config-tooltip>
|
|
||||||
</cre-bool-config>
|
|
||||||
</cre-config-list>
|
|
||||||
</cre-config-section>
|
|
||||||
|
|
||||||
<cre-config-section>
|
|
||||||
<cre-config-label>Système</cre-config-label>
|
|
||||||
<cre-config-list>
|
|
||||||
<cre-config [config]="getConfig(keys.INSTANCE_URL)">
|
|
||||||
<cre-config-label>URL de l'instance</cre-config-label>
|
|
||||||
<cre-config-tooltip>
|
|
||||||
Utilisé pour générer l'URL de certaines ressources, comme les images et les fiches signalitiques.
|
|
||||||
</cre-config-tooltip>
|
|
||||||
</cre-config>
|
|
||||||
|
|
||||||
<cre-config [config]="getConfig(keys.DATABASE_URL)">
|
|
||||||
<cre-config-label>URL de la base de données</cre-config-label>
|
|
||||||
</cre-config>
|
|
||||||
|
|
||||||
<cre-config [config]="getConfig(keys.DATABASE_USER)">
|
|
||||||
<cre-config-label>Utilisateur de la base de données</cre-config-label>
|
|
||||||
</cre-config>
|
|
||||||
|
|
||||||
<cre-config [config]="getConfig(keys.DATABASE_PASSWORD)">
|
|
||||||
<cre-config-label>Mot de passe de la base de données</cre-config-label>
|
|
||||||
</cre-config>
|
|
||||||
|
|
||||||
<cre-config [config]="getConfig(keys.DATABASE_VERSION)">
|
|
||||||
<cre-config-label>Version de la base de données</cre-config-label>
|
|
||||||
</cre-config>
|
|
||||||
|
|
||||||
<cre-config [config]="getConfig(keys.BACKEND_BUILD_VERSION)">
|
|
||||||
<cre-config-label>Version de Color Recipes Explorer</cre-config-label>
|
|
||||||
</cre-config>
|
|
||||||
|
|
||||||
<cre-date-config [config]="getConfig(keys.BACKEND_BUILD_TIME)">
|
|
||||||
<cre-config-label>Date de compilation de Color Recipes Explorer</cre-config-label>
|
|
||||||
</cre-date-config>
|
|
||||||
|
|
||||||
<cre-config [config]="getConfig(keys.JAVA_VERSION)">
|
|
||||||
<cre-config-label>Version de Java</cre-config-label>
|
|
||||||
</cre-config>
|
|
||||||
|
|
||||||
<cre-config [config]="getConfig(keys.OPERATING_SYSTEM)">
|
|
||||||
<cre-config-label>Système d'exploitation</cre-config-label>
|
|
||||||
</cre-config>
|
|
||||||
</cre-config-list>
|
|
||||||
<cre-config-actions>
|
|
||||||
<cre-warn-button (click)="restartConfirmBox.show()">Redémarrer le serveur</cre-warn-button>
|
|
||||||
</cre-config-actions>
|
|
||||||
</cre-config-section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<cre-confirm-box #invalidFormatConfirmBox message="Le format du fichier choisi n'est pas valide"></cre-confirm-box>
|
|
||||||
<cre-confirm-box #restartConfirmBox message="Voulez-vous vraiment redémarrer le serveur? Les changements nécessitant un redémarrage seront appliqués."
|
|
||||||
(confirm)="restart()"></cre-confirm-box>
|
|
||||||
<cre-confirm-box #restartingConfirmBox message="Le serveur est en cours de redémarrage" (cancel)="reload()"
|
|
||||||
(confirm)="reload()"></cre-confirm-box>
|
|
|
@ -1,26 +0,0 @@
|
||||||
<div class="cre-image-config-label">
|
|
||||||
<p>
|
|
||||||
<ng-content select="cre-config-label"></ng-content>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf="configuration"
|
|
||||||
class="d-flex flex-row justify-content-between align-items-center"
|
|
||||||
[attr.title]="tooltip?.content">
|
|
||||||
<cre-file-input
|
|
||||||
class="w-100"
|
|
||||||
accept="image/png,image/jpeg,image/x-icon,image/svg+xml"
|
|
||||||
[control]="config.control"
|
|
||||||
(selection)="updateImage($event)"
|
|
||||||
(invalidFormat)="invalidFormat.emit()">
|
|
||||||
</cre-file-input>
|
|
||||||
<div class="image-wrapper d-flex flex-column justify-content-end">
|
|
||||||
<div>
|
|
||||||
<img
|
|
||||||
[src]="updatedImage ? updatedImage : configuredImageUrl"
|
|
||||||
[attr.width]="previewWidth ? previewWidth : null"
|
|
||||||
class="mat-elevation-z3"/>
|
|
||||||
</div>
|
|
||||||
<mat-hint>{{lastUpdated}}</mat-hint>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,7 +0,0 @@
|
||||||
<div *ngIf="configuration" [attr.title]="tooltip?.content">
|
|
||||||
<cre-period-input
|
|
||||||
[control]="config.control"
|
|
||||||
[label]="label.content"
|
|
||||||
[hint]="lastUpdated">
|
|
||||||
</cre-period-input>
|
|
||||||
</div>
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
cre-button, cre-primary-button, cre-accent-button, cre-warn-button
|
||||||
|
display: inline-block
|
||||||
|
width: inherit
|
||||||
|
|
||||||
|
button
|
||||||
|
width: 100%
|
|
@ -1,51 +1,60 @@
|
||||||
import {Component, Input} from '@angular/core'
|
import {Component, Input, ViewEncapsulation} from '@angular/core'
|
||||||
import {ThemePalette} from '@angular/material/core'
|
import {ThemePalette} from '@angular/material/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cre-button',
|
selector: 'cre-button',
|
||||||
template: `
|
template: `
|
||||||
<button mat-raised-button [color]="color" [disabled]="disabled">
|
<button mat-raised-button [type]="type" [color]="color" [disabled]="disabled">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</button>
|
</button>
|
||||||
`
|
`,
|
||||||
|
styleUrls: ['buttons.sass'],
|
||||||
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class CreButtonComponent {
|
export class CreButtonComponent {
|
||||||
@Input() color: ThemePalette
|
@Input() color: ThemePalette
|
||||||
|
@Input() type = 'button'
|
||||||
@Input() disabled = false
|
@Input() disabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cre-primary-button',
|
selector: 'cre-primary-button',
|
||||||
template: `
|
template: `
|
||||||
<cre-button color="primary" [disabled]="disabled">
|
<cre-button color="primary" [type]="type" [disabled]="disabled">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</cre-button>
|
</cre-button>
|
||||||
`
|
`,
|
||||||
|
styleUrls: ['buttons.sass']
|
||||||
})
|
})
|
||||||
export class CrePrimaryButtonComponent {
|
export class CrePrimaryButtonComponent {
|
||||||
|
@Input() type = 'button'
|
||||||
@Input() disabled = false
|
@Input() disabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cre-accent-button',
|
selector: 'cre-accent-button',
|
||||||
template: `
|
template: `
|
||||||
<cre-button color="accent" [disabled]="disabled">
|
<cre-button color="accent" [type]="type" [disabled]="disabled">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</cre-button>
|
</cre-button>
|
||||||
`
|
`,
|
||||||
|
styleUrls: ['buttons.sass']
|
||||||
})
|
})
|
||||||
export class CreAccentButtonComponent {
|
export class CreAccentButtonComponent {
|
||||||
|
@Input() type = 'button'
|
||||||
@Input() disabled = false
|
@Input() disabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cre-warn-button',
|
selector: 'cre-warn-button',
|
||||||
template: `
|
template: `
|
||||||
<cre-button color="warn" [disabled]="disabled">
|
<cre-button color="warn" [type]="type" [disabled]="disabled">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</cre-button>
|
</cre-button>
|
||||||
`
|
`,
|
||||||
|
styleUrls: ['buttons.sass']
|
||||||
})
|
})
|
||||||
export class CreWarnButtonComponent {
|
export class CreWarnButtonComponent {
|
||||||
|
@Input() type = 'button'
|
||||||
@Input() disabled = false
|
@Input() disabled = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import {NgModule} from '@angular/core'
|
||||||
|
import {CreDialogBody, CrePromptDialog} from './dialogs'
|
||||||
|
import {MatDialogModule} from '@angular/material/dialog'
|
||||||
|
import {CreButtonsModule} from '../buttons/buttons.module'
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
CrePromptDialog,
|
||||||
|
CreDialogBody
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
CrePromptDialog,
|
||||||
|
CreDialogBody
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
MatDialogModule,
|
||||||
|
CreButtonsModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class CreDialogsModule {
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
@import "~src/variables";
|
||||||
|
|
||||||
|
.cre-dialog-panel {
|
||||||
|
min-width: 20rem;
|
||||||
|
|
||||||
|
mat-dialog-container {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.mat-dialog-title, .mat-dialog-content, .mat-dialog-actions {
|
||||||
|
margin: 0;
|
||||||
|
padding: $spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-dialog-title {
|
||||||
|
background-color: $color-primary;
|
||||||
|
color: $text-color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-dialog-actions {
|
||||||
|
min-height: auto;
|
||||||
|
justify-content: end;
|
||||||
|
gap: map-get($spacers, 1);
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
import {Component, Directive, EventEmitter, Input, Output, TemplateRef, ViewChild, ViewEncapsulation} from '@angular/core'
|
||||||
|
import {MatDialog, MatDialogRef} from '@angular/material/dialog'
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: 'cre-dialog-body'
|
||||||
|
})
|
||||||
|
export class CreDialogBody {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
abstract class CreDialog<D = CreDialogData> {
|
||||||
|
@ViewChild(TemplateRef) dialogTemplate: TemplateRef<any>
|
||||||
|
|
||||||
|
@Output() cancel = new EventEmitter<void>();
|
||||||
|
@Output() continue = new EventEmitter<void>();
|
||||||
|
|
||||||
|
private dialogRef: MatDialogRef<TemplateRef<any>> | null
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected dialog: MatDialog
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract get data(): D
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
|
this.close()
|
||||||
|
this.cancel.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
onContinue() {
|
||||||
|
this.close()
|
||||||
|
this.continue.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private open() {
|
||||||
|
const config = {
|
||||||
|
panelClass: 'cre-dialog-panel',
|
||||||
|
data: this.data
|
||||||
|
}
|
||||||
|
this.dialogRef = this.dialog.open(this.dialogTemplate, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
private close() {
|
||||||
|
this.dialogRef.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'cre-prompt-dialog',
|
||||||
|
templateUrl: 'prompt.html',
|
||||||
|
styleUrls: ['dialogs.scss'],
|
||||||
|
encapsulation: ViewEncapsulation.None
|
||||||
|
})
|
||||||
|
export class CrePromptDialog extends CreDialog<CrePromptDialogData> {
|
||||||
|
@Input() title: string
|
||||||
|
|
||||||
|
protected get data(): CrePromptDialogData {
|
||||||
|
return {
|
||||||
|
title: this.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class CreDialogData {
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class CrePromptDialogData extends CreDialogData {
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
<ng-template let-data>
|
||||||
|
<h1 mat-dialog-title>{{data.title}}</h1>
|
||||||
|
<div mat-dialog-content>
|
||||||
|
<ng-content select="cre-dialog-body"></ng-content>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<cre-primary-button (click)="onCancel()">Annuler</cre-primary-button>
|
||||||
|
<cre-accent-button (click)="onContinue()">Continuer</cre-accent-button>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
|
@ -1,4 +1,4 @@
|
||||||
@import "~src/custom-theme"
|
@import "~src/variables"
|
||||||
|
|
||||||
.info-banner-wrapper
|
.info-banner-wrapper
|
||||||
background-color: $color-primary
|
background-color: $color-primary
|
||||||
|
|
|
@ -1,25 +1,21 @@
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>{{label}}</mat-label>
|
<mat-label>{{label}}</mat-label>
|
||||||
<input
|
|
||||||
*ngIf="!control"
|
<ng-container *ngIf="!control">
|
||||||
matInput
|
<input
|
||||||
[type]="type"
|
#input
|
||||||
[step]="step"
|
matInput
|
||||||
[placeholder]="placeholder"
|
[disabled]="disabled"
|
||||||
[(ngModel)]="value"
|
[(ngModel)]="value"
|
||||||
[required]="required"
|
(change)="valueChange.emit(value)"/>
|
||||||
[autocomplete]="autocomplete ? 'on' : 'off'"
|
</ng-container>
|
||||||
[disabled]="disabled"
|
<ng-container *ngIf="control">
|
||||||
(change)="valueChange.emit(value)"/>
|
<input
|
||||||
<input
|
#input
|
||||||
*ngIf="control"
|
matInput
|
||||||
matInput
|
[formControl]="control">
|
||||||
[type]="type"
|
</ng-container>
|
||||||
[step]="step"
|
|
||||||
[placeholder]="placeholder"
|
|
||||||
[required]="required"
|
|
||||||
[formControl]="control"
|
|
||||||
[autocomplete]="autocomplete ? 'on' : 'off'"/>
|
|
||||||
<mat-icon
|
<mat-icon
|
||||||
matSuffix
|
matSuffix
|
||||||
[svgIcon]="icon"
|
[svgIcon]="icon"
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import {
|
import {
|
||||||
|
AfterViewInit,
|
||||||
Component,
|
Component,
|
||||||
ContentChild,
|
ContentChild,
|
||||||
|
Directive,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
|
@ -17,7 +19,14 @@ import {Observable, Subject} from 'rxjs'
|
||||||
import {map, takeUntil} from 'rxjs/operators'
|
import {map, takeUntil} from 'rxjs/operators'
|
||||||
import {MatChipInputEvent} from '@angular/material/chips'
|
import {MatChipInputEvent} from '@angular/material/chips'
|
||||||
import {MatAutocomplete, MatAutocompleteSelectedEvent} from '@angular/material/autocomplete'
|
import {MatAutocomplete, MatAutocompleteSelectedEvent} from '@angular/material/autocomplete'
|
||||||
import {MatOptionSelectionChange} from '@angular/material/core'
|
|
||||||
|
@Directive()
|
||||||
|
abstract class _CreInputBase {
|
||||||
|
@Input() control: AbstractControl | null
|
||||||
|
@Input() label: string
|
||||||
|
@Input() value
|
||||||
|
@Input() disabled = false
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cre-input',
|
selector: 'cre-input',
|
||||||
|
@ -25,24 +34,30 @@ import {MatOptionSelectionChange} from '@angular/material/core'
|
||||||
encapsulation: ViewEncapsulation.None,
|
encapsulation: ViewEncapsulation.None,
|
||||||
styleUrls: ['input.sass']
|
styleUrls: ['input.sass']
|
||||||
})
|
})
|
||||||
export class CreInputComponent {
|
export class CreInputComponent extends _CreInputBase implements AfterViewInit {
|
||||||
@Input() control: FormControl | null
|
|
||||||
@Input() type = 'text'
|
@Input() type = 'text'
|
||||||
@Input() label: string
|
|
||||||
@Input() icon: string
|
@Input() icon: string
|
||||||
@Input() iconTitle: string | null
|
@Input() iconTitle: string | null
|
||||||
@Input() required = true
|
@Input() required = true
|
||||||
@Input() autocomplete = true
|
@Input() autocomplete = true
|
||||||
@Input() placeholder: string | null
|
@Input() placeholder: string | null
|
||||||
@Input() step = 1
|
@Input() step = 1
|
||||||
@Input() value
|
|
||||||
@Input() iconColor: string = 'black'
|
@Input() iconColor: string = 'black'
|
||||||
@Input() disabled = false
|
|
||||||
@Input() hint: string | null
|
@Input() hint: string | null
|
||||||
|
|
||||||
@Output() valueChange = new EventEmitter<any>()
|
@Output() valueChange = new EventEmitter<any>()
|
||||||
|
|
||||||
|
@ViewChild('input') input: any
|
||||||
@ContentChild(TemplateRef) errors: TemplateRef<any>
|
@ContentChild(TemplateRef) errors: TemplateRef<any>
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
const element = this.input.nativeElement
|
||||||
|
element.type = this.type
|
||||||
|
element.step = this.step.toString()
|
||||||
|
element.placeholder = this.placeholder
|
||||||
|
element.required = this.required
|
||||||
|
element.autocomplete = this.autocomplete ? 'on' : 'off'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -51,7 +66,7 @@ export class CreInputComponent {
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class CreAutocompleteInputComponent {
|
export class CreAutocompleteInputComponent {
|
||||||
@Input() control: FormControl | null
|
@Input() control: AbstractControl | null
|
||||||
@Input() label: string
|
@Input() label: string
|
||||||
@Input() icon: string
|
@Input() icon: string
|
||||||
@Input() required = true
|
@Input() required = true
|
||||||
|
@ -69,7 +84,7 @@ export class CreAutocompleteInputComponent {
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class CreChipInputComponent implements OnInit {
|
export class CreChipInputComponent implements OnInit {
|
||||||
@Input() control: FormControl
|
@Input() control: AbstractControl
|
||||||
@Input() label: string
|
@Input() label: string
|
||||||
@Input() icon: string
|
@Input() icon: string
|
||||||
@Input() required = true
|
@Input() required = true
|
||||||
|
@ -118,7 +133,7 @@ export class CreChipInputComponent implements OnInit {
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class CreComboBoxComponent {
|
export class CreComboBoxComponent {
|
||||||
@Input() control: FormControl
|
@Input() control: AbstractControl
|
||||||
@Input() label: string
|
@Input() label: string
|
||||||
@Input() icon: string
|
@Input() icon: string
|
||||||
@Input() required = true
|
@Input() required = true
|
||||||
|
@ -184,12 +199,16 @@ export class CreChipComboBoxComponent extends CreChipInputComponent implements O
|
||||||
selector: 'cre-checkbox-input',
|
selector: 'cre-checkbox-input',
|
||||||
templateUrl: 'checkbox.html'
|
templateUrl: 'checkbox.html'
|
||||||
})
|
})
|
||||||
export class CreCheckboxInputComponent {
|
export class CreCheckboxInputComponent implements OnInit {
|
||||||
@Input() label: string
|
@Input() label: string
|
||||||
@Input() control: FormControl
|
@Input() control: AbstractControl
|
||||||
@Input() checked: boolean
|
@Input() checked: boolean
|
||||||
|
|
||||||
@Output() checkedChange = new EventEmitter<boolean>()
|
@Output() checkedChange = new EventEmitter<boolean>()
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.control?.setValue(this.control.value === 'true')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -200,7 +219,7 @@ export class CreFileInputComponent implements OnInit {
|
||||||
@Input() label: string
|
@Input() label: string
|
||||||
@Input() icon: string
|
@Input() icon: string
|
||||||
@Input() accept = ''
|
@Input() accept = ''
|
||||||
@Input() control: FormControl | null
|
@Input() control: AbstractControl | null
|
||||||
|
|
||||||
@Output() selection = new EventEmitter<File>()
|
@Output() selection = new EventEmitter<File>()
|
||||||
@Output() invalidFormat = new EventEmitter<void>()
|
@Output() invalidFormat = new EventEmitter<void>()
|
||||||
|
@ -234,7 +253,7 @@ export class CreFileInputComponent implements OnInit {
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class CrePeriodInputComponent implements OnInit {
|
export class CrePeriodInputComponent implements OnInit {
|
||||||
@Input() control: FormControl
|
@Input() control: AbstractControl
|
||||||
@Input() label: string
|
@Input() label: string
|
||||||
@Input() hint: string | null
|
@Input() hint: string | null
|
||||||
|
|
||||||
|
@ -264,9 +283,12 @@ export class CrePeriodInputComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
private setValuesFromPeriod(period: string) {
|
private setValuesFromPeriod(period: string) {
|
||||||
|
if (!period) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const periodTypeChar = period.slice(-1)
|
const periodTypeChar = period.slice(-1)
|
||||||
period = period.slice(1, -1)
|
period = period.slice(1, -1)
|
||||||
|
|
||||||
this.selectControl.setValue(periodTypeChar)
|
this.selectControl.setValue(periodTypeChar)
|
||||||
this.inputControl.setValue(period)
|
this.inputControl.setValue(period)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@import "~src/custom-theme"
|
@import "../../../../../custom-theme"
|
||||||
|
|
||||||
cre-table
|
cre-table
|
||||||
display: block
|
display: block
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@import "../../../../../custom-theme"
|
@import "~src/variables"
|
||||||
|
|
||||||
p, labeled-icon
|
p, labeled-icon
|
||||||
margin: 0
|
margin: 0
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import {AbstractControl, Form, FormControl, Validators} from '@angular/forms'
|
||||||
|
import {filterMap} from '../utils/map.utils'
|
||||||
|
|
||||||
export class Config {
|
export class Config {
|
||||||
static readonly INSTANCE_NAME = 'instance.name'
|
static readonly INSTANCE_NAME = 'instance.name'
|
||||||
static readonly INSTANCE_LOGO_PATH = 'instance.logo.path'
|
static readonly INSTANCE_LOGO_PATH = 'instance.logo.path'
|
||||||
|
@ -16,12 +19,63 @@ export class Config {
|
||||||
static readonly JAVA_VERSION = 'env.java.version'
|
static readonly JAVA_VERSION = 'env.java.version'
|
||||||
static readonly OPERATING_SYSTEM = 'env.os'
|
static readonly OPERATING_SYSTEM = 'env.os'
|
||||||
|
|
||||||
constructor(
|
static readonly IMAGE_CONFIG_KEYS = [
|
||||||
public key: string,
|
Config.INSTANCE_LOGO_PATH,
|
||||||
public content: string,
|
Config.INSTANCE_ICON_PATH
|
||||||
public lastUpdated: string,
|
]
|
||||||
public requireRestart: boolean,
|
|
||||||
public editable: boolean
|
static readonly PASSWORD_CONFIG_KEYS = [
|
||||||
) {
|
Config.DATABASE_PASSWORD
|
||||||
}
|
]
|
||||||
|
|
||||||
|
public key: string
|
||||||
|
public requireRestart: boolean
|
||||||
|
public editable: boolean
|
||||||
|
public content?: string
|
||||||
|
public lastUpdated?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConfigKeyContent {
|
||||||
|
public key: string
|
||||||
|
public content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConfigControl {
|
||||||
|
public config: Config
|
||||||
|
public control: AbstractControl
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildFormControl(config: Config): AbstractControl {
|
||||||
|
return new FormControl({value: config.content, disabled: !config.editable}, !configKeyIsPassword(config.key) ? Validators.required : null)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function configKeyIsPassword(key: string): boolean {
|
||||||
|
return Config.PASSWORD_CONFIG_KEYS.indexOf(key) >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterConfigKeyControlMap(map: Map<string, AbstractControl>): Map<string, AbstractControl> {
|
||||||
|
return filterMap(map, (key, control) => {
|
||||||
|
return control.dirty &&
|
||||||
|
Config.IMAGE_CONFIG_KEYS.indexOf(key) < 0 && // Filter image configs because they are sent to a different endpoint
|
||||||
|
control.value !== undefined &&
|
||||||
|
control.value !== null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterImageConfigKeyControlMap(map: Map<string, AbstractControl>): Map<string, AbstractControl> {
|
||||||
|
return filterMap(map, (key, control) => {
|
||||||
|
return Config.IMAGE_CONFIG_KEYS.indexOf(key) >= 0 && control.dirty
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToConfigKeyContent(key: string, control: AbstractControl): ConfigKeyContent {
|
||||||
|
return {key, content: control.value}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToConfigKeyContentArray(map: Map<string, AbstractControl>): ConfigKeyContent[] {
|
||||||
|
const array: ConfigKeyContent[] = []
|
||||||
|
map.forEach((control, key) => {
|
||||||
|
array.push(mapToConfigKeyContent(key, control))
|
||||||
|
})
|
||||||
|
return array
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
import {Injectable} from '@angular/core'
|
import {Injectable} from '@angular/core'
|
||||||
import {Config} from '../model/config.model'
|
import {Config, filterConfigKeyControlMap, filterImageConfigKeyControlMap, mapToConfigKeyContentArray} from '../model/config.model'
|
||||||
import {Observable} from 'rxjs'
|
import {Observable} from 'rxjs'
|
||||||
import {ApiService} from './api.service'
|
import {ApiService} from './api.service'
|
||||||
import {FormControl} from '@angular/forms'
|
import {AbstractControl, FormGroup} from '@angular/forms'
|
||||||
|
import {transformMap} from '../utils/map.utils'
|
||||||
const imageConfigsKeys = [
|
|
||||||
Config.INSTANCE_LOGO_PATH,
|
|
||||||
Config.INSTANCE_ICON_PATH
|
|
||||||
]
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
@ -18,32 +14,27 @@ export class ConfigService {
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get all(): Observable<Config[]> {
|
||||||
|
return this.api.get<Config[]>('/config')
|
||||||
|
}
|
||||||
|
|
||||||
get(key: string): Observable<Config> {
|
get(key: string): Observable<Config> {
|
||||||
return this.api.get<Config>(`/config/${key}`)
|
return this.api.get<Config>(`/config/${key}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
set(configs: Map<string, FormControl>): Observable<void> {
|
setFromForm(form: FormGroup): Observable<void> {
|
||||||
const body = []
|
const map = new Map<string, AbstractControl>()
|
||||||
for (let key in configs) {
|
for (let key in form.controls) {
|
||||||
const control = configs[key]
|
map.set(key, form.controls[key])
|
||||||
if (control.dirty && key.indexOf('path') < 0) {
|
|
||||||
body.push({key, content: control.value})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return this.set(map);
|
||||||
|
}
|
||||||
|
|
||||||
const subscriptions = []
|
set(configs: Map<string, AbstractControl>): Observable<void> {
|
||||||
imageConfigsKeys.forEach(key => {
|
const body = mapToConfigKeyContentArray(filterConfigKeyControlMap(configs))
|
||||||
if (configs[key].dirty) {
|
const imageConfigs = filterImageConfigKeyControlMap(configs)
|
||||||
subscriptions.push(this.setImage(key, configs[key].value))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
while (subscriptions.length > 0) {
|
|
||||||
const subscription = subscriptions.pop().subscribe({
|
|
||||||
next: () => subscription.unsubscribe()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
this.setImages(imageConfigs)
|
||||||
return this.api.put<void>('/config', body)
|
return this.api.put<void>('/config', body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,4 +49,19 @@ export class ConfigService {
|
||||||
restart(): Observable<void> {
|
restart(): Observable<void> {
|
||||||
return this.api.post<void>('/config/restart')
|
return this.api.post<void>('/config/restart')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setImages(configs: Map<string, AbstractControl>) {
|
||||||
|
const subscriptions = this.getImageConfigsSubscriptions(configs)
|
||||||
|
while (subscriptions.length > 0) {
|
||||||
|
const subscription = subscriptions.pop().subscribe({
|
||||||
|
next: () => subscription.unsubscribe()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getImageConfigsSubscriptions(configs: Map<string, AbstractControl>): Observable<void>[] {
|
||||||
|
return transformMap(configs, (key, control) => {
|
||||||
|
return this.setImage(key, control.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ import {InfoBannerModule} from './components/info-banner/info-banner.module'
|
||||||
import {CreFormsModule} from './components/forms/forms.module'
|
import {CreFormsModule} from './components/forms/forms.module'
|
||||||
import {VarDirective} from './directives/var.directive'
|
import {VarDirective} from './directives/var.directive'
|
||||||
import {CreColorPreview} from './components/color-preview/color-preview'
|
import {CreColorPreview} from './components/color-preview/color-preview'
|
||||||
|
import {CreDialogsModule} from './components/dialogs/dialogs.module'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [VarDirective, HeaderComponent, UserMenuComponent, LabeledIconComponent, ConfirmBoxComponent, PermissionsListComponent, PermissionsFieldComponent, NavComponent, EntityListComponent, EntityAddComponent, EntityEditComponent, FileButtonComponent, GlobalAlertHandlerComponent, SliderFieldComponent, LoadingWheelComponent, CreColorPreview],
|
declarations: [VarDirective, HeaderComponent, UserMenuComponent, LabeledIconComponent, ConfirmBoxComponent, PermissionsListComponent, PermissionsFieldComponent, NavComponent, EntityListComponent, EntityAddComponent, EntityEditComponent, FileButtonComponent, GlobalAlertHandlerComponent, SliderFieldComponent, LoadingWheelComponent, CreColorPreview],
|
||||||
|
@ -71,7 +72,8 @@ import {CreColorPreview} from './components/color-preview/color-preview'
|
||||||
InfoBannerModule,
|
InfoBannerModule,
|
||||||
CreFormsModule,
|
CreFormsModule,
|
||||||
VarDirective,
|
VarDirective,
|
||||||
CreColorPreview
|
CreColorPreview,
|
||||||
|
CreDialogsModule
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
MatTabsModule,
|
MatTabsModule,
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
export function filterMap<K, V>(map: Map<K, V>, predicate: (key: K, value: V) => boolean): Map<K, V> {
|
||||||
|
const filteredMap = new Map<K, V>()
|
||||||
|
map.forEach((value, key) => {
|
||||||
|
if (predicate(key, value)) {
|
||||||
|
filteredMap.set(key, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return filteredMap
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformMap<K, V, T>(map: Map<K, V>, transform: (key: K, value: V) => T): T[] {
|
||||||
|
const transformedArray = []
|
||||||
|
map.forEach((value, key) => {
|
||||||
|
transformedArray.push(transform(key, value))
|
||||||
|
})
|
||||||
|
return transformedArray
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
@import '~src/custom-theme'
|
@import '../../../../custom-theme'
|
||||||
|
|
||||||
.touchupkit-finish-container
|
.touchupkit-finish-container
|
||||||
display: inline-block
|
display: inline-block
|
||||||
|
|
|
@ -122,10 +122,6 @@ $color-recipes-explorer-frontend-theme: mat-light-theme($theme-primary, $theme-a
|
||||||
// that you are using.
|
// that you are using.
|
||||||
@include angular-material-theme($color-recipes-explorer-frontend-theme);
|
@include angular-material-theme($color-recipes-explorer-frontend-theme);
|
||||||
|
|
||||||
$color-primary: map-get($theme-primary, 500);
|
|
||||||
$color-accent: map-get($theme-accent, 500);
|
|
||||||
$color-warn: map-get($theme-error, 500);
|
|
||||||
|
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
@import 'assets/sass/modules/_fonts.sass'
|
@import "variables"
|
||||||
@import "custom-theme"
|
|
||||||
@import "~material-design-icons/iconfont/material-icons.css"
|
|
||||||
|
|
||||||
mat-card
|
mat-card
|
||||||
padding: 0 !important
|
padding: 0 !important
|
||||||
|
|
Loading…
Reference in New Issue