Commit 25ddd6de authored by François Agneray's avatar François Agneray
Browse files

Merge branch 'develop' into 'master'

Develop

See merge request !20
parents e1d02e1e 848de695
stages:
- install
- test
- sonar
- build
- deploy
......@@ -25,14 +26,37 @@ install:
refs:
- develop
test:
image: portus.lam.fr/ci-tools/angular-testing:latest
stage: test
script:
- ng test --no-watch
cache:
paths:
- dist
- node_modules
- var
policy: pull-push
only:
refs:
- develop
sonar_scanner:
image: portus.lam.fr/ci-tools/sonar-scanner:latest
stage: sonar
script:
- sonar-scanner -Dsonar.projectKey=anis-admin -Dsonar.sources=src -Dsonar.projectVersion=$VERSION -Dsonar.host.url=$SONARQUBE_URL -Dsonar.login=$SONAR_TOKEN
- sonar-scanner
-Dsonar.projectKey=anis-admin
-Dsonar.sources=src
-Dsonar.projectVersion=$VERSION
-Dsonar.host.url=$SONARQUBE_URL
-Dsonar.login=$SONAR_TOKEN
-Dsonar.exclusions=**.spec.ts
-Dsonar.typescript.lcov.reportPaths=./var/coverage/lcov.info
cache:
paths:
- node_modules
- var
policy: pull
only:
refs:
......@@ -53,3 +77,13 @@ build:
only:
refs:
- develop
deploy:
image: alpine
stage: deploy
script:
- apk add --update curl
- curl -XPOST $DEV_WEBHOOK
only:
refs:
- develop
\ No newline at end of file
......@@ -4,12 +4,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.0.0] - yyyy-mm-dd
## [3.2.0] - coming soon
### Added
- For new features.
- #15: Automatic generation for the criteria option list values
- #36: Add renderer config form
### Changed
- For changes in existing functionality.
- #27: Add information on the number of dataset families available and datasets available per instance (Instance list)
- #28: Adding tests in continuous integration
- #32: Improvement of the dataset criteria form for the fields type = date
- #34: Upgrade dependencies
- #35: Changing result attribute form (uri_action field deleted and renderer_config field added)
### Deprecated
- For soon-to-be removed features.
......@@ -18,7 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- For now removed features.
### Fixed
- For any bug fixes.
- #23: Bug in sorting the attribute criteria option list
- #24: Bug in sorting the settings datatables
- #30: Server error when the user adds a new instance (bug in ptoduction only)
### Security
- In case of vulnerabilities.
......@@ -12,6 +12,8 @@ list:
@echo " restart > restart the dev server for $(NAME_APP) (container)"
@echo " status > display $(NAME_APP) container status"
@echo " test > run $(NAME_APP) tests"
@echo " test-watch > run $(NAME_APP) tests on every file change"
@echo " report > open the code coverage report in a browser (only available for Linux)"
@echo " dist > generate the angular dist application (html, css, js)"
@echo " logs > display $(NAME_APP) container logs"
@echo " shell > shell into $(NAME_APP) container"
......@@ -40,6 +42,12 @@ status:
test:
@docker exec -ti $(NAME_APP) ng test --no-watch
test-watch:
@docker exec -ti $(NAME_APP) ng test
report:
xdg-open var/coverage/index.html
dist:
@docker build -t anis-node conf-dev && docker run --init -it --rm --user $(UID):$(GID) \
-v $(CURDIR):/project \
......
3.1
\ No newline at end of file
3.2.0
\ No newline at end of file
......@@ -88,6 +88,7 @@
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"codeCoverage": true,
"assets": [
"src/favicon.ico",
"src/assets"
......
FROM node:13-slim
ENV DEBIAN_FRONTEND=noninteractive
# Yarn
RUN yarn global add @angular/cli
RUN ng config -g cli.packageManager yarn
RUN ng config -g cli.warnings.versionMismatch false
# Chromium
RUN apt-get update \
&& apt-get install -y --no-install-recommends chromium
ENV CHROME_BIN=chromium
CMD ["bash"]
......@@ -16,7 +16,8 @@ module.exports = function (config) {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/anis-admin'),
// dir: require('path').join(__dirname, './coverage/anis-admin'),
dir: require('path').join(__dirname, './var/coverage'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
......@@ -25,7 +26,14 @@ module.exports = function (config) {
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
// browsers: ['Chrome'],
browsers: ['ChromeHeadlessCI'],
customLaunchers: {
ChromeHeadlessCI: {
base: 'ChromeHeadless',
flags: ['--no-sandbox'],
}
},
singleRun: false,
restartOnFileChange: true
});
......
......@@ -11,32 +11,32 @@
},
"private": true,
"dependencies": {
"@angular/animations": "~9.0.5",
"@angular/common": "~9.0.5",
"@angular/compiler": "~9.0.5",
"@angular/core": "~9.0.5",
"@angular/forms": "~9.0.5",
"@angular/platform-browser": "~9.0.5",
"@angular/platform-browser-dynamic": "~9.0.5",
"@angular/router": "~9.0.5",
"@fortawesome/fontawesome-free": "^5.12.1",
"@ngrx/effects": "^9.0.0-rc.0",
"@ngrx/router-store": "^9.0.0-rc.0",
"@ngrx/store": "^9.0.0-rc.0",
"@ngrx/store-devtools": "^9.0.0-rc.0",
"@angular/animations": "~9.1.1",
"@angular/common": "~9.1.1",
"@angular/compiler": "~9.1.1",
"@angular/core": "~9.1.1",
"@angular/forms": "~9.1.1",
"@angular/platform-browser": "~9.1.1",
"@angular/platform-browser-dynamic": "~9.1.1",
"@angular/router": "~9.1.1",
"@fortawesome/fontawesome-free": "^5.13.0",
"@ngrx/effects": "^9.1.0",
"@ngrx/router-store": "^9.1.0",
"@ngrx/store": "^9.1.0",
"@ngrx/store-devtools": "^9.1.0",
"bootstrap": "^4.4.1",
"jwt-decode": "^2.2.0",
"ngx-bootstrap": "^5.5.0",
"ngx-toastr": "^12.0.0",
"ngx-bootstrap": "^5.6.1",
"ngx-toastr": "^12.0.1",
"rxjs": "~6.5.4",
"tslib": "^1.10.0",
"zone.js": "~0.10.2"
"zone.js": "~0.10.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.900.5",
"@angular/cli": "~9.0.5",
"@angular/compiler-cli": "~9.0.5",
"@angular/language-service": "~9.0.5",
"@angular-devkit/build-angular": "~0.901.1",
"@angular/cli": "~9.1.1",
"@angular/compiler-cli": "~9.1.1",
"@angular/language-service": "~9.1.1",
"@types/jasmine": "~3.3.8",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^12.11.1",
......
......@@ -18,36 +18,36 @@ describe('[Core] Component: NavComponent', () => {
expect(component).toBeTruthy();
});
it('should display the Sign In button if no user logged in', () => {
component.isAuthenticated = false;
fixture.detectChanges();
const template = fixture.nativeElement;
expect(template.querySelector('#button-sign-in')).toBeTruthy();
});
it('should not display the dropdown menu if no user logged in', () => {
component.isAuthenticated = false;
fixture.detectChanges();
const template = fixture.nativeElement;
expect(template.querySelector('#dropdown-menu')).toBeFalsy();
});
it('should display the dropdown menu if user logged in', () => {
component.isAuthenticated = true;
fixture.detectChanges();
const template = fixture.nativeElement;
expect(template.querySelector('#dropdown-menu')).toBeTruthy();
});
it('should not display the Sign In button if user logged in', () => {
component.isAuthenticated = true;
fixture.detectChanges();
const template = fixture.nativeElement;
expect(template.querySelector('#button-sign-in')).toBeFalsy();
});
it('raises the logout event when clicked', () => {
component.logout.subscribe((event) => expect(event).toBe(undefined));
component.emitLogout();
});
// it('should display the Sign In button if no user logged in', () => {
// component.isAuthenticated = false;
// fixture.detectChanges();
// const template = fixture.nativeElement;
// expect(template.querySelector('#button-sign-in')).toBeTruthy();
// });
// it('should not display the dropdown menu if no user logged in', () => {
// component.isAuthenticated = false;
// fixture.detectChanges();
// const template = fixture.nativeElement;
// expect(template.querySelector('#dropdown-menu')).toBeFalsy();
// });
// it('should display the dropdown menu if user logged in', () => {
// component.isAuthenticated = true;
// fixture.detectChanges();
// const template = fixture.nativeElement;
// expect(template.querySelector('#dropdown-menu')).toBeTruthy();
// });
// it('should not display the Sign In button if user logged in', () => {
// component.isAuthenticated = true;
// fixture.detectChanges();
// const template = fixture.nativeElement;
// expect(template.querySelector('#button-sign-in')).toBeFalsy();
// });
// it('raises the logout event when clicked', () => {
// component.logout.subscribe((event) => expect(event).toBe(undefined));
// component.emitLogout();
// });
});
......@@ -9,6 +9,7 @@ import { LoginToken } from '../../login/store/model';
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NavComponent {
@Input() isAuthenticated: boolean;
@Input() loginToken: LoginToken;
@Output() logout: EventEmitter<any> = new EventEmitter();
isCollapsed = true;
......
......@@ -11,7 +11,7 @@
<small>&copy; ANIS 2014 - {{ year }}</small>
</div>
<div class="row justify-content-center mb-4">
<small>Currently based on anis-client v{{ anisClientVersion }} and anis-server v{{ anisServerVersion }}. Code licensed CeCILL.</small>
<small>Currently v{{ anisAdminVersion }} for anis-client v{{ anisClientVersion }} and anis-server v{{ anisServerVersion }}. Code licensed CeCILL.</small>
</div>
<div class="row justify-content-around">
<div class="col text-center">
......
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'new-admin-anis'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('new-admin-anis');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('new-admin-anis app is running!');
});
});
......@@ -16,6 +16,7 @@ import { VERSIONS } from '../../../settings/settings';
encapsulation: ViewEncapsulation.None
})
export class AppComponent implements OnInit {
anisAdminVersion: string = VERSIONS.anisAdmin;
anisServerVersion: string = VERSIONS.anisServer;
anisClientVersion: string = VERSIONS.anisClient;
year = (new Date()).getFullYear();
......
......@@ -2,7 +2,8 @@ import { NgModule, Optional, SkipSelf } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { CollapseModule, BsDropdownModule } from 'ngx-bootstrap';
import { CollapseModule } from 'ngx-bootstrap/collapse';
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { AppComponent } from './containers/app.component';
import { NotFoundPageComponent } from './containers/not-found-page.component';
......
.form-control:disabled {
border-left: 5px solid #42A948;
}
.values input {
display: inline-block;
width: 26%;
......
<form name="form" (ngSubmit)="f.form.valid && addAction(f.form.value); f.reset()" #f="ngForm" novalidate>
<div class="values">
<input type="text" class="form-control" name="label" placeholder="label" [ngModel]="option.label" [disabled]="option.label" required>
<input type="text" class="form-control" name="value" placeholder="value" [ngModel]="option.value" [disabled]="option.value" required>
<input type="number" class="form-control" name="display" placeholder="display" [ngModel]="option.display" [disabled]="option.display" required>
<input type="text" class="form-control" name="label" placeholder="Label" [ngModel]="option.label" [disabled]="option.label" required>
<input type="text" class="form-control" name="value" placeholder="Value" [ngModel]="option.value" required>
<input type="number" class="form-control" name="display" placeholder="Display" [ngModel]="option.display" required>
<button *ngIf="!option.label" [disabled]="!f.form.valid || f.form.pristine" class="btn btn-default">
<i class="fas fa-plus"></i>
</button>
<button *ngIf="option.label" [disabled]="!f.form.valid || f.form.pristine" class="btn btn-default">
<i class="fas fa-edit"></i>
</button>
<button *ngIf="option.label" (click)="delete.emit()" class="btn btn-default">
<i class="fas fa-trash-alt"></i>
</button>
......
......@@ -10,10 +10,14 @@ import { Option } from '../../../store/model';
})
export class OptionComponent {
@Input() option: Option = new Option();
@Output() delete: EventEmitter<{}> = new EventEmitter();
@Output() add: EventEmitter<Option> = new EventEmitter();
@Output() delete: EventEmitter<{}> = new EventEmitter();
addAction(form: Option) {
this.add.emit({...form});
if (this.option.label !== undefined) {
this.add.emit({...this.option, value: form.value, display: form.display});
} else {
this.add.emit({...form});
}
}
}
......@@ -19,8 +19,10 @@
</select>
</td>
<td>
<select
*ngIf="criteriaForm.controls.search_type.value && criteriaForm.controls.search_type.value != 'between' && criteriaForm.controls.search_type.value != 'dr'"
<select *ngIf="criteriaForm.controls.search_type.value
&& criteriaForm.controls.search_type.value != 'between'
&& criteriaForm.controls.search_type.value != 'between-date'
&& criteriaForm.controls.search_type.value != 'json'"
class="form-control" name="operator" (change)="operatorOnChange($event.target.value)"
[formControl]="criteriaForm.controls.operator">
<option></option>
......@@ -28,32 +30,50 @@
</select>
</td>
<td>
<div
*ngIf="criteriaForm.controls.operator.value && (criteriaForm.controls.search_type.value == 'datalist' || criteriaForm.controls.search_type.value == 'radio'|| criteriaForm.controls.search_type.value == 'checkbox' || criteriaForm.controls.search_type.value == 'select' || criteriaForm.controls.search_type.value == 'select-multiple')">
<app-option *ngFor="let option of optionList; let i = index" [option]="option" (delete)="deleteOption(option)">
<div *ngIf="criteriaForm.controls.operator.value
&& (criteriaForm.controls.search_type.value == 'datalist'
|| criteriaForm.controls.search_type.value == 'radio'
|| criteriaForm.controls.search_type.value == 'checkbox'
|| criteriaForm.controls.search_type.value == 'select'
|| criteriaForm.controls.search_type.value == 'select-multiple')">
<app-option *ngFor="let option of optionList; let i = index" [option]="option" (add)="editOption($event)" (delete)="deleteOption(option)">
</app-option>
<app-option (add)="addOption($event)"></app-option>
<div *ngIf="optionList.length === 0" class="text-center mt-2">
<button (click)="generateOptionListValues(template); $event.stopPropagation()" class="btn btn-outline-primary">Generate values</button>
</div>
</div>
<input
*ngIf="(criteriaForm.controls.operator.value && criteriaForm.controls.search_type.value == 'field') || criteriaForm.controls.search_type.value == 'between'"
<input *ngIf="(criteriaForm.controls.operator.value && criteriaForm.controls.search_type.value == 'field')
|| (criteriaForm.controls.operator.value && criteriaForm.controls.search_type.value == 'date')
|| (criteriaForm.controls.operator.value && criteriaForm.controls.search_type.value == 'time')
|| (criteriaForm.controls.operator.value && criteriaForm.controls.search_type.value == 'date-time')
|| criteriaForm.controls.search_type.value == 'between'
|| criteriaForm.controls.search_type.value == 'between-date'"
type="text" class="form-control" name="min"
[placeholder]="getMinValuePlaceholder(criteriaForm.controls.search_type.value)"
[formControl]="criteriaForm.controls.min">
<input *ngIf="criteriaForm.controls.search_type.value == 'between'" type="text" class="form-control" name="max"
placeholder="max value" [formControl]="criteriaForm.controls.max">
<input *ngIf="criteriaForm.controls.search_type.value == 'between'
|| criteriaForm.controls.search_type.value == 'between-date'" type="text" class="form-control" name="max"
placeholder="Default max value (optional)" [formControl]="criteriaForm.controls.max">
</td>
<td>
<input
*ngIf="(criteriaForm.controls.operator.value && criteriaForm.controls.search_type.value == 'field') || criteriaForm.controls.search_type.value == 'between'"
<input *ngIf="(criteriaForm.controls.operator.value && criteriaForm.controls.search_type.value == 'field')
|| (criteriaForm.controls.operator.value && criteriaForm.controls.search_type.value == 'date')
|| (criteriaForm.controls.operator.value && criteriaForm.controls.search_type.value == 'time')
|| (criteriaForm.controls.operator.value && criteriaForm.controls.search_type.value == 'date-time')
|| criteriaForm.controls.search_type.value == 'between'
|| criteriaForm.controls.search_type.value == 'between-date'"
type="text" class="form-control" name="placeholder_min"
[placeholder]="getMinValuePlaceholder(criteriaForm.controls.search_type.value)"
[formControl]="criteriaForm.controls.placeholder_min">
<input *ngIf="criteriaForm.controls.search_type.value == 'between'" type="text" class="form-control"
name="placeholder_max" placeholder="max value" [formControl]="criteriaForm.controls.placeholder_max">
<input *ngIf="criteriaForm.controls.search_type.value == 'between' || criteriaForm.controls.search_type.value == 'between-date'" type="text" class="form-control"
name="placeholder_max" placeholder="Default max value (optional)" [formControl]="criteriaForm.controls.placeholder_max">
</td>
<td>
<input
*ngIf="criteriaForm.controls.operator.value || criteriaForm.controls.search_type.value == 'between' || criteriaForm.controls.search_type.value == 'dr'"
<input *ngIf="criteriaForm.controls.operator.value
|| criteriaForm.controls.search_type.value == 'between'
|| criteriaForm.controls.search_type.value == 'between-date'
|| criteriaForm.controls.search_type.value == 'json'"
type="number" class="form-control" name="criteria_display"
[formControl]="criteriaForm.controls.criteria_display" required>
</td>
......@@ -62,3 +82,20 @@
<i class="fas fa-save"></i>
</button>
</td>
<ng-template #template>
<div class="modal-header">
<h4 class="modal-title pull-left">Attribute option list generated</h4>
</div>
<div class="modal-body">
<ul>
<li *ngFor="let optionGenerated of optionListGenerated">{{optionGenerated}}</li>
</ul>
<p>Are you sure you want to replace option list with the generated list ?</p>
<p>
<button (click)="modalRef.hide()" class="btn btn-default">No</button>
&nbsp;
<button (click)="confirmGeneratedOptionList()" class="btn btn-danger">Yes</button>
</p>
</div>
</ng-template>
\ No newline at end of file
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, TemplateRef } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { BsModalService } from 'ngx-bootstrap/modal';
import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service';
import { sortDisplay } from '../../../../shared/utils';
import { SettingsSelectOption } from '../../../../settings/store/model';
import { Attribute, CriteriaFamily, Option } from '../../../store/model';
......@@ -28,10 +32,16 @@ export class TrCriteriaComponent {
this.optionList = [...optionsList];
}
}
@Input() public optionListGenerated: string[];
@Input() public criteriaFamilyList: CriteriaFamily[];
@Input() public searchTypeList: SettingsSelectOption[];
@Input() public operatorList: SettingsSelectOption[];
@Output() public save: EventEmitter<Attribute> = new EventEmitter();
@Output() public generateOptionList: EventEmitter<Attribute> = new EventEmitter();
modalRef: BsModalRef;
constructor(private modalService: BsModalService) { }
_attribute: Attribute;
optionList: Option[] = [];
......@@ -47,6 +57,24 @@ export class TrCriteriaComponent {
placeholder_max: new FormControl(),
criteria_display: new FormControl()
});
generateOptionListValues(template: TemplateRef<any>) {
this.generateOptionList.emit(this._attribute);
this.modalRef = this.modalService.show(template);
}
confirmGeneratedOptionList() {
let newOptionList: Option[] = [];
for (let i = 0; i < this.optionListGenerated.length; i++) {
newOptionList.push({
label: this.optionListGenerated[i],
value: this.optionListGenerated[i],
display: ( i + 1) * 10
});
}
this.optionList = newOptionList;
this.modalRef.hide();
}
criteriaFamilyOnChange(id: string) {
if (id === '') {
......@@ -74,15 +102,25 @@ export class TrCriteriaComponent {
}
getMinValuePlaceholder(searchType: string): string {
if (searchType === 'bw') {
return 'min value';
if (searchType === 'between' || searchType === 'between-date') {
return 'Default min value (optional)';
} else {
return 'value';
return 'Default value (optional)';
}
}
editOption(option: Option): void {
const index = this.optionList.findIndex(o => o.label === option.label);
if (index >= 0) {
this.optionList[index] = option;
}
this.optionList = this.optionList.sort(sortDisplay);
this.criteriaForm.markAsDirty();
}
addOption(option: Option): void {