Skip to content

Commit 5df85da

Browse files
committed
feat: implement schedule configuration component with time and day selection
Signed-off-by: Manuel Abascal <mjabascal10@gmail.com>
1 parent 3fc0789 commit 5df85da

8 files changed

Lines changed: 235 additions & 48 deletions

File tree

frontend/src/app/shared/components/schedule-config/schedule-config.component.css

Whitespace-only changes.
Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,47 @@
11
<div class="schedule-config-container">
2-
<div class="schedule-section mb-3">
3-
<label class="d-block mb-2 fw-semibold">Days</label>
4-
<div class="d-flex gap-2 flex-wrap">
5-
<button *ngFor="let day of days"
2+
<div class="mb-4">
3+
<label class="fw-semibold text-dark mb-2 d-block">Days</label>
4+
<div class="d-flex flex-wrap gap-2">
5+
<a *ngFor="let day of days"
66
type="button"
77
(click)="toggleDay(day.number)"
8-
[ngClass]="{'btn-primary': isDaySelected(day.number), 'btn-outline-secondary': !isDaySelected(day.number)}"
9-
class="btn btn-sm">
8+
[class.active]="isDaySelected(day.number)"
9+
class="day-link"
10+
[attr.aria-pressed]="isDaySelected(day.number)">
1011
{{ day.name.substring(0, 3) }}
11-
</button>
12+
</a>
1213
</div>
1314
</div>
1415

15-
<div class="schedule-section">
16-
<div class="row">
17-
<div class="col-md-6 mb-3">
18-
<label for="startTime" class="form-label fw-semibold">Start Time</label>
19-
<input type="time"
20-
id="startTime"
21-
[(ngModel)]="config.startTime"
22-
(change)="onTimeChange()"
23-
class="form-control">
24-
</div>
25-
<div class="col-md-6 mb-3">
26-
<label for="endTime" class="form-label fw-semibold">End Time</label>
27-
<input type="time"
28-
id="endTime"
29-
[(ngModel)]="config.endTime"
30-
(change)="onTimeChange()"
31-
class="form-control">
16+
<div style="max-width: 500px" class="mb-4">
17+
<label class="fw-semibold text-dark mb-2 d-block">Time Range</label>
18+
<div class="card p-3 border rounded">
19+
<div class="d-flex gap-3 align-items-end">
20+
<div class="flex-grow-1">
21+
<label for="startTime" class="form-label text-muted small d-block mb-2">Start Time</label>
22+
<ngb-timepicker [(ngModel)]="config.startTime"
23+
(ngModelChange)="onTimeChange()"
24+
id="startTime">
25+
</ngb-timepicker>
26+
</div>
27+
<span class="fw-bold text-secondary mb-2"></span>
28+
<div class="flex-grow-1">
29+
<label for="endTime" class="form-label text-muted small d-block mb-2">End Time</label>
30+
<ngb-timepicker [(ngModel)]="config.endTime"
31+
(ngModelChange)="onTimeChange()"
32+
id="endTime">
33+
</ngb-timepicker>
34+
</div>
3235
</div>
3336
</div>
3437
</div>
38+
39+
<div class="mt-3 p-2 border rounded bg-light">
40+
<small class="text-muted">
41+
<strong>{{ config.days.length > 0 ? 'Schedule:' : 'No days selected' }}</strong>
42+
<span *ngIf="config.days.length > 0" class="ms-2">
43+
{{ getDayNames() }} • {{ formatTime(config.startTime) }} - {{ formatTime(config.endTime) }}
44+
</span>
45+
</small>
46+
</div>
3547
</div>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
2+
.schedule-section {
3+
display: flex;
4+
flex-direction: column;
5+
}
6+
7+
.days-container {
8+
display: flex;
9+
gap: 10px;
10+
flex-wrap: wrap;
11+
}
12+
13+
.day-link {
14+
padding: 10px 16px;
15+
border: 1px solid #e0e0e0;
16+
border-radius: 6px;
17+
font-size: 14px;
18+
font-weight: 500;
19+
color: #666;
20+
text-decoration: none;
21+
cursor: pointer;
22+
transition: all 0.2s ease;
23+
background-color: #fff;
24+
width: 4.2rem;
25+
}
26+
27+
.day-link:hover {
28+
border-color: #1565c0;
29+
background-color: #f8f9fa;
30+
}
31+
32+
.day-link.active {
33+
background-color: #1565c0;
34+
color: #fff;
35+
border-color: #1565c0;
36+
}
37+
38+
.schedule-summary {
39+
background-color: #f5f5f5;
40+
border-left: 3px solid #1565c0;
41+
margin-top: 20px;
42+
}
43+
44+
.gap-2 {
45+
gap: 0.5rem;
46+
}

frontend/src/app/shared/components/schedule-config/schedule-config.component.ts

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
1-
import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
2-
3-
interface ScheduleConfig {
4-
days: number[];
5-
startTime: string;
6-
endTime: string;
7-
}
1+
import {Component, EventEmitter, forwardRef, Input, OnInit, Output} from '@angular/core';
2+
import {NG_VALUE_ACCESSOR} from '@angular/forms';
3+
import {ScheduleConfig} from "./schedule-config.validator";
84

95
@Component({
106
selector: 'app-utm-schedule-config',
11-
templateUrl: './utm-schedule-config.component.html',
12-
styleUrls: ['./utm-schedule-config.component.scss']
7+
templateUrl: './schedule-config.component.html',
8+
styleUrls: ['./schedule-config.component.scss'],
9+
providers: [
10+
{
11+
provide: NG_VALUE_ACCESSOR,
12+
useExisting: forwardRef(() => ScheduleConfigComponent),
13+
multi: true
14+
}
15+
]
1316
})
14-
export class UtmScheduleConfigComponent implements OnInit {
15-
@Input() initialConfig: ScheduleConfig = { days: [], startTime: '22:00', endTime: '06:00' };
17+
export class ScheduleConfigComponent implements OnInit {
18+
@Input() initialConfig: ScheduleConfig = {
19+
days: [],
20+
startTime: { hour: 0, minute: 0 },
21+
endTime: { hour: 23, minute: 59 }
22+
};
1623
@Output() scheduleChange = new EventEmitter<ScheduleConfig>();
1724

1825
days = [
@@ -27,10 +34,54 @@ export class UtmScheduleConfigComponent implements OnInit {
2734

2835
config: ScheduleConfig = { ...this.initialConfig };
2936

37+
private onChange: (value: ScheduleConfig) => void = () => {};
38+
private onTouched: () => void = () => {};
39+
3040
ngOnInit() {
3141
this.config = { ...this.initialConfig };
3242
}
3343

44+
writeValue(value: ScheduleConfig): void {
45+
if (value) {
46+
this.config = {
47+
days: [...value.days],
48+
startTime: this.parseTimeString(value.startTime),
49+
endTime: this.parseTimeString(value.endTime)
50+
};
51+
}
52+
}
53+
54+
registerOnChange(fn: (value: ScheduleConfig) => void): void {
55+
this.onChange = fn;
56+
}
57+
58+
registerOnTouched(fn: () => void): void {
59+
this.onTouched = fn;
60+
}
61+
62+
setDisabledState?(isDisabled: boolean): void {
63+
64+
}
65+
66+
private emitChange() {
67+
const newConfig = { ...this.config };
68+
this.scheduleChange.emit(newConfig);
69+
this.onChange({
70+
days: newConfig.days,
71+
startTime: this.formatTime(newConfig.startTime),
72+
endTime: this.formatTime(newConfig.endTime)
73+
});
74+
this.onTouched();
75+
}
76+
77+
getDayNames(): string {
78+
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
79+
return this.config.days
80+
.map(num => dayNames[num])
81+
.join(', ');
82+
}
83+
84+
3485
toggleDay(dayNumber: number) {
3586
const index = this.config.days.indexOf(dayNumber);
3687
if (index > -1) {
@@ -50,7 +101,17 @@ export class UtmScheduleConfigComponent implements OnInit {
50101
this.emitChange();
51102
}
52103

53-
private emitChange() {
54-
this.scheduleChange.emit({ ...this.config });
104+
formatTime(time: any): string {
105+
if (!time) { return ''; }
106+
const h = time.hour.toString().padStart(2, '0');
107+
const m = time.minute.toString().padStart(2, '0');
108+
return `${h}:${m}`;
109+
}
110+
111+
private parseTimeString(timeStr: string): {hour: number, minute: number} {
112+
const [h, m] = timeStr.split(':').map(Number);
113+
return { hour: h, minute: m };
55114
}
115+
116+
56117
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
export interface ScheduleConfig {
2+
days: number[];
3+
startTime: any;
4+
endTime: any;
5+
}
6+
7+
export class ScheduleConfigValidator {
8+
static isValid(config: ScheduleConfig): boolean {
9+
if (!config) { return false; }
10+
11+
if (!Array.isArray(config.days) || config.days.length === 0) { return false; }
12+
const validDays = config.days.every(d => Number.isInteger(d) && d >= 0 && d <= 6);
13+
if (!validDays) { return false; }
14+
15+
if (!this.isValidTime(config.startTime)) { return false; }
16+
if (!this.isValidTime(config.endTime)) { return false; }
17+
18+
const start = this.toMinutes(config.startTime);
19+
const end = this.toMinutes(config.endTime);
20+
21+
if (start >= end) { return false; }
22+
23+
return true;
24+
}
25+
26+
private static isValidTime(time: any): boolean {
27+
if (!time) { return false; }
28+
29+
if (typeof time === 'object' && 'hour' in time && 'minute' in time) {
30+
return (
31+
Number.isInteger(time.hour) &&
32+
Number.isInteger(time.minute) &&
33+
time.hour >= 0 && time.hour < 24 &&
34+
time.minute >= 0 && time.minute < 60
35+
);
36+
}
37+
38+
if (typeof time === 'string') {
39+
const regex = /^([01]\d|2[0-3]):([0-5]\d)$/;
40+
return regex.test(time);
41+
}
42+
43+
return false;
44+
}
45+
46+
private static toMinutes(time: any): number {
47+
if (typeof time === 'object' && 'hour' in time && 'minute' in time) {
48+
return time.hour * 60 + time.minute;
49+
}
50+
if (typeof time === 'string') {
51+
const [h, m] = time.split(':').map(Number);
52+
return h * 60 + m;
53+
}
54+
return 0;
55+
}
56+
}

frontend/src/app/shared/components/utm/config/app-config-sections/app-config-sections.component.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,9 @@ <h6 class="font-weight-semibold mb-2">{{section.section | titlecase}}</h6>
195195

196196
<ng-container *ngIf="conf.confParamDatatype === ConfigDataTypeEnum.Cron">
197197
<!--<app-utm-cp-cron-editor [(ngModel)]="conf.confParamValue" (ngModelChange)="save(conf.confParamValue,conf)"></app-utm-cp-cron-editor>-->
198-
<app-schedule-config></app-schedule-config>
198+
<app-utm-schedule-config [(ngModel)]="conf.confParamValue"
199+
(ngModelChange)="save(conf.confParamValue, conf)">
200+
</app-utm-schedule-config>
199201
</ng-container>
200202

201203

frontend/src/app/shared/components/utm/config/app-config-sections/app-config-sections.component.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ import {
1313
import {UtmConfigParamsService} from '../../../../services/config/utm-config-params.service';
1414
import {LocationService, SelectOption} from '../../../../services/location.service';
1515
import {NetworkService} from '../../../../services/network.service';
16-
import {VersionType, VersionInfoService} from '../../../../services/version/version-info.service';
1716
import {TimezoneFormatService} from '../../../../services/utm-timezone.service';
1817
import {ConfigDataTypeEnum, SectionConfigParamType} from '../../../../types/configuration/section-config-param.type';
1918
import {ApplicationConfigSectionEnum, SectionConfigType} from '../../../../types/configuration/section-config.type';
19+
import {ScheduleConfigValidator} from '../../../schedule-config/schedule-config.validator';
2020
import {AppConfigDeleteConfirmComponent} from '../app-config-delete-confirm/app-config-delete-confirm.component';
2121

2222

@@ -96,7 +96,11 @@ export class AppConfigSectionsComponent implements OnInit, OnDestroy {
9696
this.checkedEmailConfig(false);
9797
this.saving = true;
9898
if (this.checkConfigValid()) {
99-
this.utmConfigParamsService.update(this.configToSave).subscribe(response => {
99+
const config = this.configToSave.map(conf => {
100+
conf.confParamValue = this.serializeConfigValue(conf);
101+
return conf;
102+
});
103+
this.utmConfigParamsService.update(config).subscribe(response => {
100104

101105
if (this.detectRequiredRestart()) {
102106
this.restartApiBehavior.$restartSignal.next(true);
@@ -169,6 +173,9 @@ export class AppConfigSectionsComponent implements OnInit, OnDestroy {
169173
case ConfigDataTypeEnum.EmailList:
170174
return this.isValid(config);
171175

176+
case ConfigDataTypeEnum.Cron:
177+
return ScheduleConfigValidator.isValid(config.confParamValue);
178+
172179
default:
173180
return this.isValidConfig(config);
174181
}
@@ -309,6 +316,13 @@ export class AppConfigSectionsComponent implements OnInit, OnDestroy {
309316
return this.configs.some(conf => conf.confParamShort === 'utmstack.tfa.enable' && conf.confParamValue === 'true');
310317
}
311318

319+
serializeConfigValue(conf: SectionConfigParamType): string {
320+
if (conf.confParamDatatype === ConfigDataTypeEnum.Cron) {
321+
return JSON.stringify(conf.confParamValue);
322+
}
323+
return conf.confParamValue;
324+
}
325+
312326
ngOnDestroy(): void {
313327
this.destroy$.next();
314328
this.destroy$.complete();

frontend/src/app/shared/utm-shared.module.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -245,11 +245,8 @@ import {HighlightPipe} from './pipes/text/highlight.pipe';
245245
import {TimePeriodPipe} from './pipes/time-period.pipe';
246246
import {TimezoneOffsetPipe} from './pipes/timezone-offset.pipe';
247247
import {UtmNotifier} from './websocket/utm-notifier';
248-
import {IsEnterpriseModuleDirective} from "./directives/enterprise/enterprise.directive";
249-
import {
250-
ScheduleConfigComponent,
251-
UtmScheduleConfigComponent
252-
} from './components/schedule-config/schedule-config.component';
248+
import {IsEnterpriseModuleDirective} from './directives/enterprise/enterprise.directive';
249+
import {ScheduleConfigComponent} from './components/schedule-config/schedule-config.component';
253250

254251

255252
@NgModule({
@@ -417,8 +414,7 @@ import {
417414
CodeEditorComponent,
418415
LoginProvidersComponent,
419416
IsEnterpriseModuleDirective,
420-
ScheduleConfigComponent,
421-
UtmScheduleConfigComponent
417+
ScheduleConfigComponent
422418
],
423419
exports: [
424420
IndexPatternCreateComponent,
@@ -529,7 +525,7 @@ import {
529525
ResizableFilterContainerComponent,
530526
CodeEditorComponent,
531527
IsEnterpriseModuleDirective,
532-
UtmScheduleConfigComponent
528+
ScheduleConfigComponent
533529
],
534530
entryComponents: [
535531
LoginComponent,

0 commit comments

Comments
 (0)