Compare commits
14 Commits
master
...
release-3.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7612d524da | ||
|
|
f3697a27ef | ||
|
|
8b304a7279 | ||
|
|
d6f7122329 | ||
|
|
7312caa8d0 | ||
|
|
ac3ccb1d7d | ||
|
|
57375e2489 | ||
|
|
85c4ac984d | ||
|
|
2a9c43fc6b | ||
|
|
406f7b1fdd | ||
|
|
cb5647bc8d | ||
|
|
9819ed892b | ||
|
|
7ba2c8f3a3 | ||
|
|
d1e095ce6a |
@ -86,17 +86,13 @@ section.settings {
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
.page-header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.content-inner {
|
||||
height: calc(100% - 50px);
|
||||
}
|
||||
|
||||
.overlay-shadow {
|
||||
top: 50px;
|
||||
.right-container {
|
||||
transform: translateY(61px);
|
||||
height: calc(100% - 61px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -36,4 +36,8 @@
|
||||
&.mdc-list-item--disabled {
|
||||
color: rgba(var(--iqser-text-rgb), 0.7);
|
||||
}
|
||||
|
||||
> span {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpStatusCode } from '@angular/common/http';
|
||||
import { inject, Inject, Injectable, Optional } from '@angular/core';
|
||||
import { MonoTypeOperatorFunction, Observable, retry, throwError, timer } from 'rxjs';
|
||||
import { Inject, Injectable, Optional } from '@angular/core';
|
||||
import { finalize, MonoTypeOperatorFunction, Observable, retry, throwError, timer } from 'rxjs';
|
||||
import { catchError, tap } from 'rxjs/operators';
|
||||
import { MAX_RETRIES_ON_SERVER_ERROR, SERVER_ERROR_SKIP_PATHS } from './tokens';
|
||||
import { ErrorService } from './error.service';
|
||||
import { LoadingService } from '../loading';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { IqserConfigService } from '../services';
|
||||
|
||||
@ -49,6 +50,7 @@ export class ServerErrorInterceptor implements HttpInterceptor {
|
||||
private readonly _errorService: ErrorService,
|
||||
private readonly _keycloakService: KeycloakService,
|
||||
private readonly _configService: IqserConfigService,
|
||||
private readonly _loadingService: LoadingService,
|
||||
@Optional() @Inject(MAX_RETRIES_ON_SERVER_ERROR) private readonly _maxRetries: number,
|
||||
@Optional() @Inject(SERVER_ERROR_SKIP_PATHS) private readonly _skippedPaths: string[],
|
||||
) {}
|
||||
@ -70,7 +72,11 @@ export class ServerErrorInterceptor implements HttpInterceptor {
|
||||
this._urlsWithError.add(req.url);
|
||||
}
|
||||
|
||||
return throwError(() => error);
|
||||
return throwError(() => error).pipe(
|
||||
finalize(() => {
|
||||
this._loadingService.stop();
|
||||
}),
|
||||
);
|
||||
}),
|
||||
backoffOnServerError(this._maxRetries, this._skippedPaths),
|
||||
tap(() => {
|
||||
|
||||
@ -3,7 +3,7 @@ import { combineLatest, Observable, pipe } from 'rxjs';
|
||||
import { IFilter } from '../models/filter.model';
|
||||
import { INestedFilter } from '../models/nested-filter.model';
|
||||
import { IFilterGroup } from '../models/filter-group.model';
|
||||
import { handleCheckedValue } from '../filter-utils';
|
||||
import { extractFilterValues, handleCheckedValue } from '../filter-utils';
|
||||
import { FilterService } from '../filter.service';
|
||||
import { SearchService } from '../../search';
|
||||
import { Filter } from '../models/filter';
|
||||
@ -18,6 +18,17 @@ const atLeastOneIsExpandable = pipe(
|
||||
shareDistinctLast(),
|
||||
);
|
||||
|
||||
export interface LocalStorageFilter {
|
||||
id: string;
|
||||
checked: boolean;
|
||||
children?: LocalStorageFilter[] | null;
|
||||
}
|
||||
|
||||
export interface LocalStorageFilters {
|
||||
primaryFilters: LocalStorageFilter[] | null;
|
||||
secondaryFilters: LocalStorageFilter[] | null;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'iqser-filter-card [primaryFiltersSlug]',
|
||||
templateUrl: './filter-card.component.html',
|
||||
@ -36,6 +47,7 @@ const atLeastOneIsExpandable = pipe(
|
||||
})
|
||||
export class FilterCardComponent implements OnInit {
|
||||
@Input() primaryFiltersSlug!: string;
|
||||
@Input() fileId?: string;
|
||||
@Input() actionsTemplate?: TemplateRef<unknown>;
|
||||
@Input() secondaryFiltersSlug = '';
|
||||
@Input() primaryFiltersLabel: string = _('filter-menu.filter-types');
|
||||
@ -54,7 +66,7 @@ export class FilterCardComponent implements OnInit {
|
||||
private readonly _elementRef: ElementRef,
|
||||
) {}
|
||||
|
||||
private get _primaryFilters$(): Observable<IFilter[]> {
|
||||
get #primaryFilters$(): Observable<IFilter[]> {
|
||||
return combineLatest([this.primaryFilterGroup$, this.searchService.valueChanges$]).pipe(
|
||||
map(([group]) => this.searchService.searchIn(group?.filters ?? [])),
|
||||
shareLast(),
|
||||
@ -64,7 +76,7 @@ export class FilterCardComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
this.primaryFilterGroup$ = this.filterService.getGroup$(this.primaryFiltersSlug).pipe(shareLast());
|
||||
this.secondaryFilterGroup$ = this.filterService.getGroup$(this.secondaryFiltersSlug).pipe(shareLast());
|
||||
this.primaryFilters$ = this._primaryFilters$;
|
||||
this.primaryFilters$ = this.#primaryFilters$;
|
||||
|
||||
this.atLeastOneFilterIsExpandable$ = atLeastOneIsExpandable(this.primaryFilterGroup$);
|
||||
this.atLeastOneSecondaryFilterIsExpandable$ = atLeastOneIsExpandable(this.secondaryFilterGroup$);
|
||||
@ -96,16 +108,17 @@ export class FilterCardComponent implements OnInit {
|
||||
}
|
||||
|
||||
this.filterService.refresh();
|
||||
this.#updateFiltersInLocalStorage();
|
||||
}
|
||||
|
||||
activatePrimaryFilters(): void {
|
||||
this._setFilters(this.primaryFiltersSlug, true);
|
||||
this.#setFilters(this.primaryFiltersSlug, true);
|
||||
}
|
||||
|
||||
deactivateFilters(exceptedFilterId?: string): void {
|
||||
this._setFilters(this.primaryFiltersSlug, false, exceptedFilterId);
|
||||
this.#setFilters(this.primaryFiltersSlug, false, exceptedFilterId);
|
||||
if (this.secondaryFiltersSlug) {
|
||||
this._setFilters(this.secondaryFiltersSlug, false, exceptedFilterId);
|
||||
this.#setFilters(this.secondaryFiltersSlug, false, exceptedFilterId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,7 +129,7 @@ export class FilterCardComponent implements OnInit {
|
||||
this.filterService.refresh();
|
||||
}
|
||||
|
||||
private _setFilters(filterGroup: string, checked = false, exceptedFilterId?: string) {
|
||||
#setFilters(filterGroup: string, checked = false, exceptedFilterId?: string) {
|
||||
const filters = this.filterService.getGroup(filterGroup)?.filters;
|
||||
filters?.forEach(f => {
|
||||
if (f.id !== exceptedFilterId) {
|
||||
@ -130,4 +143,21 @@ export class FilterCardComponent implements OnInit {
|
||||
});
|
||||
this.filterService.refresh();
|
||||
}
|
||||
|
||||
#updateFiltersInLocalStorage(): void {
|
||||
if (this.fileId) {
|
||||
const primaryFilters = this.filterService.getGroup('primaryFilters');
|
||||
const secondaryFilters = this.filterService.getGroup('secondaryFilters');
|
||||
|
||||
const filters: LocalStorageFilters = {
|
||||
primaryFilters: extractFilterValues(primaryFilters?.filters),
|
||||
secondaryFilters: extractFilterValues(secondaryFilters?.filters),
|
||||
};
|
||||
|
||||
const workloadFiltersString = localStorage.getItem('workload-filters') ?? '{}';
|
||||
const workloadFilters = JSON.parse(workloadFiltersString);
|
||||
workloadFilters[this.fileId] = filters;
|
||||
localStorage.setItem('workload-filters', JSON.stringify(workloadFilters));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { IFilterGroup } from './models/filter-group.model';
|
||||
import { IFilter } from './models/filter.model';
|
||||
import { NestedFilter } from './models/nested-filter';
|
||||
import { Id, IListable } from '../listing';
|
||||
import { LocalStorageFilter } from './filter-card/filter-card.component';
|
||||
|
||||
function copySettings(oldFilters: INestedFilter[], newFilters: INestedFilter[]) {
|
||||
if (!oldFilters || !newFilters) {
|
||||
@ -102,3 +103,25 @@ export function flatChildren(filters: INestedFilter[]): IFilter[] {
|
||||
export function toFlatFilters(groups: IFilterGroup[], condition = (filters: IFilter[]) => filters): IFilter[] {
|
||||
return groups.reduce((acc: IFilter[], f) => [...acc, ...condition(f.filters), ...condition(flatChildren(f.filters))], []);
|
||||
}
|
||||
|
||||
export function extractFilterValues(filters: INestedFilter[] | undefined): LocalStorageFilter[] | null {
|
||||
const extractedValues: LocalStorageFilter[] = [];
|
||||
filters?.forEach(filter => {
|
||||
extractedValues.push({
|
||||
id: filter.id,
|
||||
checked: filter.checked ?? false,
|
||||
children: extractFilterValues(filter.children),
|
||||
});
|
||||
});
|
||||
return extractedValues.length ? extractedValues : null;
|
||||
}
|
||||
|
||||
export function copyLocalStorageFiltersValues(primaryFilters: INestedFilter[], localStorageFilters: LocalStorageFilter[]) {
|
||||
primaryFilters?.forEach(filter => {
|
||||
const localStorageFilter = localStorageFilters.find(f => f.id === filter.id);
|
||||
filter.checked = localStorageFilter?.checked;
|
||||
if (filter.children && localStorageFilter?.children) {
|
||||
copyLocalStorageFiltersValues(filter.children, localStorageFilter.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
[primaryFiltersLabel]="primaryFiltersLabel"
|
||||
[primaryFiltersSlug]="primaryFiltersSlug"
|
||||
[secondaryFiltersSlug]="secondaryFiltersSlug"
|
||||
[fileId]="fileId"
|
||||
></iqser-filter-card>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
@ -14,6 +14,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
})
|
||||
export class PopupFilterComponent implements OnInit {
|
||||
@Input() primaryFiltersSlug!: string;
|
||||
@Input() fileId?: string;
|
||||
@Input() actionsTemplate?: TemplateRef<unknown>;
|
||||
@Input() secondaryFiltersSlug = '';
|
||||
@Input() primaryFiltersLabel: string = _('filter-menu.filter-types');
|
||||
|
||||
@ -3,3 +3,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: #accef7;
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@
|
||||
overflow-y: auto !important;
|
||||
|
||||
&.has-scrollbar iqser-table-item::ng-deep {
|
||||
.action-buttons {
|
||||
.action-buttons:not(.edit-button) {
|
||||
right: 0;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
@ -47,7 +47,7 @@ export class TableContentComponent<Class extends IListable<PrimaryKey>, PrimaryK
|
||||
multiSelect(entity: Class, $event: MouseEvent): void {
|
||||
if (this.selectionEnabled && this._multiSelectActive$.value) {
|
||||
$event.stopPropagation();
|
||||
this.listingService.select(entity);
|
||||
this.listingService.select(entity, $event.shiftKey);
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,6 +80,7 @@ export class TableContentComponent<Class extends IListable<PrimaryKey>, PrimaryK
|
||||
|
||||
@HostListener('window:keydown.control')
|
||||
@HostListener('window:keydown.meta')
|
||||
@HostListener('window:keydown.shift')
|
||||
private _enableMultiSelect() {
|
||||
this._multiSelectActive$.next(true);
|
||||
}
|
||||
@ -88,6 +89,7 @@ export class TableContentComponent<Class extends IListable<PrimaryKey>, PrimaryK
|
||||
@HostListener('window:blur')
|
||||
@HostListener('window:keyup.control')
|
||||
@HostListener('window:keyup.meta')
|
||||
@HostListener('window:keyup.shift')
|
||||
private _disableMultiSelect() {
|
||||
this._multiSelectActive$.next(false);
|
||||
}
|
||||
|
||||
@ -63,8 +63,11 @@
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
position: absolute;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.action-buttons:not(.edit-button) {
|
||||
position: absolute;
|
||||
right: -11px;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
|
||||
@ -51,13 +51,9 @@
|
||||
<div *ngFor="let e of ctx.draggingEntities" [style.min-height]="itemHeight + 'px'" class="placeholder"></div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template cdkDragPreview>
|
||||
<ng-template cdkDragPreview [matchSize]="true">
|
||||
<ng-container *ngFor="let e of ctx.draggingEntities">
|
||||
<div
|
||||
[class.selected]="all[e.id].isSelected$ | async"
|
||||
[ngClass]="all[e.id].classes$ | async"
|
||||
[style.width]="itemWidth + 'px'"
|
||||
>
|
||||
<div [class.selected]="all[e.id].isSelected$ | async" [ngClass]="all[e.id].classes$ | async">
|
||||
<ng-container *ngTemplateOutlet="itemTemplate; context: { entity: e }"></ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@ -4,7 +4,8 @@ import { Observable } from 'rxjs';
|
||||
import { HeadersConfiguration, List, RequiredParam, Validate } from '../utils';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
const ROOT_CHANGES_KEY = 'root';
|
||||
export const ROOT_CHANGES_KEY = 'root';
|
||||
export const LAST_CHECKED_OFFSET = 15000;
|
||||
|
||||
export interface HeaderOptions {
|
||||
readonly authorization?: boolean;
|
||||
@ -23,7 +24,9 @@ export interface QueryParam {
|
||||
*/
|
||||
export abstract class GenericService<I> {
|
||||
protected readonly _http = inject(HttpClient);
|
||||
protected readonly _lastCheckedForChanges = new Map<string, string>([[ROOT_CHANGES_KEY, new Date().toISOString()]]);
|
||||
protected readonly _lastCheckedForChanges = new Map<string, string>([
|
||||
[ROOT_CHANGES_KEY, new Date(Date.now() - LAST_CHECKED_OFFSET).toISOString()],
|
||||
]);
|
||||
protected abstract readonly _defaultModelPath: string;
|
||||
|
||||
get<T = I[]>(): Observable<T>;
|
||||
@ -144,6 +147,6 @@ export abstract class GenericService<I> {
|
||||
}
|
||||
|
||||
protected _updateLastChanged(key = ROOT_CHANGES_KEY): void {
|
||||
this._lastCheckedForChanges.set(key, new Date().toISOString());
|
||||
this._lastCheckedForChanges.set(key, new Date(Date.now() - LAST_CHECKED_OFFSET).toISOString());
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,11 @@ export abstract class IqserUserPreferenceService extends GenericService<UserAttr
|
||||
protected abstract readonly _devFeaturesEnabledKey: string;
|
||||
#userAttributes: UserAttributes = {};
|
||||
|
||||
get isIqserDevMode(): boolean {
|
||||
const value = sessionStorage.getItem(this._devFeaturesEnabledKey);
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
get userAttributes(): UserAttributes {
|
||||
return this.#userAttributes;
|
||||
}
|
||||
@ -74,3 +79,7 @@ export class DefaultUserPreferenceService extends IqserUserPreferenceService {
|
||||
protected readonly _defaultModelPath = 'attributes';
|
||||
protected readonly _devFeaturesEnabledKey = inject(BASE_HREF) + '.enable-dev-features';
|
||||
}
|
||||
|
||||
export function isIqserDevMode() {
|
||||
return inject(IqserUserPreferenceService).isIqserDevMode;
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { ErrorMessageService } from './error-message.service';
|
||||
import { isIqserDevMode } from './iqser-user-preference.service';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
|
||||
const enum NotificationType {
|
||||
@ -26,6 +27,7 @@ export interface ToasterOptions extends IndividualConfig {
|
||||
*/
|
||||
readonly params?: Record<string, string | number>;
|
||||
readonly actions?: ToasterActions[];
|
||||
readonly useRaw?: boolean;
|
||||
}
|
||||
|
||||
export interface ErrorToasterOptions extends ToasterOptions {
|
||||
@ -35,10 +37,19 @@ export interface ErrorToasterOptions extends ToasterOptions {
|
||||
readonly error?: HttpErrorResponse;
|
||||
}
|
||||
|
||||
const defaultDevToastOptions: Partial<ToasterOptions> = {
|
||||
timeOut: 10000,
|
||||
easing: 'ease-in-out',
|
||||
easeTime: 500,
|
||||
useRaw: true,
|
||||
};
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class Toaster {
|
||||
readonly #isIqserDevMode = isIqserDevMode();
|
||||
|
||||
constructor(
|
||||
private readonly _toastr: ToastrService,
|
||||
private readonly _router: Router,
|
||||
@ -62,6 +73,18 @@ export class Toaster {
|
||||
return this._toastr.error(resultedMsg, options?.title, options);
|
||||
}
|
||||
|
||||
rawError(message: string, config?: Partial<IndividualConfig<unknown>>) {
|
||||
return this._toastr.error(message, undefined, config);
|
||||
}
|
||||
|
||||
devInfo(message: string): ActiveToast<unknown> | undefined {
|
||||
if (!this.#isIqserDevMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.info(message, defaultDevToastOptions);
|
||||
}
|
||||
|
||||
info(message: string, options?: Partial<ToasterOptions>): ActiveToast<unknown> {
|
||||
return this._showToastNotification(message, NotificationType.INFO, options);
|
||||
}
|
||||
@ -79,7 +102,7 @@ export class Toaster {
|
||||
notificationType = NotificationType.INFO,
|
||||
options?: Partial<ToasterOptions>,
|
||||
): ActiveToast<unknown> {
|
||||
const translatedMsg = this._translateService.instant(message, options?.params) as string;
|
||||
const translatedMsg = options?.useRaw ? message : (this._translateService.instant(message, options?.params) as string);
|
||||
|
||||
switch (notificationType) {
|
||||
case NotificationType.SUCCESS:
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { LogoComponent, SideNavComponent, SmallChipComponent, StatusBarComponent } from './index';
|
||||
import { LogoComponent, SideNavComponent, SmallChipComponent } from './index';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { IqserIconsModule } from '../icons';
|
||||
|
||||
const components = [SmallChipComponent, StatusBarComponent, SideNavComponent, LogoComponent];
|
||||
const components = [SmallChipComponent, SideNavComponent, LogoComponent];
|
||||
|
||||
@NgModule({
|
||||
declarations: [...components],
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core';
|
||||
import { StatusBarConfig } from './status-bar-config.model';
|
||||
import { NgClass, NgForOf, NgIf, NgStyle } from '@angular/common';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
|
||||
@Component({
|
||||
selector: 'iqser-status-bar',
|
||||
@ -7,6 +9,8 @@ import { StatusBarConfig } from './status-bar-config.model';
|
||||
styleUrls: ['./status-bar.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgClass, NgStyle, NgForOf, MatTooltipModule, NgIf],
|
||||
})
|
||||
export class StatusBarComponent<T extends string> {
|
||||
@Input() configs: readonly StatusBarConfig<T>[] = [];
|
||||
|
||||
@ -2,3 +2,4 @@ export * from './pipes/log.pipe';
|
||||
export * from './pipes/humanize-camel-case.pipe';
|
||||
export * from './pipes/capitalize.pipe';
|
||||
export * from './pipes/humanize.pipe';
|
||||
export * from './pipes/size.pipe';
|
||||
|
||||
12
src/lib/standalone/pipes/size.pipe.ts
Normal file
12
src/lib/standalone/pipes/size.pipe.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { size } from '../../utils';
|
||||
|
||||
@Pipe({
|
||||
name: 'size',
|
||||
standalone: true,
|
||||
})
|
||||
export class SizePipe implements PipeTransform {
|
||||
transform(value: number): string {
|
||||
return size(value);
|
||||
}
|
||||
}
|
||||
@ -25,6 +25,18 @@ export function humanizeCamelCase(value: string): string {
|
||||
return value.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase();
|
||||
}
|
||||
|
||||
export function size(value: number): string {
|
||||
if (value >= 1000 ** 3) {
|
||||
return `${(value / 1000 ** 3).toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
if (value >= 1000 ** 2) {
|
||||
return `${(value / 1000 ** 2).toFixed(2)} MB`;
|
||||
}
|
||||
|
||||
return `${(value / 1000).toFixed(2)} KB`;
|
||||
}
|
||||
|
||||
export function escapeHtml<T extends unknown | string>(unsafe: T, options?: { ignoreTags: string[] }) {
|
||||
if (typeof unsafe !== 'string') {
|
||||
return unsafe;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user