Merge pull request 'feature/2-secure-configurations' (#2) from feature/2-secure-configurations into develop
continuous-integration/drone/push Build is passing Details

Reviewed-on: #2
This commit is contained in:
William Nolin 2021-08-09 22:16:28 -04:00
commit 18415059c6
47 changed files with 901 additions and 575 deletions

6
.dockerignore Normal file
View File

@ -0,0 +1,6 @@
**/node_modules
.gitignore
.dockerignore
Dockerfile
docker-compose.yml
package-lock.json

77
.drone.yml Normal file
View File

@ -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

View File

@ -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"

View File

@ -1,17 +1,29 @@
FROM nginx:mainline-alpine
WORKDIR /usr/bin/cre/
ARG ARTIFACT_NAME=ColorRecipesExplorer-ng
COPY $ARTIFACT_NAME.zip .
COPY nginx.conf /etc/nginx/nginx.conf
FROM alpine:latest AS build
WORKDIR /usr/src/
RUN apk update
RUN apk add --no-cache zip
RUN apk add --no-cache nodejs npm
RUN unzip $ARTIFACT_NAME.zip
RUN rm $ARTIFACT_NAME.zip
RUN npm install -g typescript@4.0.7 && \
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;"]

View File

@ -8,18 +8,18 @@ services:
MYSQL_ROOT_PASSWORD: "pass"
MYSQL_DATABASE: "cre"
ports:
- 3306:3306
- "3306:3306"
backend:
image: fyloz.dev:5443/color-recipes-explorer/backend:master
image: registry.fyloz.dev:5443/colorrecipesexplorer/backend:latest
environment:
spring_profiles_active: "mysql,debug"
cre_database_url: "mysql://database:3306/cre"
cre_database_username: "root"
cre_database_password: "pass"
CRE_ENABLE_DB_UPDATE: 0
CRE_ENABLE_DB_UPDATE: 1
server_port: 9090
ports:
- 9090:9090
- "9090:9090"
volumes:
- cre_data:/usr/bin/cre/data
- cre_config:/usr/bin/cre/config

View File

@ -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 . .

View File

@ -5,7 +5,7 @@ events { worker_connections 1024; }
http {
server {
listen 80;
root /usr/bin/cre/dist/color-recipes-explorer-frontend;
root /usr/bin/;
include /etc/nginx/mime.types;
location / {

20
src/_variables.scss Normal file
View File

@ -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);

View File

@ -3,8 +3,7 @@ import {Routes, RouterModule} from '@angular/router'
import {CatalogComponent} from './pages/catalog/catalog.component'
import {AdministrationComponent} from './pages/administration/administration.component'
import {MiscComponent} from './pages/others/misc.component'
import {CreConfigEditor} from './modules/configuration/config'
import {CreConfigEditor} from './modules/configuration/config-editor'
const routes: Routes = [{
path: 'color',

View File

@ -1,4 +1,4 @@
@import '../../../../../custom-theme'
@import "~src/variables"
mat-expansion-panel
width: 48rem

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,6 @@
<div
class="cre-config"
[class.cre-readonly-config]="readOnly"
[attr.title]="tooltip">
<ng-content></ng-content>
</div>

View File

@ -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>

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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>

View File

@ -1,8 +1,6 @@
<mat-card class="w-50 x-centered">
<mat-card-header>
<mat-card-title>
<ng-content select="cre-config-label"></ng-content>
</mat-card-title>
<mat-card-title>{{label}}</mat-card-title>
</mat-card-header>
<mat-card-content [class.no-action]="!hasActions">
<ng-content select="cre-config-list"></ng-content>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,32 +1,35 @@
import {NgModule} from '@angular/core'
import {
CreConfig,
CreConfigLabel,
CreConfigEditor,
CreConfigSection,
CreImageConfig,
CreConfigList,
CreBoolConfig,
CreConfigActions,
CreConfigTooltip, CrePeriodConfig, CreBoolConfig, CreDateConfig
CreConfigContainer,
CreConfigList,
CreConfigSection,
CreDateConfig,
CreImageConfig,
CrePeriodConfig,
CreSecureConfig,
CreTextConfig
} from './config'
import {SharedModule} from '../shared/shared.module'
import {CreInputsModule} from '../shared/components/inputs/inputs.module'
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module'
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
import {CreConfigEditor} from './config-editor'
@NgModule({
declarations: [
CreConfigLabel,
CreConfigTooltip,
CreConfigEditor,
CreConfig,
CreImageConfig,
CreConfigSection,
CreConfigList,
CreConfigActions,
CreConfigSection,
CreConfigContainer,
CreTextConfig,
CreImageConfig,
CreBoolConfig,
CrePeriodConfig,
CreDateConfig
CreDateConfig,
CreSecureConfig,
CreConfigEditor
],
imports: [
SharedModule,
@ -35,4 +38,5 @@ import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
CreButtonsModule
]
})
export class ConfigModule { }
export class ConfigModule {
}

View File

@ -1,10 +1,10 @@
mat-hint
font-size: .8em
cre-config
cre-config-container
display: block
cre-input.has-hint
.cre-config:not(.cre-editable-config)
margin-bottom: 1em
mat-hint

View File

@ -1,56 +1,12 @@
import {
AfterViewInit,
Component,
ContentChild,
Directive,
ElementRef,
EventEmitter,
Input,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core'
import {AfterViewInit, Component, ContentChild, Directive, EventEmitter, Input, Output, ViewChild, ViewEncapsulation} from '@angular/core'
import {ConfigService} from '../shared/service/config.service'
import {Config} from '../shared/model/config.model'
import {ErrorHandlingComponent, SubscribingComponent} from '../shared/components/subscribing.component'
import {Config, ConfigControl} from '../shared/model/config.model'
import {SubscribingComponent} from '../shared/components/subscribing.component'
import {ErrorService} from '../shared/service/error.service'
import {ActivatedRoute, Router} from '@angular/router'
import {formatDate, formatDateTime, getFileUrl, readFile} from '../shared/utils/utils'
import {FormControl, Validators} from '@angular/forms'
import {ConfirmBoxComponent} from '../shared/components/confirm-box/confirm-box.component'
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
}
}
import {AbstractControl} from '@angular/forms'
import {CrePromptDialog} from '../shared/components/dialogs/dialogs'
@Directive({
selector: 'cre-config-list'
@ -70,6 +26,8 @@ export class CreConfigActions {
templateUrl: 'config-section.html'
})
export class CreConfigSection {
@Input() label: string
@ContentChild(CreConfigActions) actions: CreConfigActions
get hasActions(): boolean {
@ -78,17 +36,25 @@ export class CreConfigSection {
}
@Component({
selector: 'cre-config',
templateUrl: 'config.html',
styleUrls: ['config.sass']
selector: 'cre-config-container',
templateUrl: 'config-container.html',
styleUrls: ['config.sass'],
encapsulation: ViewEncapsulation.None
})
export class CreConfig extends SubscribingComponent {
@Input() config: { key: string, control: FormControl }
export class CreConfigContainer {
@Input() configuration?: Config
@Input() tooltip: string
@ContentChild(CreConfigLabel, {static: true}) label: CreConfigLabel
@ContentChild(CreConfigTooltip, {static: true}) tooltip: CreConfigTooltip
get readOnly(): boolean {
return !this.configuration?.editable ?? true
}
}
configuration: Config | null
@Directive()
abstract class _CreConfigBase extends SubscribingComponent {
@Input() configControl: ConfigControl
@Input() label: string
@Input() tooltip?: string
constructor(
private configService: ConfigService,
@ -101,39 +67,105 @@ export class CreConfig extends SubscribingComponent {
ngOnInit() {
super.ngOnInit()
this.subscribe(
this.configService.get(this.config.key),
config => this.setConfig(config)
)
}
setConfig(config: Config) {
this.configuration = config
this.config.control.setValue(config.content)
if (!config.editable) {
this.config.control.disable()
}
get config(): Config {
return this.configControl.config
}
get control(): AbstractControl {
return this.configControl.control
}
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({
selector: 'cre-image-config',
templateUrl: 'image.html',
templateUrl: 'config-image.html',
styleUrls: ['config.sass'],
encapsulation: ViewEncapsulation.None
})
export class CreImageConfig extends CreConfig {
export class CreImageConfig extends _CreConfigBase {
@Input() previewWidth: string | null
@Output() invalidFormat = new EventEmitter<void>()
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(
configService: ConfigService,
errorService: ErrorService,
@ -143,113 +175,12 @@ export class CreImageConfig extends CreConfig {
super(configService, errorService, activatedRoute, router)
}
updateImage(file: File): any {
readFile(file, (content) => this.updatedImage = content)
onOpen() {
this.initialValue = this.control.value
this.dialog.show()
}
get configuredImageUrl(): string {
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()
onCancel() {
this.control.setValue(this.initialValue)
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,6 @@
cre-button, cre-primary-button, cre-accent-button, cre-warn-button
display: inline-block
width: inherit
button
width: 100%

View File

@ -1,51 +1,60 @@
import {Component, Input} from '@angular/core'
import {Component, Input, ViewEncapsulation} from '@angular/core'
import {ThemePalette} from '@angular/material/core'
@Component({
selector: 'cre-button',
template: `
<button mat-raised-button [color]="color" [disabled]="disabled">
<button mat-raised-button [type]="type" [color]="color" [disabled]="disabled">
<ng-content></ng-content>
</button>
`
`,
styleUrls: ['buttons.sass'],
encapsulation: ViewEncapsulation.None
})
export class CreButtonComponent {
@Input() color: ThemePalette
@Input() type = 'button'
@Input() disabled = false
}
@Component({
selector: 'cre-primary-button',
template: `
<cre-button color="primary" [disabled]="disabled">
<cre-button color="primary" [type]="type" [disabled]="disabled">
<ng-content></ng-content>
</cre-button>
`
`,
styleUrls: ['buttons.sass']
})
export class CrePrimaryButtonComponent {
@Input() type = 'button'
@Input() disabled = false
}
@Component({
selector: 'cre-accent-button',
template: `
<cre-button color="accent" [disabled]="disabled">
<cre-button color="accent" [type]="type" [disabled]="disabled">
<ng-content></ng-content>
</cre-button>
`
`,
styleUrls: ['buttons.sass']
})
export class CreAccentButtonComponent {
@Input() type = 'button'
@Input() disabled = false
}
@Component({
selector: 'cre-warn-button',
template: `
<cre-button color="warn" [disabled]="disabled">
<cre-button color="warn" [type]="type" [disabled]="disabled">
<ng-content></ng-content>
</cre-button>
`
`,
styleUrls: ['buttons.sass']
})
export class CreWarnButtonComponent {
@Input() type = 'button'
@Input() disabled = false
}

View File

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

View File

@ -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;
}
}
}

View File

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

View File

@ -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>

View File

@ -1,4 +1,4 @@
@import "~src/custom-theme"
@import "~src/variables"
.info-banner-wrapper
background-color: $color-primary

View File

@ -1,25 +1,21 @@
<mat-form-field>
<mat-label>{{label}}</mat-label>
<input
*ngIf="!control"
matInput
[type]="type"
[step]="step"
[placeholder]="placeholder"
[(ngModel)]="value"
[required]="required"
[autocomplete]="autocomplete ? 'on' : 'off'"
[disabled]="disabled"
(change)="valueChange.emit(value)"/>
<input
*ngIf="control"
matInput
[type]="type"
[step]="step"
[placeholder]="placeholder"
[required]="required"
[formControl]="control"
[autocomplete]="autocomplete ? 'on' : 'off'"/>
<ng-container *ngIf="!control">
<input
#input
matInput
[disabled]="disabled"
[(ngModel)]="value"
(change)="valueChange.emit(value)"/>
</ng-container>
<ng-container *ngIf="control">
<input
#input
matInput
[formControl]="control">
</ng-container>
<mat-icon
matSuffix
[svgIcon]="icon"

View File

@ -1,6 +1,8 @@
import {
AfterViewInit,
Component,
ContentChild,
Directive,
ElementRef,
EventEmitter,
Input,
@ -17,7 +19,14 @@ import {Observable, Subject} from 'rxjs'
import {map, takeUntil} from 'rxjs/operators'
import {MatChipInputEvent} from '@angular/material/chips'
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({
selector: 'cre-input',
@ -25,24 +34,30 @@ import {MatOptionSelectionChange} from '@angular/material/core'
encapsulation: ViewEncapsulation.None,
styleUrls: ['input.sass']
})
export class CreInputComponent {
@Input() control: FormControl | null
export class CreInputComponent extends _CreInputBase implements AfterViewInit {
@Input() type = 'text'
@Input() label: string
@Input() icon: string
@Input() iconTitle: string | null
@Input() required = true
@Input() autocomplete = true
@Input() placeholder: string | null
@Input() step = 1
@Input() value
@Input() iconColor: string = 'black'
@Input() disabled = false
@Input() hint: string | null
@Output() valueChange = new EventEmitter<any>()
@ViewChild('input') input: 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({
@ -51,7 +66,7 @@ export class CreInputComponent {
encapsulation: ViewEncapsulation.None
})
export class CreAutocompleteInputComponent {
@Input() control: FormControl | null
@Input() control: AbstractControl | null
@Input() label: string
@Input() icon: string
@Input() required = true
@ -69,7 +84,7 @@ export class CreAutocompleteInputComponent {
encapsulation: ViewEncapsulation.None
})
export class CreChipInputComponent implements OnInit {
@Input() control: FormControl
@Input() control: AbstractControl
@Input() label: string
@Input() icon: string
@Input() required = true
@ -118,7 +133,7 @@ export class CreChipInputComponent implements OnInit {
encapsulation: ViewEncapsulation.None
})
export class CreComboBoxComponent {
@Input() control: FormControl
@Input() control: AbstractControl
@Input() label: string
@Input() icon: string
@Input() required = true
@ -184,12 +199,16 @@ export class CreChipComboBoxComponent extends CreChipInputComponent implements O
selector: 'cre-checkbox-input',
templateUrl: 'checkbox.html'
})
export class CreCheckboxInputComponent {
export class CreCheckboxInputComponent implements OnInit {
@Input() label: string
@Input() control: FormControl
@Input() control: AbstractControl
@Input() checked: boolean
@Output() checkedChange = new EventEmitter<boolean>()
ngOnInit(): void {
this.control?.setValue(this.control.value === 'true')
}
}
@Component({
@ -200,7 +219,7 @@ export class CreFileInputComponent implements OnInit {
@Input() label: string
@Input() icon: string
@Input() accept = ''
@Input() control: FormControl | null
@Input() control: AbstractControl | null
@Output() selection = new EventEmitter<File>()
@Output() invalidFormat = new EventEmitter<void>()
@ -234,7 +253,7 @@ export class CreFileInputComponent implements OnInit {
encapsulation: ViewEncapsulation.None
})
export class CrePeriodInputComponent implements OnInit {
@Input() control: FormControl
@Input() control: AbstractControl
@Input() label: string
@Input() hint: string | null
@ -264,9 +283,12 @@ export class CrePeriodInputComponent implements OnInit {
}
private setValuesFromPeriod(period: string) {
if (!period) {
return
}
const periodTypeChar = period.slice(-1)
period = period.slice(1, -1)
this.selectControl.setValue(periodTypeChar)
this.inputControl.setValue(period)
}

View File

@ -1,4 +1,4 @@
@import "~src/custom-theme"
@import "../../../../../custom-theme"
cre-table
display: block

View File

@ -1,4 +1,4 @@
@import "../../../../../custom-theme"
@import "~src/variables"
p, labeled-icon
margin: 0

View File

@ -1,3 +1,6 @@
import {AbstractControl, Form, FormControl, Validators} from '@angular/forms'
import {filterMap} from '../utils/map.utils'
export class Config {
static readonly INSTANCE_NAME = 'instance.name'
static readonly INSTANCE_LOGO_PATH = 'instance.logo.path'
@ -16,12 +19,63 @@ export class Config {
static readonly JAVA_VERSION = 'env.java.version'
static readonly OPERATING_SYSTEM = 'env.os'
constructor(
public key: string,
public content: string,
public lastUpdated: string,
public requireRestart: boolean,
public editable: boolean
) {
}
static readonly IMAGE_CONFIG_KEYS = [
Config.INSTANCE_LOGO_PATH,
Config.INSTANCE_ICON_PATH
]
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
}

View File

@ -1,13 +1,9 @@
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 {ApiService} from './api.service'
import {FormControl} from '@angular/forms'
const imageConfigsKeys = [
Config.INSTANCE_LOGO_PATH,
Config.INSTANCE_ICON_PATH
]
import {AbstractControl, FormGroup} from '@angular/forms'
import {transformMap} from '../utils/map.utils'
@Injectable({
providedIn: 'root'
@ -18,32 +14,27 @@ export class ConfigService {
) {
}
get all(): Observable<Config[]> {
return this.api.get<Config[]>('/config')
}
get(key: string): Observable<Config> {
return this.api.get<Config>(`/config/${key}`)
}
set(configs: Map<string, FormControl>): Observable<void> {
const body = []
for (let key in configs) {
const control = configs[key]
if (control.dirty && key.indexOf('path') < 0) {
body.push({key, content: control.value})
}
setFromForm(form: FormGroup): Observable<void> {
const map = new Map<string, AbstractControl>()
for (let key in form.controls) {
map.set(key, form.controls[key])
}
return this.set(map);
}
const subscriptions = []
imageConfigsKeys.forEach(key => {
if (configs[key].dirty) {
subscriptions.push(this.setImage(key, configs[key].value))
}
})
while (subscriptions.length > 0) {
const subscription = subscriptions.pop().subscribe({
next: () => subscription.unsubscribe()
})
}
set(configs: Map<string, AbstractControl>): Observable<void> {
const body = mapToConfigKeyContentArray(filterConfigKeyControlMap(configs))
const imageConfigs = filterImageConfigKeyControlMap(configs)
this.setImages(imageConfigs)
return this.api.put<void>('/config', body)
}
@ -58,4 +49,19 @@ export class ConfigService {
restart(): Observable<void> {
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)
})
}
}

View File

@ -36,6 +36,7 @@ import {InfoBannerModule} from './components/info-banner/info-banner.module'
import {CreFormsModule} from './components/forms/forms.module'
import {VarDirective} from './directives/var.directive'
import {CreColorPreview} from './components/color-preview/color-preview'
import {CreDialogsModule} from './components/dialogs/dialogs.module'
@NgModule({
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,
CreFormsModule,
VarDirective,
CreColorPreview
CreColorPreview,
CreDialogsModule
],
imports: [
MatTabsModule,

View File

@ -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
}

View File

@ -1,4 +1,4 @@
@import '~src/custom-theme'
@import '../../../../custom-theme'
.touchupkit-finish-container
display: inline-block

View File

@ -122,10 +122,6 @@ $color-recipes-explorer-frontend-theme: mat-light-theme($theme-primary, $theme-a
// that you are using.
@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 {
height: 100%;

View File

@ -1,6 +1,4 @@
@import 'assets/sass/modules/_fonts.sass'
@import "custom-theme"
@import "~material-design-icons/iconfont/material-icons.css"
@import "variables"
mat-card
padding: 0 !important