diff --git a/src/CommonAssemblyInfo.cs b/src/CommonAssemblyInfo.cs index 0ce31ad..1e915fd 100644 --- a/src/CommonAssemblyInfo.cs +++ b/src/CommonAssemblyInfo.cs @@ -10,5 +10,5 @@ using System.Reflection; [assembly: AssemblyProductAttribute("CrystalQuartz")] -[assembly: AssemblyVersionAttribute("7.0.0.25")] -[assembly: AssemblyFileVersionAttribute("7.0.0.25")] +[assembly: AssemblyVersionAttribute("7.2.0.0")] +[assembly: AssemblyFileVersionAttribute("7.2.0.0")] diff --git a/src/CrystalQuartz.Application.Client2/dev/dev-server.ts b/src/CrystalQuartz.Application.Client2/dev/dev-server.ts new file mode 100644 index 0000000..9b14d70 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/dev/dev-server.ts @@ -0,0 +1,153 @@ +import { Schedule } from "./fake-scheduler"; +import { FakeSchedulerServer } from "./fake-scheduler-server"; + +import * as querystring from 'querystring'; +import * as http from 'http'; +import * as url from 'url'; +import * as fs from 'fs'; +import * as path from 'path'; + +const port = 3000; + +const mimeTypeResolver = (fileName: string) => { + const extension = path.extname(fileName).toUpperCase(); + + switch (extension) { + case '.HTML': return 'text/html'; + case '.CSS': return 'text/css'; + case '.js': return 'application/javascript'; + default: return null; + } +}; + +const + SECONDS = (x: number) => x * 1000, + MINUTES = (x: number) => SECONDS(60 * x); + +const + options = { + version: 'dev', + quartzVersion: 'nodejs-emulation', + dotNetVersion: 'none', + timelineSpan: 3600 * 1000, + schedulerName: 'DemoScheduler', + }, + + now = new Date().getTime(), + + schedule: Schedule = { + 'Maintenance': { + 'DB_Backup': { + duration: SECONDS(20), + triggers: { + 'db_trigger_1': { repeatInterval: MINUTES(1), initialDelay: SECONDS(5) }, + 'db_trigger_2': { repeatInterval: MINUTES(1.5) }, + } + }, + 'Compress_Logs': { + duration: MINUTES(1), + triggers: { + 'logs_trigger_1': { repeatInterval: MINUTES(3) }, + 'logs_trigger_2': { repeatInterval: MINUTES(4), pause: true } + } + } + }, + 'Domain': { + 'Email_Sender': { + duration: SECONDS(10), + triggers: { + 'email_sender_trigger_1': { repeatInterval: MINUTES(2), repeatCount: 5 } + } + }, + 'Remove_Inactive_Users': { + duration: SECONDS(30), + triggers: { + 'remove_users_trigger_1': { repeatInterval: MINUTES(3), repeatCount: 5, persistAfterExecution: true } + } + } + }, + 'Reporting': { + 'Daily Sales': { + duration: MINUTES(7), + triggers: { + 'ds_trigger': { repeatInterval: MINUTES(60), } + } + }, + 'Services Health': { + duration: MINUTES(2), + triggers: { + 'hr_trigger': { repeatInterval: MINUTES(30), startDate: now + MINUTES(1) } + } + }, + 'Resource Consumption': { + duration: MINUTES(1), + triggers: { + 'rc_trigger': { repeatInterval: MINUTES(10), startDate: now + MINUTES(2), endDate: now + MINUTES(40), persistAfterExecution: true } + } + } + } + }, + + schedulerServer = new FakeSchedulerServer({ + dotNetVersion: options.dotNetVersion, + quartzVersion: options.quartzVersion, + schedule: schedule, + schedulerName: options.schedulerName, + timelineSpan: options.timelineSpan, + version: options.version + }); + +const requestHandler = (request: any, response: any) => { + const requestUrl = url.parse(request.url, true); + + if (request.method === 'GET') { + console.log(request.url); + + const filePath = requestUrl.query.path + ? 'dist/' + requestUrl.query.path + : 'dist/index.html'; + + if (fs.existsSync(filePath)) { + response.writeHead(200, { "Content-Type": mimeTypeResolver(filePath) }); + response.write(fs.readFileSync(filePath)); + response.end(); + return; + } + + response.writeHead(404, { "Content-Type": 'text/plain' }); + response.write('Not found'); + response.end(); + } else { + //var POST = {}; + request.on( + 'data', + (data: any) => { + data = data.toString(); + var POST = querystring.parse(data); + +// data = data.split('&'); +// for (var i = 0; i < data.length; i++) { +// var _data = data[i].replace(/\+/g, ' ').split("="); +// POST[_data[0]] = _data[1]; +// } + console.log(POST); + + const result = schedulerServer.handleRequest(POST); + response.writeHead(200, { "Content-Type": 'application/json' }); + response.write(JSON.stringify(result)); + response.end(); + }); + } +}; + +const server = http.createServer(requestHandler); + +server.listen( + port /*, + (err: any) => { + if (err) { + return console.log('something bad happened', err); + } + + console.log(`server is listening on ${port}`); + }*/); diff --git a/src/CrystalQuartz.Application.Client2/dev/fake-scheduler-server.ts b/src/CrystalQuartz.Application.Client2/dev/fake-scheduler-server.ts new file mode 100644 index 0000000..29cb329 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/dev/fake-scheduler-server.ts @@ -0,0 +1,283 @@ +import { FakeScheduler, Schedule, ScheduleTrigger } from "./fake-scheduler"; +import { SchedulerStatus } from "../src/api"; + +export interface IFakeSchedulerOptions { + version: string; + quartzVersion: string; + dotNetVersion: string; + timelineSpan: number; + schedulerName: string; + schedule: Schedule; +} + +export class FakeSchedulerServer { + private _scheduler: FakeScheduler; + private _commandHandlers: { [command: string]: (args: any) => any }; + + constructor(options: IFakeSchedulerOptions) { + this._scheduler = new FakeScheduler(options.schedulerName, options.schedule); + + this._commandHandlers = { + 'get_env': () => ({ + _ok: 1, + sv: options.version, + qv: options.quartzVersion, + dnv: options.dotNetVersion, + ts: options.timelineSpan + }), + 'get_input_types': (args) => ({ + _ok: 1, + i: [ + { "_": 'string', l: 'string' }, + { "_": 'int', l: 'int' }, + { "_": 'long', l: 'long' }, + { "_": 'float', l: 'float' }, + { "_": 'double', l: 'double' }, + { "_": 'boolean', l: 'boolean', v: 1 }, + { "_": 'ErrorTest', l: 'Error test' } + ] + }), + 'get_input_type_variants': (args) => ({ + _ok: 1, + i: [ + { "_": 'true', l: 'True' }, + { "_": 'false', l: 'False' } + ] + }), + 'get_job_types': (args) => ({ + _ok: 1, + i: [ + "HelloJob|CrystalQuartz.Samples|CrystalQuartz", + "CleanupJob|CrystalQuartz.Samples|CrystalQuartz", + "GenerateReports|CrystalQuartz.Samples|CrystalQuartz" + ] + }), + 'get_data': (args) => { + return this.mapCommonData(args); + }, + 'resume_trigger': (args) => { + this._scheduler.resumeTrigger(args.trigger); + return this.mapCommonData(args); + }, + 'pause_trigger': (args) => { + this._scheduler.pauseTrigger(args.trigger); + return this.mapCommonData(args); + }, + 'delete_trigger': (args) => { + this._scheduler.deleteTrigger(args.trigger); + return this.mapCommonData(args); + }, + 'pause_job': (args) => { + this._scheduler.pauseJob(args.group, args.job); + return this.mapCommonData(args); + }, + 'resume_job': (args) => { + this._scheduler.resumeJob(args.group, args.job); + return this.mapCommonData(args); + }, + 'delete_job': (args) => { + this._scheduler.deleteJob(args.group, args.job); + return this.mapCommonData(args); + }, + 'pause_group': (args) => { + this._scheduler.pauseGroup(args.group); + return this.mapCommonData(args); + }, + 'resume_group': (args) => { + this._scheduler.resumeGroup(args.group); + return this.mapCommonData(args); + }, + 'delete_group': (args) => { + this._scheduler.deleteGroup(args.group); + return this.mapCommonData(args); + }, + 'get_scheduler_details': (args) => ({ + _ok: 1, + ism: this._scheduler.status === SchedulerStatus.Ready, + jsc: false, + jsp: false, + je: this._scheduler.jobsExecuted, + rs: this._scheduler.startedAt, + siid: 'IN_BROWSER', + sn: this._scheduler.name, + isr: false, + t: null, + isd: this._scheduler.status === SchedulerStatus.Shutdown, + ist: this._scheduler.status === SchedulerStatus.Started, + tps: 1, + tpt: null, + v: 'In-Browser Emulation' + }), + 'get_job_details': (args) => ({ + _ok: true, + jd: { + ced: true, // ConcurrentExecutionDisallowed + ds: '', // Description + pjd: false, // PersistJobDataAfterExecution + d: false, // Durable + t: 'SampleJob|Sample|InBrowser', // JobType + rr: false // RequestsRecovery + }, + jdm: { + '_': 'object', + v: { + 'Test1': { '_': 'single', k: 1, v: 'String value'}, + 'Test2': { + '_': 'object', + k: 1, + v: { + "FirstName": { '_': 'single', v: 'John' }, + "LastName": { '_': 'single', v: 'Smith' }, + "TestError": { '_': 'error', _err: 'Exception text' } + } + }, + 'Test3': { + '_': 'enumerable', + v: [ + { '_': 'single', v: 'Value 1' }, + { '_': 'single', v: 'Value 2' }, + { '_': 'single', v: 'Value 3' } + ] + } + } + } // todo: take actual from job + }), + 'start_scheduler': (args) => { + this._scheduler.start(); + return this.mapCommonData(args); + }, + 'pause_scheduler': (args) => { + this._scheduler.pauseAll(); + return this.mapCommonData(args); + }, + 'resume_scheduler': (args) => { + this._scheduler.resumeAll(); + return this.mapCommonData(args); + }, + 'standby_scheduler': (args) => { + this._scheduler.standby(); + return this.mapCommonData(args); + }, + 'stop_scheduler': (args) => { + this._scheduler.shutdown(); + return this.mapCommonData(args); + }, + 'add_trigger': (args) => { + const triggerType = args.triggerType; + + let i = 0, + errors: any = null; + + while (args['jobDataMap[' + i + '].Key']) { + if (args['jobDataMap[' + i + '].InputTypeCode'] === 'ErrorTest') { + errors = errors || {}; + errors[args['jobDataMap[' + i + '].Key']] = 'Testing error message'; + } + + i++; + } + + if (errors) { + return { + ...this.mapCommonData(args), + ve: errors + }; + } + + if (triggerType !== 'Simple') { + return { + _err: 'Only "Simple" trigger type is supported by in-browser fake scheduler implementation' + }; + } + + const + job = args.job, + group = args.group, + name = args.name, + trigger: ScheduleTrigger = { + repeatCount: args.repeatForever ? null : args.repeatCount, + repeatInterval: args.repeatInterval + }; + + this._scheduler.triggerJob(group, job, name, trigger); + + return this.mapCommonData(args); + }, + 'execute_job': (args) => { + this._scheduler.executeNow(args.group, args.job); + return this.mapCommonData(args); + } + }; + + this._scheduler.init(); + this._scheduler.start(); + } + + handleRequest(data: any) { + const handler = this._commandHandlers[data.command]; + + if (handler) { + return handler(data); + } + + return { _err: 'Fake scheduler server does not support command ' + data.command }; + } + + private mapCommonData(args: any) { + const + scheduler = this._scheduler, + data = scheduler.getData(); + + return { + _ok: 1, + sim: scheduler.startedAt, + rs: scheduler.startedAt, + n: data.name, + st: scheduler.status.code, + je: scheduler.jobsExecuted, + jt: data.jobsCount, + ip: scheduler.inProgress.map(ip => ip.fireInstanceId + '|' + ip.trigger.name), + jg: data.groups.map(g => ({ + n: g.name, + s: g.getStatus().value, + + jb: g.jobs.map(j => ({ + n: j.name, + s: j.getStatus().value, + gn: g.name, + _: g.name + '_' + j.name, + + tr: j.triggers.map(t => ({ + '_': t.name, + n: t.name, + s: t.getStatus().value, + sd: t.startDate, + ed: t.endDate, + nfd: t.nextFireDate, + pfd: t.previousFireDate, + tc: 'simple', + tb: (t.repeatCount === null ? '-1' : t.repeatCount.toString()) + '|' + t.repeatInterval + '|' + t.executedCount + })) + })) + })), + ev: scheduler.findEvents(+args.minEventId).map( + ev => { + const result: any = { + '_': `${ev.id}|${ev.date}|${ev.eventType}|${ev.scope}`, + k: ev.itemKey, + fid: ev.fireInstanceId + }; + + if (ev.faulted) { + result['_err'] = ev.errors ? + ev.errors.map(er => ({ "_": er.text, l: er.level })) : + 1 + } + + return result; + } + //`${ev.id}|${ev.date}|${ev.eventType}|${ev.scope}|${ev.fireInstanceId}|${ev.itemKey}` + ) + }; + } +} diff --git a/src/CrystalQuartz.Application.Client2/dev/fake-scheduler.ts b/src/CrystalQuartz.Application.Client2/dev/fake-scheduler.ts new file mode 100644 index 0000000..d496e6c --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/dev/fake-scheduler.ts @@ -0,0 +1,560 @@ +import {ActivityStatus, SchedulerEventScope, SchedulerEventType, SchedulerStatus, ErrorMessage } from "../src/api"; +import { Timer } from "../src/global/timers/timer"; + +export type ScheduleTrigger = { + repeatInterval: number, + repeatCount?: number, + + initialDelay?: number, + pause?: boolean, + startDate?: number, + endDate?: number, + persistAfterExecution?: boolean; +}; +export type ScheduleJob = { + duration: number, + triggers: { [name:string]: ScheduleTrigger } +}; +export type ScheduleGroup = { [name: string]: ScheduleJob }; +export type Schedule = { [name:string]: ScheduleGroup }; + +export abstract class Activity { + protected constructor(public name: string) { + } + + abstract getStatus(): ActivityStatus; + abstract setStatus(status: ActivityStatus): void; +} + +export abstract class CompositeActivity extends Activity { + protected constructor(name: string) { + super(name); + } + + abstract getNestedActivities(): Activity[]; + + getStatus(): ActivityStatus { + const + activities = this.getNestedActivities(), + activitiesCount = activities.length; + + if (activitiesCount === 0) { + return ActivityStatus.Complete; + } + + let + activeCount = 0, + completeCount = 0, + pausedCount = 0; + + for (let i = 0; i < activitiesCount; i++) { + const + activity = activities[i], + status = activity.getStatus(); + + if (status === ActivityStatus.Mixed) { + return ActivityStatus.Mixed; + } else if (status === ActivityStatus.Active){ + activeCount++; + } else if (status === ActivityStatus.Paused){ + pausedCount++; + } else if (status === ActivityStatus.Complete){ + completeCount++; + } + } + + if (activeCount === activitiesCount) { + return ActivityStatus.Active; + } + + if (pausedCount === activitiesCount) { + return ActivityStatus.Paused; + } + + if (completeCount === activitiesCount) { + return ActivityStatus.Complete; + } + + return ActivityStatus.Mixed; + } + + setStatus(status: ActivityStatus) { + this.getNestedActivities().forEach(a => a.setStatus(status)); + } +} + +export class JobGroup extends CompositeActivity { + constructor( + name: string, + public jobs: Job[]){ + + super(name); + } + + getNestedActivities() { + return this.jobs; + } + + findJob(jobName: string) { + return this.jobs.find(j => j.name === jobName); + } + + addJob(jobName: string) { + const result = new Job(jobName, 10000, []); + this.jobs.push(result); + + return result; + } +} + +export class Job extends CompositeActivity{ + constructor( + name: string, + public duration: number, + public triggers: Trigger[]){ + + super(name); + } + + getNestedActivities() { + return this.triggers; + } +} + +export class Trigger extends Activity { + nextFireDate: number = 0; + previousFireDate: number = 0; + + executedCount: number = 0; + + constructor( + name: string, + public status: ActivityStatus, + public repeatInterval: number, + public repeatCount: number|null, + public initialDelay: number, + public startDate: number|null, + public endDate: number|null, + public persistAfterExecution: boolean, + public duration: number){ + + super(name); + } + + getStatus() { + return this.status; + } + + setStatus(status: ActivityStatus) { + this.status = status; + } + + isDone(): boolean { + if (this.repeatCount !== null && this.executedCount >= this.repeatCount) { + return true; + } + + if (this.endDate !== null && this.endDate < new Date().getTime()) { + return true; + } + + return false; + } +} + +export class SchedulerEvent { + constructor( + public id: number, + public date: number, + public scope: SchedulerEventScope, + public eventType: SchedulerEventType, + public itemKey: string | null, + public fireInstanceId?: string, + public faulted: boolean = false, + public errors: ErrorMessage[] | null = null + ){} +} + +export class FakeScheduler { + startedAt: number|null = null; + status:SchedulerStatus = SchedulerStatus.Ready; + + private _groups: JobGroup[] = []; + private _triggers: Trigger[] = []; + private _events: SchedulerEvent[] = []; + + private _fireInstanceId = 1; + private _latestEventId = 1; + + private _timer = new Timer(); + + jobsExecuted = 0; + inProgress: { trigger: Trigger, fireInstanceId: string, startedAt: number, completesAt: number }[] = []; + + constructor( + public name: string, + private schedule: Schedule + ) {} + + private mapTrigger(name: string, duration: number, trigger: ScheduleTrigger){ + return new Trigger( + name, + trigger.pause ? ActivityStatus.Paused : ActivityStatus.Active, + trigger.repeatInterval, + trigger.repeatCount || null, + trigger.initialDelay || 0, + trigger.startDate || null, + trigger.endDate || null, + !!trigger.persistAfterExecution, + duration); + } + + init() { + const + mapJob = (name: string, data: ScheduleJob) => new Job( + name, + data.duration, + Object.keys(data.triggers).map( + key => this.mapTrigger(key, data.duration, data.triggers[key]))), + + mapJobGroup = (name: string, data: ScheduleGroup) => new JobGroup( + name, + Object.keys(data).map(key => mapJob(key, data[key]))); + + this._groups = Object.keys(this.schedule).map( + key => mapJobGroup(key, this.schedule[key])); + + this._triggers = this._groups.flatMap( + g => g.jobs.flatMap(j => j.triggers)); + } + + + private initTrigger(trigger: Trigger) { + trigger.startDate = trigger.startDate || new Date().getTime(), + trigger.nextFireDate = trigger.startDate + trigger.initialDelay; + } + + start() { + const now = new Date().getTime(); + if (this.startedAt === null) { + this.startedAt = now; + } + + this.status = SchedulerStatus.Started; + + this._triggers.forEach(trigger => { + this.initTrigger(trigger); + }); + + this.pushEvent(SchedulerEventScope.Scheduler, SchedulerEventType.Resumed, null); + this.doStateCheck(); + } + + getData() { + return { + name: this.name, + groups: this._groups, + jobsCount: this._groups.flatMap(g => g.jobs).length + }; + } + + findEvents(minEventId: number) { + return this._events.filter(ev => ev.id > minEventId); + } + + + private doStateCheck() { + this._timer.reset(); + + const + now = new Date().getTime(), + + triggersToStop = this.inProgress.filter(item => { + return item.completesAt <= now; + }); + + triggersToStop.forEach(item => { + const index = this.inProgress.indexOf(item); + this.inProgress.splice(index, 1); + this.jobsExecuted++; + item.trigger.executedCount++; + this.pushEvent(SchedulerEventScope.Trigger, SchedulerEventType.Complete, item.trigger.name, item.fireInstanceId); + }); + + if (this.status === SchedulerStatus.Started) { + const triggersToStart = this._triggers.filter(trigger => { + return trigger.status === ActivityStatus.Active && + (!trigger.isDone()) && + trigger.nextFireDate <= now && + !this.isInProgress(trigger); + }); + + triggersToStart.forEach(trigger => { + const fireInstanceId = (this._fireInstanceId++).toString(); + + trigger.previousFireDate = now; + trigger.nextFireDate = now + trigger.repeatInterval; + + this.inProgress.push({ + trigger: trigger, + startedAt: now, + completesAt: now + trigger.duration, + fireInstanceId: fireInstanceId + }); + + this.pushEvent(SchedulerEventScope.Trigger, SchedulerEventType.Fired, trigger.name, fireInstanceId); + }); + } + + const triggersToDeactivate = this._triggers.filter(trigger => trigger.isDone()); + triggersToDeactivate.forEach(trigger => { + if (trigger.persistAfterExecution) { + trigger.setStatus(ActivityStatus.Complete); + } else { + this.deleteTriggerInstance(trigger); + } + }); + + let nextUpdateAt: number|null = null; + + if (this.inProgress.length > 0) { + nextUpdateAt = Math.min(...this.inProgress.map(item => item.startedAt + item.trigger.duration)); + } + + const activeTriggers = this._triggers.filter(trigger => trigger.status === ActivityStatus.Active && trigger.nextFireDate); + if (this.status !== SchedulerStatus.Shutdown && activeTriggers.length > 0) { + const nextTriggerFireAt = Math.min(...activeTriggers.map(item => item.nextFireDate)); + + nextUpdateAt = nextUpdateAt === null ? nextTriggerFireAt : Math.min(nextUpdateAt, nextTriggerFireAt); + } + + if (nextUpdateAt === null) { + if (this.status === SchedulerStatus.Shutdown) { + this._timer.dispose(); + } else { + this.status = SchedulerStatus.Empty; + } + } else { + if (this.status === SchedulerStatus.Empty) { + this.status = SchedulerStatus.Started; + } + + const nextUpdateIn = nextUpdateAt - now; + + this._timer.schedule( + () => this.doStateCheck(), + nextUpdateIn); + } + } + + private isInProgress(trigger: Trigger) { + return this.inProgress.some(item => item.trigger === trigger); + } + + private pushEvent(scope: SchedulerEventScope, eventType: SchedulerEventType, itemKey: string | null, fireInstanceId?: string) { + const faulted = Math.random() > 0.5; /* todo: failure rate per job */ + + this._events.push({ + id: this._latestEventId++, + date: new Date().getTime(), + scope: scope, + eventType: eventType, + itemKey: itemKey, + fireInstanceId: fireInstanceId, + faulted: faulted, + errors: faulted ? [new ErrorMessage(0, 'Test exception text'), new ErrorMessage(1, 'Inner exception text')] : null + }); + + while (this._events.length > 1000) { + this._events.splice(0, 1); + } + } + + private findTrigger(triggerName: string) { + const result = this._triggers.filter(t => t.name === triggerName); + return result.length > 0 ? result[0] : null; + } + + private findGroup(groupName: string) { + const result = this._groups.filter(t => t.name === groupName); + return result.length > 0 ? result[0] : null; + } + + private changeTriggerStatus(triggerName: string, status: ActivityStatus) { + const trigger = this.findTrigger(triggerName); + if (trigger) { + trigger.setStatus(status); + } + + this.doStateCheck(); + this.pushEvent(SchedulerEventScope.Trigger, this.getEventTypeBy(status), trigger?.name ?? null) + } + + private changeJobStatus(groupName: string, jobName: string, status: ActivityStatus) { + const group: JobGroup | null = this.findGroup(groupName); + if (group) { + const job = group.findJob(jobName); + + if (job) { + job.setStatus(status); + + this.doStateCheck(); + this.pushEvent(SchedulerEventScope.Job, this.getEventTypeBy(status), group.name + '.' + job.name); + } + } + } + + private changeGroupStatus(groupName: string, status: ActivityStatus) { + const group: JobGroup | null = this.findGroup(groupName); + if (group) { + group.setStatus(status); + this.doStateCheck(); + this.pushEvent(SchedulerEventScope.Group, this.getEventTypeBy(status), group.name) + } + } + + private changeSchedulerStatus(status: ActivityStatus) { + this._groups.forEach(g => g.setStatus(status)); + this.doStateCheck(); + this.pushEvent(SchedulerEventScope.Scheduler, this.getEventTypeBy(status), null); + } + + private getEventTypeBy(status: ActivityStatus): SchedulerEventType { + if (status === ActivityStatus.Paused) { + return SchedulerEventType.Paused; + } + + if (status === ActivityStatus.Active) { + return SchedulerEventType.Resumed; + } + + throw new Error('Unsupported activity status ' + status.title); + } + + resumeTrigger(triggerName: string) { + this.changeTriggerStatus(triggerName, ActivityStatus.Active); + } + + pauseTrigger(triggerName: string) { + this.changeTriggerStatus(triggerName, ActivityStatus.Paused); + } + + deleteTrigger(triggerName: string) { + const trigger = this.findTrigger(triggerName); + if (trigger) { + this.deleteTriggerInstance(trigger); + } + } + + private deleteTriggerInstance(trigger: Trigger) { + this.removeTriggerFromMap(trigger); + + const allJobs = this._groups.flatMap(g => g.jobs); + + allJobs.forEach(job => { + const triggerIndex = job.triggers.indexOf(trigger); + if (triggerIndex > -1) { + job.triggers.splice(triggerIndex, 1); + } + }); + } + + private removeTriggerFromMap(trigger: Trigger) { + const index = this._triggers.indexOf(trigger); + this._triggers.splice(index, 1); + } + + deleteJob(groupName: string, jobName: string) { + const group = this.findGroup(groupName); + const job = group?.findJob(jobName) ?? null; + + if (group && job) { + const jobIndex = group.jobs.indexOf(job); + + group.jobs.splice(jobIndex, 1); + + job.triggers.forEach(trigger => this.removeTriggerFromMap(trigger)); + } + } + + deleteGroup(groupName: string) { + const group = this.findGroup(groupName); + + if (group) { + const groupIndex = this._groups.indexOf(group); + const triggers = group.jobs.flatMap(j => j.triggers); + + this._groups.splice(groupIndex, 1); + + triggers.forEach(trigger => this.removeTriggerFromMap(trigger)); + } + } + + pauseJob(groupName: string, jobName: string) { + this.changeJobStatus(groupName, jobName, ActivityStatus.Paused); + } + + resumeJob(groupName: string, jobName: string) { + this.changeJobStatus(groupName, jobName, ActivityStatus.Active); + } + + pauseGroup(groupName: string) { + this.changeGroupStatus(groupName, ActivityStatus.Paused); + } + + resumeGroup(groupName: string) { + this.changeGroupStatus(groupName, ActivityStatus.Active); + } + + pauseAll() { + this.changeSchedulerStatus(ActivityStatus.Paused); + } + + resumeAll() { + this.changeSchedulerStatus(ActivityStatus.Active); + } + + standby() { + this.status = SchedulerStatus.Ready; + this.pushEvent(SchedulerEventScope.Scheduler, SchedulerEventType.Paused, null); + } + + shutdown() { + this.status = SchedulerStatus.Shutdown; + this._groups = []; + this._triggers = []; + this.doStateCheck(); + + alert('Fake in-browser scheduler has just been shut down. Just refresh the page to make it start again!') + } + + triggerJob(groupName: any, jobName: string, triggerName: any, triggerData: ScheduleTrigger) { + const + actualGroupName = groupName || 'Default', + group = this.findGroup(actualGroupName) || this.addGroup(actualGroupName), + job = group.findJob(jobName) || group.addJob(jobName || GuidUtils.generate()), + trigger = this.mapTrigger(triggerName || GuidUtils.generate(), job.duration, triggerData); + + job.triggers.push(trigger); + this._triggers.push(trigger); + this.initTrigger(trigger); + this.doStateCheck(); + } + + executeNow(groupName: string, jobName: string) { + this.triggerJob(groupName, jobName, null, { repeatCount: 1, repeatInterval: 1 }) + } + + private addGroup(name: string) { + const result = new JobGroup(name, []); + this._groups.push(result); + return result; + } +} + +class GuidUtils { + static generate(): string { + const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); + } +} diff --git a/src/CrystalQuartz.Application.Client2/index.html b/src/CrystalQuartz.Application.Client2/index.html new file mode 100644 index 0000000..d8489d3 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/index.html @@ -0,0 +1,238 @@ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ + + + diff --git a/src/CrystalQuartz.Application.Client2/package-lock.json b/src/CrystalQuartz.Application.Client2/package-lock.json new file mode 100644 index 0000000..0961143 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/package-lock.json @@ -0,0 +1,2587 @@ +{ + "name": "crystalquartz.application.client2", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "crystalquartz.application.client2", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "john-smith": "^4.0.1-alpha.26" + }, + "devDependencies": { + "css-loader": "^7.1.2", + "html-webpack-plugin": "^5.6.0", + "mini-css-extract-plugin": "^2.9.0", + "sass": "^1.77.3", + "sass-loader": "^14.2.1", + "ts-loader": "^9.5.1", + "typescript": "^5.4.5", + "webpack": "^5.91.0", + "webpack-bundle-analyzer": "^4.10.2", + "webpack-cli": "^5.1.4" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.25", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", + "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==", + "dev": true + }, + "node_modules/@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.12.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.14.0.tgz", + "integrity": "sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001625", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz", + "integrity": "sha512-4KE9N2gcRH+HQhpeiRZXd+1niLB/XNLAhSy4z7fI8EzcbcPoAqjNInxVHTiTwWfTIV4w096XG8OtCOCQQKPv3w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.783", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.783.tgz", + "integrity": "sha512-bT0jEz/Xz1fahQpbZ1D7LgmPYZ3iHVY39NcWWro1+hA2IvjiPeaXtfSqrQ+nXjApMvQRE2ASt1itSLRrebHMRQ==", + "dev": true + }, + "node_modules/enhanced-resolve": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz", + "integrity": "sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", + "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.3.tgz", + "integrity": "sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz", + "integrity": "sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw==", + "dev": true, + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/immutable": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "dev": true + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/john-smith": { + "version": "4.0.1-alpha.26", + "resolved": "https://registry.npmjs.org/john-smith/-/john-smith-4.0.1-alpha.26.tgz", + "integrity": "sha512-e4oXK9i+G+wtXEO6c/GHV2YTozScgAjx0RRVYAdCBgsXbVrFgHOMDXRxrMamIidMoVUwqaYAHDpp0mEhmuRE7w==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", + "integrity": "sha512-Zs1YsZVfemekSZG+44vBsYTLQORkPMwnlv+aehcxK/NLKC+EGhDB39/YePYYqx/sTk6NnYpuqikhSn7+JIevTA==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.14.0.tgz", + "integrity": "sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", + "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/sass": { + "version": "1.77.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.3.tgz", + "integrity": "sha512-WJHo+jmFp0dwRuymPmIovuxHaBntcCyja5hCB0yYY9wWrViEp4kF5Cdai98P72v6FzroPuABqu+ddLMbQWmwzA==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-loader": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.2.1.tgz", + "integrity": "sha512-G0VcnMYU18a4N7VoNDegg2OuMjYtxnqzQWARVWCIVSZwJeiL9kg8QMsuIZOplsJgTzZLF6jGxI3AClj8I9nRdQ==", + "dev": true, + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz", + "integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-loader": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true + }, + "node_modules/watchpack": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.91.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", + "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.16.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/src/CrystalQuartz.Application.Client2/package.json b/src/CrystalQuartz.Application.Client2/package.json new file mode 100644 index 0000000..45d67f1 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/package.json @@ -0,0 +1,33 @@ +{ + "name": "crystalquartz.application.client2", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "watch": "webpack --watch --progress", + "build-debug": "webpack", + "build": "npm run build-debug", + "build-release": "webpack", + "build-demo": "webpack --env.demo", + "build-dev-server": "webpack --config webpack.dev-server.config.js", + "run-dev-server": "node dist-dev-server", + "dev-server": "npm run build-dev-server && npm run run-dev-server" + }, + "author": "", + "license": "ISC", + "dependencies": { + "john-smith": "^4.0.1-alpha.26" + }, + "devDependencies": { + "css-loader": "^7.1.2", + "html-webpack-plugin": "^5.6.0", + "mini-css-extract-plugin": "^2.9.0", + "sass": "^1.77.3", + "sass-loader": "^14.2.1", + "ts-loader": "^9.5.1", + "typescript": "^5.4.5", + "webpack": "^5.91.0", + "webpack-bundle-analyzer": "^4.10.2", + "webpack-cli": "^5.1.4" + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/api/index.ts b/src/CrystalQuartz.Application.Client2/src/api/index.ts new file mode 100644 index 0000000..7b0a722 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/api/index.ts @@ -0,0 +1,276 @@ +type ApplicationStatusByCode = {[key: string]:SchedulerStatus}; + +export class SchedulerStatus { + static Offline = new SchedulerStatus(-1, 'Offline'); + static Empty = new SchedulerStatus(0, 'empty'); + static Ready = new SchedulerStatus(1, 'ready'); + static Started = new SchedulerStatus(2, 'started'); + static Shutdown = new SchedulerStatus(3, 'shutdown'); + + private static _all = [ + SchedulerStatus.Offline, + SchedulerStatus.Empty, + SchedulerStatus.Ready, + SchedulerStatus.Started, + SchedulerStatus.Shutdown]; + + private static _dictionaryByCode: ApplicationStatusByCode = SchedulerStatus._all.reduce( + (result:ApplicationStatusByCode, item:SchedulerStatus) => { + result[item.code] = item; + return result; + }, + {}); + + constructor( + public value: number, + public code: string) { + } + + static findByCode(code: string): SchedulerStatus { + return this._dictionaryByCode[code]; + } +} + +export class ActivityStatus { + static Active = new ActivityStatus(0, 'Active', 'active'); + static Paused = new ActivityStatus(1, 'Paused', 'paused'); + static Mixed = new ActivityStatus(2, 'Mixed', 'mixed'); + static Complete = new ActivityStatus(3, 'Complete', 'complete'); + + private static _dictionary: Record = { + 0: ActivityStatus.Active, + 1: ActivityStatus.Paused, + 2: ActivityStatus.Mixed, + 3: ActivityStatus.Complete + }; + + constructor( + public value: number, + public title: string, + public code: string) { + } + + static findBy(value: number) { + return ActivityStatus._dictionary[value]; + } +} + +export interface Activity { + Name: string; + Status: ActivityStatus; +} + +export interface ManagableActivity extends Activity { +} + +export interface RunningJob { + FireInstanceId: string; + UniqueTriggerKey: string; +} + +export interface SchedulerData { + Name: string; + Status: string; + InstanceId: string; + RunningSince: number | null; + JobsTotal: number; + JobsExecuted: number; + ServerInstanceMarker: number; + JobGroups: JobGroup[]; + InProgress: RunningJob[]; + Events: SchedulerEvent[]; +} + +export interface TypeInfo { + Namespace: string; + Name: string; + Assembly: string; +} + +export interface SchedulerDetails { //1 + InStandbyMode: boolean; + JobStoreClustered: boolean; + JobStoreSupportsPersistence: boolean; + JobStoreType: TypeInfo | null; + NumberOfJobsExecuted: number; + RunningSince: number|null; + SchedulerInstanceId: string; + SchedulerName: string; + SchedulerRemote: boolean; + SchedulerType: TypeInfo | null; + Shutdown: boolean; + Started: boolean; + ThreadPoolSize: number; + ThreadPoolType: TypeInfo | null; + Version: string; +} + +export interface EnvironmentData { + SelfVersion: string; + QuartzVersion: string; + DotNetVersion: string; + CustomCssUrl: string; + TimelineSpan: number; +} + +export interface JobGroup extends ManagableActivity { + Jobs: Job[]; +} + +export interface Job extends ManagableActivity { + GroupName: string; + UniqueName: string; + Triggers: Trigger[]; +} + +export interface TriggerType { + Code: string; + supportedMisfireInstructions: { [index:number]:string }; +} + +export interface SimpleTriggerType extends TriggerType { + RepeatCount: number; + RepeatInterval: number; + TimesTriggered: number; +} + +export interface CronTriggerType extends TriggerType { + CronExpression: string; +} + +export interface Trigger extends ManagableActivity { + GroupName: string; + EndDate: number | null; /* todo */ + NextFireDate: number | null; /* todo */ + PreviousFireDate: number | null; /* todo */ + StartDate: number; /* todo */ + TriggerType: TriggerType; + UniqueTriggerKey: string; +} + +export interface TriggerData { + Trigger: Trigger; +} + +/** + todo + */ +export interface Property { + Name: string; + TypeName: string; + Value: string; +} + +// todo: remove +export interface IGenericObject { + Title: string; + TypeCode: string; + Value: any; + Level?: number; +} + +export class PropertyValue { + constructor( + public typeCode: string, + public rawValue: string, + public errorMessage: string, + public nestedProperties: Property[] | null, + public isOverflow: boolean, + public kind: number) { } + + isSingle() { + return this.typeCode === 'single' || this.typeCode === 'error' || this.typeCode === '...'; + } +} + +export class Property { + constructor( + public title: string, + public value: PropertyValue) { } +} + +export interface JobProperties { + Description: string; + ConcurrentExecutionDisallowed: boolean; + PersistJobDataAfterExecution: boolean; + RequestsRecovery: boolean; + Durable: boolean; + JobType: TypeInfo; +} + +export interface JobDetails { + JobDataMap: PropertyValue | null; + JobDetails: JobProperties; +} + +export interface TriggerDetails { + trigger: Trigger; + jobDataMap: PropertyValue | null; + secondaryData: { + priority: number; + misfireInstruction: number; + description: string; + } | null; +} + +export class NullableDate { + private _isEmpty: boolean; + + constructor(private date: number | null) { + this._isEmpty = date == null; + } + + isEmpty() { + return this._isEmpty; + } + + getDate(): number | null { + return this.date; + } +} + +export class ErrorMessage { + constructor( + public level: number, + public text: string){} +} + +export class SchedulerEvent { + constructor( + public id: number, + public date: number, + public scope: SchedulerEventScope, + public eventType: SchedulerEventType, + public itemKey: string, + public fireInstanceId: string, + public faulted: boolean, + public errors: ErrorMessage[] + ){} +} + +export interface InputType { + code: string; + label: string; + hasVariants: boolean; +} + +export interface InputTypeVariant { + value: string; + label: string; +} + +export enum SchedulerEventScope { + Scheduler = 0, + Group = 1, + Job = 2, + Trigger = 3 +} + +export enum SchedulerEventType { + Fired = 0, + Complete = 1, + Paused = 2, + Resumed = 3, + Standby = 4, + Shutdown = 5 +} diff --git a/src/CrystalQuartz.Application.Client2/src/application-model.ts b/src/CrystalQuartz.Application.Client2/src/application-model.ts new file mode 100644 index 0000000..d6962bd --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/application-model.ts @@ -0,0 +1,68 @@ +import { SchedulerData, Job, JobGroup } from './api'; +import {SchedulerExplorer} from './scheduler-explorer'; +import { ObservableValue } from 'john-smith/reactive'; +import { Event } from 'john-smith/reactive/event'; + +export class ApplicationModel implements SchedulerExplorer { + private _currentData: SchedulerData | null = null; + + schedulerName = new ObservableValue(''); + autoUpdateMessage = new ObservableValue(''); + isOffline = new ObservableValue(false); + + inProgressCount = new ObservableValue(0); + + onDataChanged = new Event(); + onDataInvalidate = new Event(); + + offlineSince: number | null = null; + + setData(data: SchedulerData) { + this._currentData = data; + + this.onDataChanged.trigger(data); + if (data && data.Name && this.schedulerName.getValue() !== data.Name) { + this.schedulerName.setValue(data.Name); + } + + const inProgressValue = (data.InProgress || []).length; + if (this.inProgressCount.getValue() !== inProgressValue) { + this.inProgressCount.setValue(inProgressValue); + } + } + + getData() { + return this._currentData; + } + + /** + * Causes application to reload all job gorups, jobs and triggers. + */ + invalidateData() { + this.onDataInvalidate.trigger(null); + } + + goOffline(){ + this.offlineSince = new Date().getTime(); + if (!this.isOffline.getValue()) { + this.isOffline.setValue(true); + } + + this.autoUpdateMessage.setValue('offline'); + } + + goOnline() { + this.offlineSince = null; + if (!!this.isOffline.getValue()) { + this.isOffline.setValue(false); + } + } + + listGroups(): JobGroup[] { + if (this._currentData) { + return this._currentData.JobGroups; + } + + return []; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/application.view-model.ts b/src/CrystalQuartz.Application.Client2/src/application.view-model.ts new file mode 100644 index 0000000..319d009 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/application.view-model.ts @@ -0,0 +1,3 @@ +export class ApplicationViewModel { + public readonly id: string = ''; +} diff --git a/src/CrystalQuartz.Application.Client2/src/application.view.tsx b/src/CrystalQuartz.Application.Client2/src/application.view.tsx new file mode 100644 index 0000000..c9fc70d --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/application.view.tsx @@ -0,0 +1,7 @@ +import { ApplicationViewModel } from './application.view-model'; + +import 'john-smith/view/jsx'; + +export const ApplicationView = (viewModel: ApplicationViewModel) => { + return
test
; +} diff --git a/src/CrystalQuartz.Application.Client2/src/command-action.ts b/src/CrystalQuartz.Application.Client2/src/command-action.ts new file mode 100644 index 0000000..cc22cfc --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/command-action.ts @@ -0,0 +1,20 @@ +import Action from './global/actions/action'; +import { CommandService } from './services'; +import { ApplicationModel } from './application-model'; +import { ICommand } from './commands/contracts'; +import { SchedulerData } from './api'; + +export default class CommandAction extends Action { + constructor( + application: ApplicationModel, + commandService: CommandService, + title: string, + commandFactory: () => ICommand, + confirmText?: string) { + + super( + title, + () => commandService.executeCommand(commandFactory()).then(data => application.setData(data)), + confirmText); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/command-progress/command-progress-view-model.ts b/src/CrystalQuartz.Application.Client2/src/command-progress/command-progress-view-model.ts new file mode 100644 index 0000000..433df1c --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/command-progress/command-progress-view-model.ts @@ -0,0 +1,34 @@ +import { ICommand } from '../commands/contracts'; +import { CommandService } from '../services'; +import { ObservableValue } from 'john-smith/reactive'; + +export default class CommandProgressViewModel { + private _commands: ICommand[] = []; + + active = new ObservableValue(false); + commandsCount = new ObservableValue(0); + currentCommand = new ObservableValue(null); + + constructor(private commandService: CommandService) { + commandService.onCommandStart.listen(command => this.addCommand(command)); + commandService.onCommandComplete.listen(command => this.removeCommand(command)); + } + + private addCommand(command: ICommand) { + this._commands.push(command); + this.updateState(); + } + + private removeCommand(command: ICommand) { + this._commands = this._commands.filter(c => c !== command); + this.updateState(); + } + + private updateState() { + this.active.setValue(this._commands.length > 0); + this.commandsCount.setValue(this._commands.length); + if (this._commands.length > 0) { + this.currentCommand.setValue((this._commands[this._commands.length - 1]).message); + } + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/command-progress/command-progress-view.tsx b/src/CrystalQuartz.Application.Client2/src/command-progress/command-progress-view.tsx new file mode 100644 index 0000000..82de0e9 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/command-progress/command-progress-view.tsx @@ -0,0 +1,37 @@ +import ViewModel from './command-progress-view-model'; +import { HtmlDefinition, View } from 'john-smith/view'; + +export default class CommandProgressView implements View { + constructor(private readonly viewModel: ViewModel) { + } + + template(): HtmlDefinition { + return
+
+ +

{this.viewModel.currentCommand}

+
+
; + } + + // todo + // init(dom: js.IDom, viewModel: ViewModel) { + // dom('.js_commandMessage').observes(viewModel.currentCommand); + // + // var timer = null; + // viewModel.active.listen((value => { + // if (value) { + // if (timer) { + // clearTimeout(timer); + // timer = null; + // } + // + // dom.$.stop().show(); + // } else { + // timer = setTimeout(() => { + // dom.$.fadeOut(); + // }, 1000); + // } + // })); + // } +} diff --git a/src/CrystalQuartz.Application.Client2/src/command-progress/command-progress.tmpl.html b/src/CrystalQuartz.Application.Client2/src/command-progress/command-progress.tmpl.html new file mode 100644 index 0000000..49677ea --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/command-progress/command-progress.tmpl.html @@ -0,0 +1,6 @@ +
+
+ +

+
+
\ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/command-progress/index.less b/src/CrystalQuartz.Application.Client2/src/command-progress/index.less new file mode 100644 index 0000000..1c22ebe --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/command-progress/index.less @@ -0,0 +1,28 @@ +.progress-indicator { + width: 50%; + float: left; + display: none; + margin-top: 3px; + height: @loading-image-width; + + div { + width: 200px; + margin: 0 auto; + } + + img, p { + float: left; + } + + img { + margin-left: 5px; + } + + p { + display: inline; + color: #FFFFFF; + font-size: 11px; + line-height: 24px; + margin-left: 3px; + } +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/command-progress/index_sm.less b/src/CrystalQuartz.Application.Client2/src/command-progress/index_sm.less new file mode 100644 index 0000000..e2e0179 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/command-progress/index_sm.less @@ -0,0 +1,20 @@ +.progress-indicator { + position: absolute; + left: 50%; + top: 0; + bottom: 0; + width: @loading-image-width; + + img { + margin: 0; + } + + div { + width: @loading-image-width; + margin-left: -@loading-image-width/2; + } + + p { + display: none; + } +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/command-progress/loading.gif b/src/CrystalQuartz.Application.Client2/src/command-progress/loading.gif new file mode 100644 index 0000000..f6a1698 Binary files /dev/null and b/src/CrystalQuartz.Application.Client2/src/command-progress/loading.gif differ diff --git a/src/CrystalQuartz.Application.Client2/src/commands/abstract-command.ts b/src/CrystalQuartz.Application.Client2/src/commands/abstract-command.ts new file mode 100644 index 0000000..4d6a646 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/commands/abstract-command.ts @@ -0,0 +1,11 @@ +import { ICommand } from './contracts'; + +export abstract class AbstractCommand implements ICommand { + abstract code: string; + data: any; + abstract message: string; + + constructor() { + this.data = {}; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/commands/common-mappers.ts b/src/CrystalQuartz.Application.Client2/src/commands/common-mappers.ts new file mode 100644 index 0000000..a9aedbe --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/commands/common-mappers.ts @@ -0,0 +1,257 @@ +import { + SchedulerData, + JobGroup, + Job, + Trigger, + ActivityStatus, + RunningJob, + SchedulerEvent, + TriggerType, + SimpleTriggerType, + CronTriggerType, + TypeInfo, ErrorMessage, PropertyValue, Property +} from '../api'; + +export var SCHEDULER_DATA_MAPPER = mapSchedulerData; +export var TYPE_MAPPER = mapTypeInfo; +export var PARSE_OPTIONAL_INT = parseOptionalInt; +export var PROPERTY_VALUE_MAPPER = mapPropertyValue; +export var TRIGGER_MAPPER = mapSingleTrigger; + +function mapSchedulerData(data: any): SchedulerData { + return { + Name: data.n, + ServerInstanceMarker: data.sim, + Status: data.st, + InstanceId: data['_'], + RunningSince: data.rs ? parseInt(data.rs, 10) : null, + JobsTotal: data.jt ? parseInt(data.jt, 10) : 0, + JobsExecuted: data.je ? parseInt(data.je, 10) : 0, + JobGroups: mapJobGroups(data.jg), + InProgress: mapInProgress(data.ip), + Events: mapEvents(data.ev) + }; +} + +function mapEvents(events: any[] | null | undefined): SchedulerEvent[] { + if (!events) { + return []; + } + + return events.map((dto: any) => { + const + primary = dto['_'], + parts = parseJoined(primary, 4), + errors = dto['_err']; + + return new SchedulerEvent( + parseInt(parts[0], 10), + parseInt(parts[1], 10), + parseInt(parts[3], 10), + parseInt(parts[2], 10), + dto['k'], + dto['fid'], + !!errors, + (errors && errors !== 1) ? errors.map((err: any) => new ErrorMessage(err['l'] || 0, err['_'])) : null); + }); +} + +function mapJobGroups(groups: any[] | null | undefined): JobGroup[] { + if (!groups) { + return []; + } + + return groups.map((dto: any) => ({ + Name: dto.n, + Status: ActivityStatus.findBy(parseInt(dto.s, 10)), + Jobs: mapJobs(dto.jb) + })); +} + +function mapJobs(jobs: any[] | null | undefined): Job[] { + if (!jobs) { + return []; + } + + return jobs.map((dto: any) => ({ + Name: dto.n, + Status: ActivityStatus.findBy(parseInt(dto.s, 10)), + GroupName: dto.gn, + UniqueName: dto['_'], + Triggers: mapTriggers(dto.tr) + })); +} + +function mapTriggers(triggers: any[] | null | undefined): Trigger[] { + if (!triggers) { + return []; + } + + return triggers.map(x => mapSingleTrigger(x)!); +} + +function mapSingleTrigger(dto: any): Trigger | null { + if (!dto) { + return null; + } + + return { + Name: dto.n, + Status: ActivityStatus.findBy(parseInt(dto.s, 10)), + GroupName: dto.gn, + EndDate: parseOptionalInt(dto.ed), + NextFireDate: parseOptionalInt(dto.nfd), + PreviousFireDate: parseOptionalInt(dto.pfd), + StartDate: parseInt(dto.sd), + TriggerType: mapTriggerType(dto), + UniqueTriggerKey: dto['_'] + }; +} + +function mapTriggerType(dto: any): TriggerType { + const triggerTypeCode = dto.tc, + triggerData: string = dto.tb; + + switch (triggerTypeCode) { + case 'simple': + return parseSimpleTriggerType(triggerTypeCode, triggerData); + case 'cron': + return parseCronTriggerType(triggerTypeCode, triggerData); + default: + return { + Code: triggerTypeCode, + supportedMisfireInstructions: {} + }; + } +} + +function parseSimpleTriggerType(code: string, data: string): SimpleTriggerType { + const parts = parseJoined(data, 3); + + return { + Code: code, + RepeatCount: parseInt(parts[0], 10), + RepeatInterval: parseInt(parts[1], 10), + TimesTriggered: parseInt(parts[2], 10), + supportedMisfireInstructions: { + 1: 'Fire Now', + 2: 'Reschedule Now With Existing RepeatCount', + 3: 'Reschedule Now With RemainingRepeatCount', + 4: 'Reschedule Next With Remaining Count', + 5: 'Reschedule Next With Existing Count' + } + }; +} + +function parseCronTriggerType(code: string, data: string): CronTriggerType { + return { + Code: code, + CronExpression: data, + supportedMisfireInstructions: { + 1: 'Fire Once Now', + 2: 'Do Nothing' + } + }; +} + +function mapInProgress(inProgress: any[] | null | undefined): RunningJob[] { + if (!inProgress) { + return []; + } + + return inProgress.map((dto: any) => { + const parts = parseJoined(dto, 2); + + return { + FireInstanceId: parts[0], + UniqueTriggerKey: parts[1] + }; + }); +} + +function mapTypeInfo(data: string): TypeInfo | null { + if (!data) { + return null; + } + + const parts = parseJoined(data, 3); + + return { + Assembly: parts[0], + Namespace: parts[1], + Name: parts[2] + }; +} + +function parseOptionalInt(dto: any) { + if (dto === null || dto === undefined) { + return null; + } + + return parseInt(dto, 10); +} + +function parseJoined(dto: string, expectedCount: number): string[] { + const parts = dto.split('|'); + + if (parts.length === expectedCount) { + return parts; + } + + if (parts.length < expectedCount) { + throw new Error('Unexpected joined string: ' + + dto + + '. Expected ' + + expectedCount + + ' parts but got ' + + parts.length); + } + + const result = []; + const tail = []; + + for (var i = 0; i < parts.length; i++) { + if (i < expectedCount - 1) { + result.push(parts[i]); + } else { + tail.push(parts[i]); + } + } + + result.push(tail.join('|')); + + return result; +} + +function mapPropertyValue(data: any): PropertyValue | null { + if (!data) { + return null; + } + + const + typeCode = data["_"], + isSingle = typeCode === 'single'; + + return new PropertyValue( + data["_"], + isSingle ? data["v"] : null, + data["_err"], + isSingle ? null : mapProperties(typeCode, data['v']), + isSingle ? false : !!data['...'], + data["k"]); +} + +function mapProperties(typeCode: string, data: any | any[] | null | undefined): Property[] | null { + if (!data) { + return null; + } + + if (typeCode === 'enumerable') { + return data.map((item: any, index: number) => new Property('[' + index + ']', mapPropertyValue(item)!)); + } else if (typeCode === "object") { + return Object.keys(data).map( + key => new Property(key, mapPropertyValue(data[key])!)); + } else { + throw new Error('Unknown type code ' + typeCode); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/commands/contracts.ts b/src/CrystalQuartz.Application.Client2/src/commands/contracts.ts new file mode 100644 index 0000000..af6cb2d --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/commands/contracts.ts @@ -0,0 +1,6 @@ +export interface ICommand { + code: string; + data: any; + message: string; + mapper?: (data: any) => TOutput; +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/commands/global-commands.ts b/src/CrystalQuartz.Application.Client2/src/commands/global-commands.ts new file mode 100644 index 0000000..bb6a5bb --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/commands/global-commands.ts @@ -0,0 +1,32 @@ +import { AbstractCommand } from './abstract-command'; +import { EnvironmentData, SchedulerData } from '../api'; + +import { SCHEDULER_DATA_MAPPER } from './common-mappers'; + +export class GetEnvironmentDataCommand extends AbstractCommand { + code = 'get_env'; + message = 'Loading environment data'; + + constructor() { + super(); + } + + mapper = (data: any) => ({ + SelfVersion: data.sv, + QuartzVersion: data.qv, + DotNetVersion: data.dnv, + CustomCssUrl: data.ccss, + TimelineSpan: parseInt(data.ts, 10) + }); +} + +export class GetDataCommand extends AbstractCommand { + code = 'get_data'; + message = 'Loading scheduler data'; + + constructor() { + super(); + } + + mapper = SCHEDULER_DATA_MAPPER; +} diff --git a/src/CrystalQuartz.Application.Client2/src/commands/job-commands.ts b/src/CrystalQuartz.Application.Client2/src/commands/job-commands.ts new file mode 100644 index 0000000..380ecfa --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/commands/job-commands.ts @@ -0,0 +1,112 @@ +import { AbstractCommand } from './abstract-command'; +import { SchedulerData, JobDetails, JobProperties } from '../api'; +import { SCHEDULER_DATA_MAPPER, TYPE_MAPPER, PROPERTY_VALUE_MAPPER } from './common-mappers'; + +/* + * Job Commands + */ + +export class PauseJobCommand extends AbstractCommand { + code = 'pause_job'; + message = 'Pausing job'; + + constructor(group: string, job: string) { + super(); + + + this.data = { + group: group, + job: job + }; + } + + mapper = SCHEDULER_DATA_MAPPER; +} + +export class ResumeJobCommand extends AbstractCommand { + code = 'resume_job'; + message = 'Resuming job'; + + constructor(group: string, job: string) { + super(); + + + this.data = { + group: group, + job: job + }; + } + + mapper = SCHEDULER_DATA_MAPPER; +} + +export class DeleteJobCommand extends AbstractCommand { + code = 'delete_job'; + message = 'Deleting job'; + + constructor(group: string, job: string) { + super(); + + + this.data = { + group: group, + job: job + }; + } + + mapper = SCHEDULER_DATA_MAPPER; +} + +export class ExecuteNowCommand extends AbstractCommand { + code = 'execute_job'; + message = 'Executing job'; + + constructor(group: string, job: string) { + super(); + + this.data = { + group: group, + job: job + }; + } + + mapper = SCHEDULER_DATA_MAPPER; +} + +export class GetJobDetailsCommand extends AbstractCommand { + code = 'get_job_details'; + message = 'Loading job details'; + + constructor(group: string, job: string) { + super(); + + this.data = { + group: group, + job: job + }; + } + + mapper = mapJobDetailsData; +} + +function mapJobDetailsData(data: any): JobDetails { + return { + JobDetails: mapJobDetails(data.jd)!, + JobDataMap: PROPERTY_VALUE_MAPPER(data.jdm) + }; +} + +function mapJobDetails(data: any): JobProperties | null { + if (!data) { + return null; + } + + return { + ConcurrentExecutionDisallowed: !!data.ced, + Description: data.ds, + PersistJobDataAfterExecution: !!data.pjd, + Durable: !!data.d, + JobType: TYPE_MAPPER(data.t)!, + RequestsRecovery: !!data.rr + }; +} diff --git a/src/CrystalQuartz.Application.Client2/src/commands/job-data-map-commands.ts b/src/CrystalQuartz.Application.Client2/src/commands/job-data-map-commands.ts new file mode 100644 index 0000000..468268f --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/commands/job-data-map-commands.ts @@ -0,0 +1,51 @@ +import {AbstractCommand} from './abstract-command'; +import {InputType} from '../api'; + +import {InputTypeVariant} from '../api'; + +export class GetInputTypesCommand extends AbstractCommand { + code = 'get_input_types'; + message = 'Loading job data map types'; + + constructor() { + super(); + } + + mapper = (dto: any): InputType[] => { + if (!dto.i) { + return []; + } + + return dto.i.map((x: any) => ({ + code: x['_'], + label: x['l'], + hasVariants: !!x['v'] + })); + }; +} + +export class GetInputTypeVariantsCommand extends AbstractCommand { + code = 'get_input_type_variants'; + message = ''; // todo + + constructor(inputType: InputType) { + super(); + + this.message = 'Loading options for type ' + inputType.label; + + this.data = { + inputTypeCode: inputType.code + }; + } + + mapper = (dto: any): InputTypeVariant[] => { + if (!dto.i) { + return []; + } + + return dto.i.map((x: any) => ({ + value: x['_'], + label: x['l'] + })); + }; +} diff --git a/src/CrystalQuartz.Application.Client2/src/commands/job-group-commands.ts b/src/CrystalQuartz.Application.Client2/src/commands/job-group-commands.ts new file mode 100644 index 0000000..da59918 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/commands/job-group-commands.ts @@ -0,0 +1,49 @@ +import { AbstractCommand } from './abstract-command'; +import { SchedulerData } from '../api'; +import { SCHEDULER_DATA_MAPPER } from './common-mappers'; + +/* + * Group Commands + */ + +export class PauseGroupCommand extends AbstractCommand { + code = 'pause_group'; + message = 'Pausing group'; + + constructor(group: string) { + super(); + this.data = { + group: group + }; + } + + mapper = SCHEDULER_DATA_MAPPER; +} + +export class ResumeGroupCommand extends AbstractCommand { + code = 'resume_group'; + message = 'Resuming group'; + + constructor(group: string) { + super(); + this.data = { + group: group + }; + } + + mapper = SCHEDULER_DATA_MAPPER; +} + +export class DeleteGroupCommand extends AbstractCommand { + code = 'delete_group'; + message = 'Deleting group'; + + constructor(group: string) { + super(); + this.data = { + group: group + }; + } + + mapper = SCHEDULER_DATA_MAPPER; +} diff --git a/src/CrystalQuartz.Application.Client2/src/commands/scheduler-commands.ts b/src/CrystalQuartz.Application.Client2/src/commands/scheduler-commands.ts new file mode 100644 index 0000000..b8901b9 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/commands/scheduler-commands.ts @@ -0,0 +1,94 @@ +import { AbstractCommand } from './abstract-command'; +import { SchedulerData, SchedulerDetails } from '../api'; + +import { + SCHEDULER_DATA_MAPPER, + TYPE_MAPPER, + PARSE_OPTIONAL_INT +} from './common-mappers'; + +export class StartSchedulerCommand extends AbstractCommand { + code = 'start_scheduler'; + message = 'Starting the scheduler'; + + constructor() { + super(); + } + + mapper = SCHEDULER_DATA_MAPPER; +} + +export class StopSchedulerCommand extends AbstractCommand { + code = 'stop_scheduler'; + message = 'Stopping the scheduler'; + + constructor() { + super(); + } + + mapper = SCHEDULER_DATA_MAPPER; +} + +export class GetSchedulerDetailsCommand extends AbstractCommand { + code = 'get_scheduler_details'; + message = 'Loading scheduler details'; + + constructor() { + super(); + } + + mapper = mapSchedulerDetails; +} + +function mapSchedulerDetails(data: any): SchedulerDetails { + return { + InStandbyMode: !!data.ism, + JobStoreClustered: !!data.jsc, + JobStoreSupportsPersistence: !!data.jsp, + JobStoreType: TYPE_MAPPER(data.jst), + NumberOfJobsExecuted: parseInt(data.je, 10), + RunningSince: PARSE_OPTIONAL_INT(data.rs), + SchedulerInstanceId: data.siid, + SchedulerName: data.sn, + SchedulerRemote: !!data.isr, + SchedulerType: TYPE_MAPPER(data.t), + Shutdown: !!data.isd, + Started: !!data.ist, + ThreadPoolSize: parseInt(data.tps, 10), + ThreadPoolType: TYPE_MAPPER(data.tpt), + Version: data.v + }; +} + +export class PauseSchedulerCommand extends AbstractCommand { + code = 'pause_scheduler'; + message = 'Pausing all jobs'; + + constructor() { + super(); + } + + mapper = SCHEDULER_DATA_MAPPER; +} + +export class ResumeSchedulerCommand extends AbstractCommand { + code = 'resume_scheduler'; + message = 'Resuming all jobs'; + + constructor() { + super(); + } + + mapper = SCHEDULER_DATA_MAPPER; +} + +export class StandbySchedulerCommand extends AbstractCommand { + code = 'standby_scheduler'; + message = 'Switching to standby mode'; + + constructor() { + super(); + } + + mapper = SCHEDULER_DATA_MAPPER; +} diff --git a/src/CrystalQuartz.Application.Client2/src/commands/trigger-commands.ts b/src/CrystalQuartz.Application.Client2/src/commands/trigger-commands.ts new file mode 100644 index 0000000..bc611d2 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/commands/trigger-commands.ts @@ -0,0 +1,152 @@ +import { AbstractCommand } from './abstract-command'; +import { TriggerDetails, SchedulerData, TypeInfo} from '../api'; +import { + PARSE_OPTIONAL_INT, + PROPERTY_VALUE_MAPPER, + SCHEDULER_DATA_MAPPER, + TRIGGER_MAPPER, + TYPE_MAPPER +} from './common-mappers'; + +/* + * Trigger Commands + */ + +export class PauseTriggerCommand extends AbstractCommand { + code = 'pause_trigger'; + message = 'Pausing trigger'; + + constructor(group: string, trigger: string) { + super(); + + this.data = { + group: group, + trigger: trigger + }; + } + + mapper = SCHEDULER_DATA_MAPPER; +} + +export class ResumeTriggerCommand extends AbstractCommand { + code = 'resume_trigger'; + message = 'Resuming trigger'; + + constructor(group: string, trigger: string) { + super(); + this.data = { + group: group, + trigger: trigger + }; + } + + mapper = SCHEDULER_DATA_MAPPER; +} + +export class DeleteTriggerCommand extends AbstractCommand { + code = 'delete_trigger'; + message = 'Deleting trigger'; + + constructor(group: string, trigger: string) { + super(); + + + this.data = { + group: group, + trigger: trigger + }; + } + + mapper = SCHEDULER_DATA_MAPPER; +} + +export interface IAddTrackerForm { + name: string; + job: string; + jobClass: string; + group: string; + triggerType: string; + cronExpression?: string; + repeatForever?: boolean; + repeatCount?: number; + repeatInterval?: number; + jobDataMap: { key: string; value: string; inputTypeCode: string; }[]; +} + +export interface AddTriggerResult { + validationErrors: { [key: string]: string }; +} + +export class AddTriggerCommand extends AbstractCommand { + code = 'add_trigger'; + message = 'Adding new trigger'; + + constructor(form: IAddTrackerForm) { + super(); + + this.data = { + name: form.name, + job: form.job, + jobClass: form.jobClass, + group: form.group, + triggerType: form.triggerType, + cronExpression: form.cronExpression, + repeatForever: form.repeatForever, + repeatCount: form.repeatCount, + repeatInterval: form.repeatInterval + }; + + if (form.jobDataMap) { + var index = 0; + form.jobDataMap.forEach(x => { + this.data['jobDataMap[' + index + '].Key'] = x.key; + this.data['jobDataMap[' + index + '].Value'] = x.value; + this.data['jobDataMap[' + index + '].InputTypeCode'] = x.inputTypeCode; + + index++; + }); + + } + } + + mapper = (dto: any): AddTriggerResult => ({ validationErrors: dto['ve'] }); +} + +export class GetTriggerDetailsCommand extends AbstractCommand { + code = 'get_trigger_details'; + message = 'Loading trigger details'; + + constructor(group: string, trigger: string) { + super(); + + this.data = { + group: group, + trigger: trigger + }; + } + + mapper = mapJobDetailsData; +} + +function mapJobDetailsData(data: any): TriggerDetails { + return { + jobDataMap: PROPERTY_VALUE_MAPPER(data.jdm), + trigger: TRIGGER_MAPPER(data.t)!, + secondaryData: data.ts ? { + priority: parseInt(data.ts.p, 10), + misfireInstruction: parseInt(data.ts.mfi), + description: data.ts.d + } : null + }; +} + +export class GetJobTypesCommand extends AbstractCommand { + code = 'get_job_types'; + message = 'Loading allowed job types'; + + constructor() { + super(); + } + + mapper = (dto: any): TypeInfo[] => dto.i.map(TYPE_MAPPER); +} diff --git a/src/CrystalQuartz.Application.Client2/src/common.scss b/src/CrystalQuartz.Application.Client2/src/common.scss new file mode 100644 index 0000000..6b8f4a3 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/common.scss @@ -0,0 +1,86 @@ +@import "global/constants"; + +/* Common styles */ + +body { + background: #F8F8F8; +} + +html, +body, +#application { + height: 100%; +} + +/* Common (Header and Content) */ + +.ellipsis-container { + .ellipsis { + display: none; + } + + &:hover { + text-decoration: none; + + .ellipsis { + display: inline-block; + } + } +} + +.ellipsis { + height: 12px; + padding: 0 5px 5px; + font-size: 12px; + font-weight: bold; + line-height: 6px; + color: #555555; + text-decoration: none; + vertical-align: middle; + background: #DDDDDD; + border: 0; + border-radius: 1px; +} + +.data-header-item, +.data-item { + width: 16.667%; + float: left; + border-right: 1px solid #CCCCCC; + font-size: 12px; + padding: 0 10px; + line-height: 20px; + text-align: left; + text-overflow: ellipsis; + overflow: hidden; +} + +.data-header-item:last-child, +.data-item:last-child { + border-right: none; +} + +/* Main Footer */ + +.main-footer { + position: fixed; + left: $aside-width; + right: 0; + bottom: 0; + height: $main-footer-height; + line-height: $main-footer-height; + background: #EEEEEE; + border-top: 1px solid #DDDDDD; + color: #666666; + padding: 0 10px; + font-size: 11px; + + .cq-version-container { + float: left; + margin-right: 20px; + } + + .cq-version { + font-weight: bold; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/common_sm.less b/src/CrystalQuartz.Application.Client2/src/common_sm.less new file mode 100644 index 0000000..bcfa79e --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/common_sm.less @@ -0,0 +1,20 @@ +.scrollable-area { + margin-top: @main-header-primary-height + 2 * @main-header-secondary-height; +} + +.data-header .primary-data, +.data-header .ticks-container, +.data-row .primary-data { + width: 100%; + float: none; + position: relative; +} + +.data-row.data-row-trigger { + height: @data-row-height + @timeline-row-height-sm; +} + +.data-row .dropdown-menu { + right: 0; + left: auto; +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/data-loader.ts b/src/CrystalQuartz.Application.Client2/src/data-loader.ts new file mode 100644 index 0000000..1162713 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/data-loader.ts @@ -0,0 +1,156 @@ +import { ApplicationModel } from './application-model'; +import { CommandService, ErrorInfo } from './services'; +import { GetDataCommand } from './commands/global-commands'; +import { SchedulerData, Job, Trigger, ActivityStatus } from './api'; + +import { Timer } from "./global/timers/timer"; + +export class DataLoader { + private static DEFAULT_UPDATE_INTERVAL = 30000; // 30sec + private static MAX_UPDATE_INTERVAL = 300000; // 5min + private static MIN_UPDATE_INTERVAL = 10000; // 10sec + private static DEFAULT_UPDATE_INTERVAL_IN_PROGRESS = 20000; // 20sec + + private _autoUpdateTimer = new Timer(); + + constructor( + private applicationModel: ApplicationModel, + private commandService: CommandService) { + + applicationModel.onDataChanged.listen(data => this.setData(data)); + applicationModel.onDataInvalidate.listen(data => this.invalidateData()); + applicationModel.isOffline.listen(isOffline => { + if (isOffline) { + this.goOffline() + } + }); + } + + start() { + this.updateData(); + } + + private goOffline() { + this.resetTimer(); + } + + private invalidateData() { + this.resetTimer(); + this.updateData(); + } + + private setData(data: SchedulerData) { + this.resetTimer(); + + const + nextUpdateDate = this.calculateNextUpdateDate(data), + sleepInterval = this.calculateSleepInterval(nextUpdateDate); + + this.scheduleUpdateIn(sleepInterval); + } + + private scheduleRecovery() { + this.scheduleUpdateIn(DataLoader.DEFAULT_UPDATE_INTERVAL); + } + + private scheduleUpdateIn(sleepInterval: number) { + const now = new Date(), + actualUpdateDate = new Date(now.getTime() + sleepInterval), + message = 'next update at ' + actualUpdateDate.toTimeString(); + + this.applicationModel.autoUpdateMessage.setValue(message); + + this._autoUpdateTimer.schedule(() => { + this.updateData(); + }, sleepInterval); + } + + private resetTimer() { + this._autoUpdateTimer.reset(); + } + + private calculateSleepInterval(nextUpdateDate: Date) { + var now = new Date(), + sleepInterval = nextUpdateDate.getTime() - now.getTime(); + + if (sleepInterval < 0) { + // updateDate is in the past, the scheduler is probably not started yet + return DataLoader.DEFAULT_UPDATE_INTERVAL; + } + + if (sleepInterval < DataLoader.MIN_UPDATE_INTERVAL) { + // the delay interval is too small + // we need to extend it to avoid huge amount of queries + return DataLoader.MIN_UPDATE_INTERVAL; + } + + if (sleepInterval > DataLoader.MAX_UPDATE_INTERVAL) { + // the interval is too big + return DataLoader.MAX_UPDATE_INTERVAL; + } + + return sleepInterval; + } + + private updateData() { + this.applicationModel.autoUpdateMessage.setValue('updating...'); + this.commandService + .executeCommand(new GetDataCommand()) + .then((data) => { + this.applicationModel.setData(data); + }) + .catch((error: ErrorInfo) => { + if (!error.disconnected) { + this.scheduleRecovery(); + } + + // we do not schedule recovery + // if server is not available as + // this should be done by + // Offline Mode Screen + }); + } + + private getDefaultUpdateDate() { + var now = new Date(); + now.setSeconds(now.getSeconds() + 30); + return now; + } + + private getLastActivityFireDate(data: SchedulerData): Date | null { + if (data.Status !== 'started') { + return null; + } + + const allJobs = data.JobGroups.flatMap(group => group.Jobs); + const allTriggers = allJobs.flatMap(job => job.Triggers); + const activeTriggers = allTriggers.filter(trigger => trigger.Status === ActivityStatus.Active); + const nextFireDates = activeTriggers.map(trigger => trigger.NextFireDate).filter(date => date != null) as number[]; + + return nextFireDates.length > 0 ? new Date(Math.min(...nextFireDates)) : null; + } + + private getExecutingNowBasedUpdateDate(data: SchedulerData): Date | null { + if (data.InProgress && data.InProgress.length > 0) { + return this.nowPlusMilliseconds(DataLoader.DEFAULT_UPDATE_INTERVAL_IN_PROGRESS); + } + + return null; + } + + private calculateNextUpdateDate(data: SchedulerData): Date { + const + inProgressBasedUpdateDate = this.getExecutingNowBasedUpdateDate(data), + triggersBasedUpdateDate = this.getLastActivityFireDate(data) || this.getDefaultUpdateDate(); + + if (inProgressBasedUpdateDate && triggersBasedUpdateDate.getTime() > inProgressBasedUpdateDate.getTime()) { + return inProgressBasedUpdateDate; + } + + return triggersBasedUpdateDate; + } + + private nowPlusMilliseconds(value: number) { + return new Date(new Date().getTime() + value); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/activity-details/activity-details-view-model.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/activity-details/activity-details-view-model.ts new file mode 100644 index 0000000..ae89d63 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/activity-details/activity-details-view-model.ts @@ -0,0 +1,25 @@ +import { DialogViewModel } from '../dialog-view-model'; +import TimelineActivity from '../../timeline/timeline-activity'; +import { TimelineActivityViewModel} from '../../timeline/timeline-activity-view-model'; + +export default class ActivityDetailsViewModel extends DialogViewModel { + activityModel: TimelineActivityViewModel; + fireInstanceId: string; + + constructor( + private activity: TimelineActivity) { + + super(); + + this.activityModel = new TimelineActivityViewModel(activity); + this.fireInstanceId = activity.key!; // todo + } + + loadDetails() { + this.activityModel.init(); + } + + releaseState() { + this.activityModel.dispose(); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/activity-details/activity-details-view.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/activity-details/activity-details-view.ts new file mode 100644 index 0000000..91b6a1c --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/activity-details/activity-details-view.ts @@ -0,0 +1,32 @@ +import DialogViewBase from '../dialog-view-base'; + +import ViewModel from './activity-details-view-model'; + +import { ErrorsView } from '../common/errors-view'; +// import { NullableDateView } from '../../main-content/nullable-date-view'; +import { ActivityStateView } from '../../global/activities/activity-state-view'; + +// import TEMPLATE from './activity-details.tmpl.html'; + +export default class ActivityDetailsView extends DialogViewBase { + // template = TEMPLATE; + // + // init(dom: js.IDom, viewModel:ViewModel) { + // super.init(dom, viewModel); + // + // const + // activityModel = viewModel.activityModel, + // duration = activityModel.duration; + // + // dom('.js_fireInstanceId').observes(viewModel.fireInstanceId); + // dom('.js_durationValue').observes(duration.value); + // dom('.js_durationUnit').observes(duration.measurementUnit); + // + // dom('.js_startedAt').observes(activityModel.startedAt, NullableDateView); + // dom('.js_completedAt').observes(activityModel.completedAt, NullableDateView); + // dom('.js_errors').observes(activityModel.errors, ErrorsView); + // dom('.js_status').observes(activityModel.status, ActivityStateView); + // + // viewModel.loadDetails(); + // } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/activity-details/activity-details.tmpl.html b/src/CrystalQuartz.Application.Client2/src/dialogs/activity-details/activity-details.tmpl.html new file mode 100644 index 0000000..cba2c38 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/activity-details/activity-details.tmpl.html @@ -0,0 +1,45 @@ +
+
+
+ × +

Trigger fire info

+
+ +
+
+
Summary
+ + + + + + + + + + + + + + + + + + + + + +
Fire status
Trigger started at
Trigger completed at
Duration + + +
Fire instance ID
+
+ +
+
+ + +
+
\ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/activity-details/object-browser.less b/src/CrystalQuartz.Application.Client2/src/dialogs/activity-details/object-browser.less new file mode 100644 index 0000000..d3d044f --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/activity-details/object-browser.less @@ -0,0 +1,29 @@ +.object-browser { + table-layout: fixed; + border-bottom: 1px solid #DDDDDD; + width: 100%; + + .property-title, + .property-value { + width: 50%; + font-size: 12px; + padding-top: 3px; + padding-bottom: 3px; + overflow: hidden; + text-overflow: ellipsis; + } + + .property-title { + background: #F8F8F8; + border-right: 1px solid #DDDDDD; + } + + .property-value { + padding-left: 15px; + padding-right: 15px; + + &.error { + color: @error; + } + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/common/errors-view.tsx b/src/CrystalQuartz.Application.Client2/src/dialogs/common/errors-view.tsx new file mode 100644 index 0000000..d5f107c --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/common/errors-view.tsx @@ -0,0 +1,11 @@ +import { List } from 'john-smith/view/components'; +import { ErrorMessage } from '../../api'; +import 'john-smith/view'; + +export const ErrorsView = (errors: ErrorMessage[]) => +
+
Errors
+
    +
  • {errorMessage}
  • } model={errors}>
    +
+
; diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/common/job-data-map-item-view.tsx b/src/CrystalQuartz.Application.Client2/src/dialogs/common/job-data-map-item-view.tsx new file mode 100644 index 0000000..2096fc9 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/common/job-data-map-item-view.tsx @@ -0,0 +1,64 @@ +import { JobDataMapItem } from './job-data-map'; +import {InputType} from '../../api'; +import {InputTypeVariant} from '../../api'; +import { View } from 'john-smith/view'; + +export const OptionView = (viewModel: InputType) => ; + +// todo replace with SelectOptionView? +const VariantOptionView = (viewModel: InputTypeVariant) => ; + +export class JobDataMapItemView implements View { + + constructor(private readonly viewModel: JobDataMapItem) { + } + + // todo single root + template = () =>
+ + + + + + + + + × + + + + +

+ + +
; + + // init(dom: js.IDom, viewModel: JobDataMapItem): void{ + // let $valueInput = dom('.js_value'); + // + // $valueInput.$.prop('placeholder', 'Enter ' + viewModel.key + ' value'); + // dom('.js_key').observes(viewModel.key); + // $valueInput.observes(viewModel.value, { bidirectional: true }); + // + // const $inputTypeSelect = dom('.js_inputType'); + // $inputTypeSelect.observes(viewModel.inputTypes, OptionView); + // $inputTypeSelect.observes(viewModel.inputTypeCode, { bidirectional: true, command: viewModel.setInputTypeCode }); + // + // const $valueSelect = dom('.js_inputTypeVariants'); + // $valueSelect.observes(viewModel.variants, VariantOptionView); + // $valueSelect.observes(viewModel.selectedVariantValue, { bidirectional: true }); + // + // dom.manager.manage(viewModel.hasVariants.listen(hasVariants => { + // $valueInput.$.css('display', hasVariants ? 'none' : 'inline'); + // $valueSelect.$.css('display', !hasVariants ? 'none' : 'inline'); + // })); + // + // dom('.js_remove').on('click').react(viewModel.remove); + // + // const $error = dom('.js_error'); + // $error.observes(viewModel.error); + // dom.manager.manage(viewModel.error.listen(error => { + // $error.$.css('display', error ? 'block' : 'none'); + // })); + // } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/common/job-data-map.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/common/job-data-map.ts new file mode 100644 index 0000000..25eee1e --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/common/job-data-map.ts @@ -0,0 +1,72 @@ +import {InputType} from '../../api'; +import {InputTypeVariant} from '../../api'; + +import {CommandService} from '../../services'; +import {GetInputTypeVariantsCommand} from '../../commands/job-data-map-commands'; +import { ObservableList, ObservableValue } from 'john-smith/reactive'; +import { Event } from 'john-smith/reactive/event'; + +export class JobDataMapItem { + value = new ObservableValue(null); + selectedVariantValue = new ObservableValue(null); + error = new ObservableValue(null); + inputTypeCode = new ObservableValue(null); + variants = new ObservableList(); + hasVariants = new ObservableValue(false); + + onRemoved = new Event(); + + constructor( + public key: string, + public inputTypes: InputType[], + public cachedVariants: { [inputTypeCode: string]: InputTypeVariant[] }, + private commandService: CommandService) { + + if (inputTypes.length > 0) { + this.setInputTypeCode(inputTypes[0].code); + } + } + + remove() { + this.onRemoved.trigger(null); + } + + setInputTypeCode(value: string) { + this.inputTypeCode.setValue(value); + + const inputType = this.inputTypes.find(x => x.code === value); + if (inputType && inputType.hasVariants) { + if (this.cachedVariants[inputType.code]) { + this.setVariants(this.cachedVariants[inputType.code]); + } else { + this.commandService + .executeCommand(new GetInputTypeVariantsCommand(inputType)) + .then(variants => { + this.cachedVariants[inputType.code] = variants; + this.setVariants(variants); + }); + } + + this.hasVariants.setValue(true); + } else { + this.hasVariants.setValue(false); + this.variants.setValue([]); + } + } + + getActualValue(): string { + if (this.hasVariants.getValue()) { + return this.selectedVariantValue.getValue()!; + } + + return this.value.getValue()!; + } + + private setVariants(variants: InputTypeVariant[]) { + this.variants.setValue(variants); + + if (variants.length > 0) { + this.selectedVariantValue.setValue(variants[0].value); + } + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/common/object-browser/index.scss b/src/CrystalQuartz.Application.Client2/src/dialogs/common/object-browser/index.scss new file mode 100644 index 0000000..4d59132 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/common/object-browser/index.scss @@ -0,0 +1,29 @@ +.object-browser { + table-layout: fixed; + border-bottom: 1px solid #DDDDDD; + width: 100%; + + .property-title, + .property-value { + width: 50%; + font-size: 12px; + padding-top: 3px; + padding-bottom: 3px; + overflow: hidden; + text-overflow: ellipsis; + } + + .property-title { + background: #F8F8F8; + border-right: 1px solid #DDDDDD; + } + + .property-value { + padding-left: 15px; + padding-right: 15px; + + &.error { + color: $error; + } + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/common/object-browser/index.tsx b/src/CrystalQuartz.Application.Client2/src/dialogs/common/object-browser/index.tsx new file mode 100644 index 0000000..89344c2 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/common/object-browser/index.tsx @@ -0,0 +1,107 @@ +import {Property, PropertyValue} from "../../../api"; +import DateUtils from "../../../utils/date"; +import { View } from 'john-smith/view'; +import { Listenable } from 'john-smith/reactive'; +import { List, Value } from "john-smith/view/components"; + +const IS_SINGLE = (value: PropertyValue) => { + return value === null || value.isSingle(); +}; + +export var RENDER_PROPERTIES = (property: Listenable): JSX.IElement => { + return { + return IS_SINGLE(prop) ? prop : + }} model={property}>; + + //dom.observes(properties, p => IS_SINGLE(p) ? null : FlatObjectRootView) +} + +class FlatObjectItem { + constructor( + public title: string, + public value: string, + public code: string, + public level: number) { } +} + +class FlatObjectRootView implements View { + + constructor( + private readonly viewModel: PropertyValue, + ) { + } + + template = () => + + ; + + // init(dom: js.IDom, viewModel: PropertyValue) { + // const flattenViewModel = this.flatNestedProperties(viewModel, 1); + // + // dom('tbody').observes(flattenViewModel, FlatObjectItemView); + // } + // + private flatNestedProperties(value: PropertyValue, level: number): FlatObjectItem[] { + if (value.nestedProperties === null || value.nestedProperties.length === 0) { + return [ + new FlatObjectItem(value.typeCode === 'object' ? 'No properties exposed' : 'No items', '', 'empty', level) + ]; + } + + const result = + value.nestedProperties.flatMap( + (p:Property) => { + if (IS_SINGLE(p.value)) { + const singleData = this.mapSinglePropertyValue(p.value); + + return [new FlatObjectItem(p.title, singleData.value, singleData.code, level)]; + } + + const head = new FlatObjectItem(p.title, '', '', level); + + return [ + head, + ...this.flatNestedProperties(p.value, level + 1) + ]; + }); + + if (value.isOverflow) { + return [...result, new FlatObjectItem('...', 'Rest items hidden', 'overflow', level)] + } + + return result; + } + + private mapSinglePropertyValue(value: PropertyValue): { value: string, code: string } { + if (value === null) { + return { value: 'Null', code: 'null' }; + } else if (value.typeCode === 'single') { + return { value: this.formatSingleValue(value), code: 'single' }; + } else if (value.typeCode === 'error') { + return { value: value.errorMessage, code: 'error' }; + } else if (value.typeCode === '...') { + return { value: '...', code: 'overflow' }; + } + + throw new Error('Unknown type code: ' + value.typeCode); + } + + private formatSingleValue(value: PropertyValue) { + if (value.kind === 3) { + try { + return DateUtils.smartDateFormat(parseInt(value.rawValue, 10)); + } catch (e) { + } + } + + return value.rawValue; + } +} + +const FlatObjectItemView = (viewModel: FlatObjectItem) => + + {viewModel.title} + + {viewModel.value} + ; diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/common/property-view.tsx b/src/CrystalQuartz.Application.Client2/src/dialogs/common/property-view.tsx new file mode 100644 index 0000000..ce1ffd0 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/common/property-view.tsx @@ -0,0 +1,9 @@ +import { Property, PropertyType } from './property'; + +import formatter from './value-formatting'; + +export const PropertyView = (property: Property) => + + {property.title} + + ; diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/common/property.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/common/property.ts new file mode 100644 index 0000000..2361f30 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/common/property.ts @@ -0,0 +1,16 @@ +export enum PropertyType { + String, + Boolean, + Type, + Numeric, + Array, + Object, + Date +} + +export class Property { + constructor( + public title: string, + public value: any, + public valueType: PropertyType) { } +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/common/select-option-view.tsx b/src/CrystalQuartz.Application.Client2/src/dialogs/common/select-option-view.tsx new file mode 100644 index 0000000..7c8eb8a --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/common/select-option-view.tsx @@ -0,0 +1,7 @@ +import {SelectOption} from './select-option'; + +export const SelectOptionView = (viewModel: SelectOption|string) => { + const actualOption: SelectOption = (typeof viewModel === 'string') ? { title: viewModel, value: viewModel } : viewModel; + + return ; +}; diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/common/select-option.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/common/select-option.ts new file mode 100644 index 0000000..ec71c10 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/common/select-option.ts @@ -0,0 +1,4 @@ +export interface SelectOption { + title: string; + value: string; +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/render-validator.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/render-validator.ts new file mode 100644 index 0000000..688766a --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/render-validator.ts @@ -0,0 +1,21 @@ +// import { Validators } from './validators'; +// import {ValidatorView} from './validator-view'; +// +// export const RENDER_VALIDATOR = (dom: js.IListenerDom, validatorDom: js.IListenerDom, source: js.IObservable < any >, validators: Validators, observationOptions: js.ListenerOptions = null) => { +// +// dom.observes(source, observationOptions); +// var sourceValidator = validators.findFor(source); +// if (sourceValidator) { +// validatorDom.render(ValidatorView, { errors: sourceValidator.errors }); +// +// sourceValidator.errors.listen(errors => { +// if (errors && errors.length > 0) { +// dom.$.addClass('cq-error-control'); +// } else { +// dom.$.removeClass('cq-error-control'); +// } +// }); +// +// dom.on((observationOptions ? observationOptions.event : null) || 'blur').react(sourceValidator.makeDirty, sourceValidator); +// } +// } diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/validator-options.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/validator-options.ts new file mode 100644 index 0000000..04f7e2e --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/validator-options.ts @@ -0,0 +1,7 @@ +import { Listenable } from 'john-smith/reactive'; + +export interface ValidatorOptions { + source: Listenable; + key?: any; + condition?: Listenable; +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/validator-view-model.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/validator-view-model.ts new file mode 100644 index 0000000..df7b2c6 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/validator-view-model.ts @@ -0,0 +1,100 @@ +import { IValidator } from './validator'; +import { Owner, Disposable } from 'john-smith/common'; +import { Listenable, ObservableValue } from 'john-smith/reactive'; +import { combine } from 'john-smith/reactive/transformers/combine'; +import { combineAll } from 'john-smith/reactive/transformers/combine-all'; +import { map } from 'john-smith/reactive/transformers/map'; + +export class ValidatorViewModel implements Disposable { + private _owner = new Owner(); + private _errors = new ObservableValue([]); + + dirty = new ObservableValue(false); + errors!: Listenable; + validated = new ObservableValue<{ data: T, errors: string[] } | null>(null); + + constructor( + forced: Listenable, + + public key: any, + public source: Listenable, + public validators: IValidator[], + private condition: Listenable) { + + var conditionErrors = condition ? + //this.own( + combine( + condition, + this._errors, + (validationAllowed: boolean, errors: string[]) => validationAllowed ? errors : [] + ) + // js.dependentValue( + // (validationAllowed: boolean, errors: string[]) => validationAllowed ? errors : [], + // condition, + // this._errors) + //) + : + this._errors; + + this.errors = map( + combineAll([this.dirty, forced, conditionErrors]), + ([isDirty, isForced, errors]) => { + if (isForced || isDirty) { + return errors; + } + + return []; + }); + + // this.own(js.dependentValue( + // (isDirty: boolean, isForced: boolean, errors: string[]) => { + // if (isForced || isDirty) { + // return errors; + // } + // + // return []; + // }, + // this.dirty, + // forced, + // conditionErrors)); + + this._owner.own(source.listen( + (value) => { + var actualErrors = []; + for (var i = 0; i < validators.length; i++) { + const errors = validators[i](value); + if (errors) { + for (var j = 0; j < errors.length; j++) { + actualErrors.push(errors[j]); + } + } + } + + this._errors.setValue(actualErrors); + this.validated.setValue({ data: value, errors: this._errors.getValue() || [] }); + })); + } + + dispose(): void { + this._owner.dispose(); + } + + reset() { + this._errors.setValue([]); + } + + makeDirty() { + this.dirty.setValue(true); + } + + markPristine() { + this.dirty.setValue(false); + } + + hasErrors() { + const errors = this._errors.getValue(); + return errors && errors.length > 0; + // const errors = this.errors.getValue(); + // return errors && errors.length > 0; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/validator-view.tsx b/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/validator-view.tsx new file mode 100644 index 0000000..f5fb0bc --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/validator-view.tsx @@ -0,0 +1,7 @@ +import { Listenable } from 'john-smith/reactive'; +import { List } from 'john-smith/view/components'; + +export const ValidatorView = (viewModel: { errors: Listenable }) => +
    +
  • {error}
  • } model={viewModel.errors}>
    +
; diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/validator.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/validator.ts new file mode 100644 index 0000000..0fdfc0e --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/validator.ts @@ -0,0 +1,3 @@ +export interface IValidator { + (value: T): string[] | undefined; +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/validators-factory.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/validators-factory.ts new file mode 100644 index 0000000..ee151f2 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/validators-factory.ts @@ -0,0 +1,30 @@ +export class ValidatorsFactory { + static required(message: string) { + return (value: T) => { + if (!value) { + return [message]; + } + + return []; + } + } + + static isInteger(message: string) { + return (value: T) => { + if (value === null || value === undefined) { + return []; + } + + const rawValue = value.toString(); + + for (var i = 0; i < rawValue.length; i++) { + const char = rawValue.charAt(i); + if (char < '0' || char > '9') { + return [message]; + } + } + + return []; + } + } +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/validators.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/validators.ts new file mode 100644 index 0000000..60d8bda --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/common/validation/validators.ts @@ -0,0 +1,51 @@ +import { ValidatorViewModel } from './validator-view-model'; +import { IValidator } from './validator'; +import { ValidatorOptions } from './validator-options'; +import { Disposable } from 'john-smith/common'; +import { ObservableValue } from 'john-smith/reactive'; + +export class Validators implements Disposable { + private _forced = new ObservableValue(false); + + public validators: ValidatorViewModel[] = []; + + register( + options: ValidatorOptions, + ...validators: IValidator[]) { + + const result = new ValidatorViewModel( + this._forced, + options.key || options.source, + options.source, + validators, + options.condition!); + + this.validators.push(result); + + return result; + } + + findFor(key: any) { + for (var i = 0; i < this.validators.length; i++) { + if (this.validators[i].key === key) { + return this.validators[i]; + } + } + + return null; + } + + validate() { + this._forced.setValue(true); + return !this.validators.some(v => v.hasErrors()); + } + + markPristine() { + this._forced.setValue(false); + this.validators.forEach(x => x.markPristine()); + } + + dispose() { + this.validators.forEach(x => x.dispose()); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/common/value-formatting.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/common/value-formatting.ts new file mode 100644 index 0000000..9daba70 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/common/value-formatting.ts @@ -0,0 +1,23 @@ +import { PropertyType } from './property'; +import { TypeInfo } from '../../api'; +import DateUtils from '../../utils/date'; + +export default class ValueFormatter { + static format(value: any, typeCode: PropertyType) { + if (value == null || value == undefined) { + return ''; + } + + if (typeCode === PropertyType.Type) { + const type = value; + + return `${type.Namespace}.${type.Name}, ${type.Assembly}`; + } + + if (typeCode === PropertyType.Date) { + return DateUtils.smartDateFormat(value); + } + + return value.toString(); + } +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/dialog-manager.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/dialog-manager.ts new file mode 100644 index 0000000..1c07191 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/dialog-manager.ts @@ -0,0 +1,55 @@ +import { IDialogViewModel } from './dialog-view-model'; +import { ObservableList } from 'john-smith/reactive'; +import { Disposable } from 'john-smith/common'; + +export interface IDialogManager { + showModal(viewModel: IDialogViewModel, resultHandler: ((result: TResult) => void)): void; +} + +export class DialogManager implements IDialogManager { + visibleDialogs = new ObservableList>(); + + showModal(viewModel: IDialogViewModel, resultHandler: ((result: TResult) => void)) { + while (this.visibleDialogs.getValue().length > 0) { + // support only 1 visible dialog for now + this.closeTopModal(); + } + + var wiresToDispose: Disposable[] = []; + + const dispose = () => { + for (var i = 0; i < wiresToDispose.length; i++) { + wiresToDispose[i].dispose(); + } + + this.visibleDialogs.remove(viewModel); + console.log(this.visibleDialogs); + }; + + const accespedWire = viewModel.accepted.listen(result => { + resultHandler(result); + dispose(); + }); + + const canceledWire = viewModel.canceled.listen(() => { + dispose(); + }); + + wiresToDispose.push(accespedWire); + wiresToDispose.push(canceledWire); + + this.visibleDialogs.add(viewModel); + + console.log(this.visibleDialogs); + } + + closeTopModal() { + const dialogs = this.visibleDialogs.getValue(); + + if (dialogs.length > 0) { + const topDialog = dialogs[dialogs.length - 1]; + + topDialog.cancel(); + } + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/dialog-view-base.tsx b/src/CrystalQuartz.Application.Client2/src/dialogs/dialog-view-base.tsx new file mode 100644 index 0000000..9fcf264 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/dialog-view-base.tsx @@ -0,0 +1,71 @@ +import { OptionalDisposables } from 'john-smith/common'; +import { IDialogViewModel } from './dialog-view-model'; +import { DomElement, DomElementClasses, HtmlDefinition, View } from 'john-smith/view'; +import { DomEngine } from 'john-smith/view/dom-engine'; +import { OnInit } from 'john-smith/view/hooks'; + +export default abstract class DialogViewBase> implements View, OnInit { + private _classes: DomElementClasses | null = null; + + constructor(protected readonly viewModel: T, private readonly title?: string) { + } + + public onInit(root: DomElement | null, domEngine: DomEngine): OptionalDisposables { + + if (root !== null) { + const classes = root.createClassNames(); + classes.add('showing'); + + setTimeout(() => { + classes.remove('showing'); + }, 10); + } + } + + template(): HtmlDefinition { + console.log('template', this.viewModel); + + return
+
+
+ × + +

{this.title ?? 'Title'}

+
+ + {this.getBodyContent()} + {this.getFooterContent()} +
+
; + } + + protected getBodyContent(): JSX.IElement { + return todo; + } + + protected getFooterContent(): JSX.IElement { + return ; + } + + protected close() { + this.viewModel.cancel(); + } + + // abstract template: string; + + // init(dom: js.IDom, viewModel:T) { + // dom('.js_close').on('click').react(viewModel.cancel); /* todo: base class */ + // + // dom.$.addClass('showing'); + // setTimeout(() => { + // dom.$.removeClass('showing'); + // }, 10); + // + // dom.onUnrender().listen(() => { + // dom.$.addClass('showing'); + // setTimeout(() => { + // dom.$.remove(); + // }, 1000); + // }); + // } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/dialog-view-model.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/dialog-view-model.ts new file mode 100644 index 0000000..f16bdef --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/dialog-view-model.ts @@ -0,0 +1,30 @@ +import { DataState } from '../global/data-state'; +import { Event } from 'john-smith/reactive/event'; +import { ObservableValue } from 'john-smith/reactive'; + +export interface IDialogViewModel { + accepted: Event; + canceled: Event; + + cancel(): void; +} + +export class DialogViewModel implements IDialogViewModel { + accepted = new Event(); + canceled = new Event(); + state = new ObservableValue('unknown'); + errorMessage = new ObservableValue(null); + + constructor() { + this.state.setValue('unknown'); + } + + cancel() { + this.canceled.trigger({}); + } + + protected goToErrorState(message: string) { + this.state.setValue('error'); + this.errorMessage.setValue(message); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/dialogs-view-factory.tsx b/src/CrystalQuartz.Application.Client2/src/dialogs/dialogs-view-factory.tsx new file mode 100644 index 0000000..d275231 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/dialogs-view-factory.tsx @@ -0,0 +1,80 @@ +import { DialogManager } from './dialog-manager'; +import { HtmlDefinition, View, ViewDefinition } from 'john-smith/view'; +import { List, Value } from 'john-smith/view/components'; +import { IDialogViewModel } from './dialog-view-model'; + +export interface IDialogConfig { + readonly viewModel: { + new (...args: any): T + }; + readonly view: ViewDefinition; +} + +export default class DialogsViewFactory +{ + createView(config: IDialogConfig[]) { + + return class implements View { + + constructor(private readonly dialogManager: DialogManager) { + } + + // todo root element + template(): HtmlDefinition { + const viewSelector = (dialog: IDialogViewModel) => { + for (let i = 0; i < config.length; i++) { + if (dialog instanceof config[i].viewModel) { + return ; + } + } + + throw new Error('Unknown dialog view model'); + }; + + return
+
+
+ +
+
; + } + + // init(dom: js.IDom, dialogManager: DialogManager) { + + // + // const $overlay = dom('.js_dialogsOverlay').$; + // + // dom('.js_dialogs').observes(dialogManager.visibleDialogs, viewSelector); + // + // var timerRef = null; + // dom.manager.manage(dialogManager.visibleDialogs.count().listen(visibleDialogsCount => { + // if (timerRef) { + // clearTimeout(timerRef); + // timerRef = null; + // } + // + // if (visibleDialogsCount) { + // $overlay.css('display', 'block'); + // timerRef = setTimeout(() => { + // $overlay.css('opacity', '0.8'); + // }, 10); + // } else { + // $overlay.css('opacity', '0'); + // timerRef = setTimeout(() => { + // $overlay.css('display', 'none'); + // }, 1000); + // } + // })); + // + // /** + // * Handle escape button click. + // */ + // $(document).keyup(e => { + // if (e.keyCode === 27) { + // dialogManager.closeTopModal(); + // } + // }); + // } + } + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/index.scss b/src/CrystalQuartz.Application.Client2/src/dialogs/index.scss new file mode 100644 index 0000000..3725837 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/index.scss @@ -0,0 +1,160 @@ +//@import "schedule-job/index"; +@import "common/object-browser/index"; + +$dialog-vertical-padding: 20px; +$dialog-showing-shift: 150px; + +$dialog-top: $main-header-primary-height + $dialog-vertical-padding; +$dialog-bottom: $main-footer-height + $dialog-vertical-padding; +$dialog-header-height: $data-row-height * 2; + +.dialog-container, +.dialogs-overlay { + position: fixed; + width: 50%; + margin-right: -$aside-width/2; + right: 0; +} + +.dialog-container { + top: $dialog-top; + bottom: $dialog-bottom; + + transition: top 0.5s ease, bottom 0.5s ease, opacity 0.5s ease; + opacity: 1; + + &.showing { + top: $dialog-top - $dialog-showing-shift; + bottom: $dialog-bottom + $dialog-showing-shift; + opacity: 0; + } +} + +.dialog-content.dialog-content-no-padding { + padding: 0; +} + +$dialog-content-padding: 15px; + +.dialog { + height: 100%; + position: absolute; + left: 20px; + right: 20px + $aside-width/2; + background: #FFFFFF; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); + + > header { + background: #555555; + + h2 { + padding: 0 0 0 $dialog-content-padding; + margin: 0; + font-size: 16px; + line-height: $dialog-header-height; + color: #FFFFFF; + text-shadow: 1px 1px 2px #333333; + } + + a { + float: right; + height: $dialog-header-height; + width: $dialog-header-height; + line-height: $dialog-header-height; + font-size: 18px; + color: #FFFFFF; + text-align: center; + border-left: 1px solid #777777; + } + + a:hover { + text-decoration: none; + background: #333333; + } + } + + > footer { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 15px 15px 0 15px; + height: 55px; + background: #666666; + } +} + +.dialog-content { + position: absolute; + + top: $dialog-header-height; + bottom: 55px; + left: 0; + right: 0; + + padding: 15px; + overflow: auto; + + border-left: 1px solid #AAAAAA; + border-right: 1px solid #AAAAAA; + + .dialog-loading-message, + .dialog-global-error + { + padding: 15px; + } + + .dialog-global-error + { + color: $error; + } +} + +.dialogs-overlay { + top: $main-header-primary-height; + bottom: $main-footer-height; + + opacity: 0; + display: none; + background: #FFFFFF; + transition: opacity 0.5s ease; +} + +h2.dialog-header { + padding: 15px; + margin: 0; +} + +.properties-panel > header, +h2.dialog-header { + background: #E9E9E9; + border-bottom: 1px solid #DDDDDD; + font-weight: bold; + font-size: 12px; +} + +.properties-panel { + > header { + padding: 5px 15px; + + } + + > table { + width: 100%; + + tr td, + tr th { + padding: 5px 15px; + border-bottom: 1px solid #DDDDDD; + font-size: 12px; + width: 50%; + } + } + + ul.errors li { + padding: 5px 15px; + border-bottom: 1px solid #DDDDDD; + font-size: 12px; + color: $error; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/index_sm.less b/src/CrystalQuartz.Application.Client2/src/dialogs/index_sm.less new file mode 100644 index 0000000..f3ba7bd --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/index_sm.less @@ -0,0 +1,12 @@ +.dialog-container, +.dialogs-overlay { + position: fixed; + margin-right: 0; + width: auto; + left: @aside-width; + right: 0; +} + +.dialog { + right: 20px; +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/job-details/job-details-view-model.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/job-details/job-details-view-model.ts new file mode 100644 index 0000000..3975140 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/job-details/job-details-view-model.ts @@ -0,0 +1,53 @@ +import { DialogViewModel } from '../dialog-view-model'; +import { CommandService, ErrorInfo } from '../../services'; +import { Job, JobDetails, PropertyValue } from '../../api'; +import { GetJobDetailsCommand } from '../../commands/job-commands'; +import { Property, PropertyType } from '../common/property'; +import { ObservableList, ObservableValue } from 'john-smith/reactive'; + +export default class JobDetailsViewModel extends DialogViewModel { + summary = new ObservableList(); + identity = new ObservableList(); + jobDataMap = new ObservableValue(null); + + constructor( + private job: Job, + private commandService: CommandService) { + + super(); + } + + loadDetails() { + this.commandService + .executeCommand(new GetJobDetailsCommand(this.job.GroupName, this.job.Name), true) + .then(details => { + this.identity.setValue([ + new Property('Name', this.job.Name, PropertyType.String), + new Property('Group', this.job.GroupName, PropertyType.String) + ]); + + if (details.JobDetails) { + this.summary.add( + new Property('Job type', details.JobDetails.JobType, PropertyType.Type), + new Property('Description', details.JobDetails.Description, PropertyType.String), + new Property('Concurrent execution disallowed', + details.JobDetails.ConcurrentExecutionDisallowed, + PropertyType.Boolean), + new Property('Persist after execution', + details.JobDetails.PersistJobDataAfterExecution, + PropertyType.Boolean), + new Property('Requests recovery', details.JobDetails.RequestsRecovery, PropertyType.Boolean), + new Property('Durable', details.JobDetails.Durable, PropertyType.Boolean)); + + this.jobDataMap.setValue(details.JobDataMap); + + this.state.setValue('ready'); + } else { + this.goToErrorState('No details found, the Job no longer available.') + } + }) + .catch((error: ErrorInfo) => { + this.goToErrorState(error.errorMessage); + }); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/job-details/job-details-view.tsx b/src/CrystalQuartz.Application.Client2/src/dialogs/job-details/job-details-view.tsx new file mode 100644 index 0000000..a976b05 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/job-details/job-details-view.tsx @@ -0,0 +1,88 @@ +import DialogViewBase from '../dialog-view-base'; + +import ViewModel from './job-details-view-model'; +import { PropertyView } from '../common/property-view'; +import JobDetailsViewModel from './job-details-view-model'; +import { List, Value } from 'john-smith/view/components'; +import { DomElement } from 'john-smith/view'; +import { DomEngine } from 'john-smith/view/dom-engine'; +import { OptionalDisposables } from 'john-smith/common'; +import { RENDER_PROPERTIES } from '../common/object-browser'; + +// import TEMPLATE from './job-details.tmpl.html'; + +// import { RENDER_PROPERTIES } from '../common/object-browser'; +// import { CHANGE_DOM_DISPLAY } from "../schedule-job/steps/view-commons"; + +export default class JobDetailsView extends DialogViewBase { + constructor(viewModel: JobDetailsViewModel) { + super(viewModel, 'Job Details'); + } + + onInit(root: DomElement | null, domEngine: DomEngine): OptionalDisposables { + this.viewModel.loadDetails(); + return super.onInit(root, domEngine); + } + + protected getBodyContent(): JSX.IElement { + return
+ { + if (state === 'ready') { + return
+
+
Identity
+ + +
+
+
+
Summary
+ + +
+
+
+
Job Data Map
+ + {RENDER_PROPERTIES(this.viewModel.jobDataMap)} +
+
+
+ } + + if (state === 'error') { + return
{this.viewModel.errorMessage}
; + } + + return
+ Loading Job details... +
+ }} model={this.viewModel.state}>
+ + + + + +
; + } + +// template = TEMPLATE; + // + // init(dom: js.IDom, viewModel:ViewModel) { + // super.init(dom, viewModel); + // + // const stateUi = [ + // { code: 'unknown', dom: dom('.js_stateUnknown') }, + // { code: 'error', dom: dom('.js_stateError') }, + // { code: 'ready', dom: dom('.js_stateReady') } + // ]; + // + // dom.manager.manage(viewModel.state.listen(state => { + // CHANGE_DOM_DISPLAY(stateUi, state.toString()); + // })); + // + // RENDER_PROPERTIES(dom('.js_jobDataMap'), viewModel.jobDataMap); + // + // viewModel.loadDetails(); + // } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/job-details/job-details.tmpl.html b/src/CrystalQuartz.Application.Client2/src/dialogs/job-details/job-details.tmpl.html new file mode 100644 index 0000000..8e6ede3 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/job-details/job-details.tmpl.html @@ -0,0 +1,15 @@ +
+
+
+ × + +

Job details

+
+ + + + +
+
diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/index.less b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/index.less new file mode 100644 index 0000000..85575e6 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/index.less @@ -0,0 +1,149 @@ +.dialog-content-no-padding .cq-form { + padding: 15px; +} + +.cq-form input:focus, +.cq-form select:focus { + border-color: #999999; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} + +.cq-form .control-label { + text-align: left; + + sup { + color: red; + } +} + +.cq-form .form-group.last { + margin-bottom: 0; +} + +.cq-form .cq-error-control, +.cq-form .cq-error-control:focus { + border-color: #cb4437; + box-shadow: inset 0 1px 5px rgba(203, 68, 55, .2); +} + +.cq-form-separator { + margin-bottom: 15px; + border-bottom: 1px solid #EFEFEF; +} + +.cq-field-description { + font-size: 12px; + color: #CCCCCC; + margin: 5px 0 0 0; +} + +.cq-field-description a { + color: #CCCCCC; + text-decoration: underline; +} + +.cq-field-description a:hover { + text-decoration: none; +} + +.cq-field-description:hover, +.cq-field-description:hover a, +.cq-form input.cq-error-control ~ .cq-field-description, +.cq-form input.cq-error-control ~ .cq-field-description a, +.cq-form input:focus ~ .cq-field-description, +.cq-form input:focus ~ .cq-field-description a { + color: #666666; +} + +.cq-validator { + padding: 0; + margin: 0; +} + +.cq-validator, .cq-validator li { + list-style-type: none; + list-style-image: none; +} + +.cq-validator li { + font-size: 12px; + color: #FFFFFF; + background: #cb4437; + padding: 2px 10px; +} + +.job-data-map-input { + width: 100%; + border: 1px solid #DDDDDD; + + td, th { + font-size: 12px; + border-bottom: 1px solid #DDDDDD; + } + + .no-border td { + border: none; + } + + .no-padding td { + padding: 0; + } + + td { + padding: 10px; + } + + td .error { + background: #cb4437; + color: #FFFFFF; + margin: 0 10px 10px 10px; + padding: 2px 10px; + } + + thead tr { + background: #F8F8F8; + + th { + padding: 5px 10px; + } + } + + .job-data-key { + width: 30%; + } + + .job-data-input-type { + width: 30%; + } + + .job-data-remove { + + //width: 31px; + padding-left: 0; + } + + td.job-data-key { + padding: 0 10px; + } + + td input, + td select { + width: 100%; + } + + td.job-data-remove a { + display: block; + font-size: 14px; + text-align: center; + text-decoration: none; + height: 100%; + line-height: 31px; + width: 31px; + color: #333333; + + &:hover { + background: #E9E9E9; + color: #000000; + } + } +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/schedule-job-view-model.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/schedule-job-view-model.ts new file mode 100644 index 0000000..93e5a54 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/schedule-job-view-model.ts @@ -0,0 +1,183 @@ +import { IDialogViewModel } from '../dialog-view-model'; +import { SchedulerExplorer } from '../../scheduler-explorer'; + +import { CommandService } from '../../services'; +import { GetJobTypesCommand } from '../../commands/trigger-commands'; +import { TypeInfo } from '../../api'; +import { ConfigurationStep, ConfigurationStepData } from './steps/configuration-step'; +// import { GroupConfigurationStep } from './steps/group-configuration-step'; +// import { JobConfigurationStep } from './steps/job-configuration-step'; +// import { TriggerConfigurationStep } from './steps/trigger-configuration-step'; +import { AddTriggerCommand } from '../../commands/trigger-commands'; +import { AddTriggerResult } from '../../commands/trigger-commands'; +import { IAddTrackerForm } from '../../commands/trigger-commands'; +import { Owner } from 'john-smith/common'; +import { ObservableValue } from 'john-smith/reactive'; +import { Event } from 'john-smith/reactive/event'; +import { GroupConfigurationStep } from './steps/group-configuration-step'; + +export interface ScheduleJobOptions { + predefinedGroup?: string; + predefinedJob?: string; +} + +export class ConfigarationState { + static Loading = 'loading'; + static Ready = 'ready'; + static Error = 'error'; +} + +type None = {}; +const NoneInstance: None = {}; + +export class ScheduleJobViewModel /*extends Owner*/ implements IDialogViewModel/*, js.IViewModel */ { + private _steps: ConfigurationStep[] = []; + private _currentData!: ConfigurationStepData; + // private _finalStep: TriggerConfigurationStep; + + isSaving = new ObservableValue(false); + + currentStep = new ObservableValue(null); + previousStep = new ObservableValue(null); + nextStep = new ObservableValue(NoneInstance); + + state = new ObservableValue(null); + + accepted = new Event(); /* todo: base class */ + canceled = new Event(); + + constructor( + private schedulerExplorer: SchedulerExplorer, + private commandService: CommandService, + private options: ScheduleJobOptions = {}) { + + //super(); + } + + private initConfigSteps(allowedJobTypes: TypeInfo[]) { + if (allowedJobTypes.length === 0) { + this.state.setValue(ConfigarationState.Error); + return; + } + + this._currentData = { + groupName: this.options.predefinedGroup || null, + jobName: this.options.predefinedJob || null, + jobClass: null + }; + + // this._finalStep = new TriggerConfigurationStep(this.commandService); + + const steps: ConfigurationStep[] = []; + + if (this._currentData.groupName === null) { + steps.push(new GroupConfigurationStep(this.schedulerExplorer)); + } + + // if (this._currentData.jobName === null) { + // steps.push(new JobConfigurationStep(this.schedulerExplorer, allowedJobTypes)); + // } + // + // steps.push(this._finalStep); + + this._steps = steps; + + this.setCurrentStep(this._steps[0]); + + this.state.setValue(ConfigarationState.Ready); + } + + initState(): void { + this.state.setValue(ConfigarationState.Loading); + + this.commandService + .executeCommand(new GetJobTypesCommand()) + .then(data => { this.initConfigSteps(data); }); + } + + releaseState() { + } + + cancel() { + this.canceled.trigger({}); + } + + goBackOrCancel() { + const previousStep = this.previousStep.getValue(); + if (previousStep) { + this.setCurrentStep(this.previousStep.getValue()!); + } else { + this.cancel(); + } + } + + goNextOrSave() { + const currentStep = this.currentStep.getValue(); + if (currentStep && currentStep.validators && !currentStep.validators.validate()) { + return false; + } + + let nextStep = this.nextStep.getValue(); + if (nextStep !== NoneInstance) { + this.setCurrentStep(nextStep as ConfigurationStep); + } else { + if (this.isSaving.getValue()) { + return false; + } + + this.save(); + } + + return true; + } + + save() { + this.isSaving.setValue(true); + + // const form: IAddTrackerForm = { + // ...this._finalStep.composeTriggerStepData(), + // group: this._currentData.groupName, + // job: this._currentData.jobName, + // jobClass: this._currentData.jobClass + // }; + // + // this.commandService + // .executeCommand(new AddTriggerCommand(form)) + // .then((result: AddTriggerResult) => { + // if (result.validationErrors) { + // this._finalStep.displayValidationErrors(result.validationErrors); + // } else { + // this.accepted.trigger(true); + // } + // }) + // .always(() => { + // this.isSaving.setValue(false); + // }) + // .fail((reason) => { + // /* todo */ + // }); + } + + private setCurrentStep(step: ConfigurationStep) { + const previousStep = this.currentStep.getValue(); + + if (previousStep && previousStep.onLeave) { + this._currentData = previousStep.onLeave(this._currentData); + } + + if (step.validators) { + step.validators.markPristine(); + } + + if (step.onEnter) { + this._currentData = step.onEnter(this._currentData); + } + + this.currentStep.setValue(step); + + const stepIndex = this._steps.indexOf(step); + + this.previousStep.setValue(stepIndex > 0 ? this._steps[stepIndex - 1] : null); + this.nextStep.setValue(stepIndex < this._steps.length - 1 ? this._steps[stepIndex + 1] : NoneInstance); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/schedule-job-view.tsx b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/schedule-job-view.tsx new file mode 100644 index 0000000..76d6483 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/schedule-job-view.tsx @@ -0,0 +1,147 @@ +// import TEMPLATE from './schedule-job.tmpl.html'; +import { Value } from 'john-smith/view/components'; +import DialogViewBase from '../dialog-view-base'; +import { ConfigarationState, ScheduleJobViewModel } from './schedule-job-view-model'; +import { DomElement } from 'john-smith/view'; +import { DomEngine } from 'john-smith/view/dom-engine'; +import { OptionalDisposables } from 'john-smith/common'; +import { GroupConfigurationStepView } from './steps/group-configuration-step-view'; +import { GroupConfigurationStep } from './steps/group-configuration-step'; + +// import __each from 'lodash/each'; + +// import {CHANGE_DOM_DISPLAY} from './steps/view-commons'; +// import {GroupConfigurationStepView} from './steps/group-configuration-step-view'; +// import {JobConfigurationStepView} from './steps/job-configuration-step-view'; +// import {TriggerConfigurationStepView} from './steps/trigger-configuration-step-view'; +// import {GroupConfigurationStep} from './steps/group-configuration-step'; + +export class ScheduleJobView extends DialogViewBase { + constructor(viewModel: ScheduleJobViewModel) { + super(viewModel, 'Schedule Job'); + } + + + onInit(root: DomElement | null, domEngine: DomEngine): OptionalDisposables { + this.viewModel.initState(); + + return super.onInit(root, domEngine); + } + + protected getBodyContent(): JSX.IElement { + return
+ { + if (state === 'ready') { + return
+ { + if (currentStep.code === 'group') { + return + } + }} model={this.viewModel.currentStep}> +
+ +
+ +
+
; + } + + if (state === ConfigarationState.Error) { + return
+ Can not schedule a job as no allowed job types provided.
+ Please make sure you configured allowed job types. +
; + } + + return
Loading...
+ }} model={this.viewModel.state}>
+
; + } + + + protected getFooterContent(): JSX.IElement { + return ; + } + +// template = TEMPLATE; + // + // init(dom: js.IDom, viewModel: ScheduleJobViewModel): void { + // super.init(dom, viewModel); + // + // const + // $nextButton = dom('.js_nextButton'), + // $backButton = dom('.js_backButton'); + // + // const steps = [ + // { code: 'group', dom: dom('.js_stepGroup'), view: GroupConfigurationStepView }, + // { code: 'job', dom: dom('.js_stepJob'), view: JobConfigurationStepView }, + // { code: 'trigger', dom: dom('.js_stepTrigger'), view: TriggerConfigurationStepView } + // ]; + // + // const states = [ + // { code: 'loading', dom: dom('.js_stateLoading') }, + // { code: 'ready', dom: dom('.js_stateReady') }, + // { code: 'error', dom: dom('.js_stateError') } + // ]; + // + // const renderedSteps: { [code: string]: boolean } = {}; + // + // dom.manager.manage(viewModel.state.listen(state => { + // CHANGE_DOM_DISPLAY(states, state); + // })); + // + // dom.manager.manage(viewModel.currentStep.listen(currentStep => { + // if (!currentStep) { + // return; + // } + // + // CHANGE_DOM_DISPLAY(steps, currentStep.code); + // + // __each(steps, step => { + // if (step.code === currentStep.code && !renderedSteps[step.code]) { + // var t = step.dom.render(step.view, currentStep); + // t.init(); + // + // renderedSteps[step.code] = true; + // } + // }); + // })); + // + // dom.manager.manage(viewModel.nextStep.listen(step => { + // $nextButton.$.html(step ? 'Next →' : 'Save'); + // })); + // + // dom.manager.manage(viewModel.previousStep.listen(step => { + // $backButton.$.html(step ? '← ' + step.navigationLabel : 'Cancel'); + // })); + // + // $nextButton.on('click').react(() => { + // // if (viewModel.isSaving.getValue()) { + // // return; + // // } + // + // const isValid = viewModel.goNextOrSave(); + // if (!isValid) { + // $nextButton.$.addClass("effects-shake"); + // setTimeout(() => { + // $nextButton.$.removeClass("effects-shake"); + // }, 2000); + // } + // }); + // + // //$nextButton.on('click').react(viewModel.goNextOrSave); + // $backButton.on('click').react(viewModel.goBackOrCancel); + // } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/schedule-job.tmpl.html b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/schedule-job.tmpl.html new file mode 100644 index 0000000..466752d --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/schedule-job.tmpl.html @@ -0,0 +1,31 @@ +
+
+
+ × +

Schedule Job

+
+ +
+
Loading...
+ +
+
+ +
+ +
+
+ +
+ Can not schedule a job as no allowed job types provided.
+ Please make sure you configured allowed job types. +
+
+ +
+ + + +
+
+
\ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/configuration-step.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/configuration-step.ts new file mode 100644 index 0000000..4d9399a --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/configuration-step.ts @@ -0,0 +1,15 @@ +import {Validators} from '../../common/validation/validators'; + +export interface ConfigurationStepData { + groupName: string|null; + jobName: string|null; + jobClass: string|null; +} + +export interface ConfigurationStep { + code: string; + navigationLabel: string; + onEnter?: (data: ConfigurationStepData) => ConfigurationStepData; + onLeave?: (data: ConfigurationStepData) => ConfigurationStepData; + validators?: Validators; +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/group-configuration-step-view.tsx b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/group-configuration-step-view.tsx new file mode 100644 index 0000000..e7bc959 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/group-configuration-step-view.tsx @@ -0,0 +1,96 @@ +import { OptionalDisposables } from 'john-smith/common'; +import { GroupConfigurationStep, JobGroupType } from './group-configuration-step'; + +// import TEMPLATE from './group-configuration-step.tmpl.html'; +import { SelectOptionView } from '../../common/select-option-view'; +import { DomElement, HtmlDefinition, View } from 'john-smith/view'; +import { DomEngine } from 'john-smith/view/dom-engine'; +import { OnInit } from 'john-smith/view/hooks'; +import { List, Value } from 'john-smith/view/components'; +import { OptionView } from '../../common/job-data-map-item-view'; +import { ValidatorView } from '../../common/validation/validator-view'; +// import { CHANGE_DOM_DISPLAY } from './view-commons'; + +// import { RENDER_VALIDATOR } from '../../common/validation/render-validator'; + +export class GroupConfigurationStepView implements View, OnInit { + constructor( + private readonly viewModel: GroupConfigurationStep) { + } + + onInit(root: DomElement | null, domEngine: DomEngine): OptionalDisposables { + } + + template(): HtmlDefinition { + // const jobGroupTypes = [ + // { code: JobGroupType.Existing, dom: dom('.js_jobGroupTypeExisting') }, + // { code: JobGroupType.New, dom: dom('.js_jobGroupTypeNew') } + // ]; + + return
+

Job Group Configuration

+
+
+ +
+ +
+
+ +
+ + { + if (jobGroupType === JobGroupType.Existing) { + return
+ + +
+ + + {this.viewModel.validators.findFor(this.viewModel.selectedJobGroup)} +
+
+ } + + if (jobGroupType === JobGroupType.New) { + return
+ +
+ +
+
+ } + }} model={this.viewModel.jobGroupType}>
+
+
+ } + + // init(dom: js.IDom, viewModel: GroupConfigurationStep): void { + + // + // dom('.js_jobGroupSelect').observes(viewModel.jobGroupType, { bidirectional: true }); + // dom('.js_jobGroupSelect').observes(viewModel.jobGroupTypeOptions, SelectOptionView); + // + // dom.manager.manage(viewModel.jobGroupType.listen(currentJobGroupType => { + // CHANGE_DOM_DISPLAY(jobGroupTypes, currentJobGroupType); + // })); + // + // dom('.js_existingJobGroupSelect').observes(viewModel.existingJobGroups, SelectOptionView); + // + // dom('.js_newJobGroupInput').observes(viewModel.newJobGroup, { bidirectional: true }); + // + // RENDER_VALIDATOR( + // dom('.js_existingJobGroupSelect'), + // dom('.js_existingJobGroupContainer'), + // viewModel.selectedJobGroup, + // viewModel.validators); + // } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/group-configuration-step.tmpl.html b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/group-configuration-step.tmpl.html new file mode 100644 index 0000000..9e3de6c --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/group-configuration-step.tmpl.html @@ -0,0 +1,29 @@ +
+

Job Group Configuration

+
+
+ +
+ +
+
+ +
+ +
+ + +
+ +
+
+ +
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/group-configuration-step.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/group-configuration-step.ts new file mode 100644 index 0000000..59b0ced --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/group-configuration-step.ts @@ -0,0 +1,85 @@ +import { ConfigurationStep, ConfigurationStepData } from './configuration-step'; +import { SelectOption } from '../../common/select-option'; +import { SchedulerExplorer } from '../../../scheduler-explorer'; +import { Validators } from '../../common/validation/validators'; + +import { ValidatorsFactory } from '../../common/validation/validators-factory'; +import { BidirectionalValue, ObservableList, ObservableValue } from 'john-smith/reactive'; +import { Owner } from 'john-smith/common'; +import { map } from 'john-smith/reactive/transformers/map'; + +export class JobGroupType { + static None = 'none'; + static Existing = 'existing'; + static New = 'new'; +} + +export class GroupConfigurationStep /*extends Owner*/ implements ConfigurationStep { + code = 'group'; + navigationLabel = 'Configure Group'; + + jobGroupType = new BidirectionalValue(value => true, JobGroupType.None); + jobGroupTypeOptions = new ObservableList(); + existingJobGroups = new ObservableList(); + selectedJobGroup = new ObservableValue(''); + newJobGroup = new ObservableValue(''); + + validators = new Validators(); + + constructor( + private schedulerExplorer: SchedulerExplorer) { + + // super(); + + const groups = schedulerExplorer.listGroups().map(g => ({ value: g.Name, title: g.Name})); + + this.existingJobGroups.setValue([ + { value: '', title: '- Select a Job Group -'}, + ...groups + ]); + + this.jobGroupTypeOptions.add({ value: JobGroupType.None, title: 'Not specified (Use default)' }); + if (groups.length > 0) { + this.jobGroupTypeOptions.add({ value: JobGroupType.Existing, title: 'Use existing group' }); + } + + this.jobGroupTypeOptions.add({ value: JobGroupType.New, title: 'Define new group' }); + + this.jobGroupType.setValue(JobGroupType.None); + + this.validators.register( + { + source: this.selectedJobGroup, + condition: map(this.jobGroupType, x => x === JobGroupType.Existing) + }, + ValidatorsFactory.required('Please select a group')); + + //this.own(this.validators); + } + + onLeave(data: ConfigurationStepData): ConfigurationStepData { + return { + groupName: this.getGroupName(), + jobClass: data.jobClass, + jobName: data.jobName + } + } + + getGroupName(): string|null { + const jobGroupType = this.jobGroupType.getValue(); + + if (jobGroupType === JobGroupType.None) { + return null; + } else if (jobGroupType === JobGroupType.Existing) { + return this.selectedJobGroup.getValue(); + } else if (jobGroupType === JobGroupType.New) { + return this.newJobGroup.getValue(); + } + + return null; + } + + releaseState() { + //this.dispose(); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/job-configuration-step-view.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/job-configuration-step-view.ts new file mode 100644 index 0000000..c8952f1 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/job-configuration-step-view.ts @@ -0,0 +1,43 @@ +// import TEMPLATE from './job-configuration-step.tmpl.html'; +// import { SelectOptionView } from '../../common/select-option-view'; +// import { CHANGE_DOM_DISPLAY } from './view-commons'; +// import { JobConfigurationStep } from './job-configuration-step'; +// import { JobType } from './job-configuration-step'; +// import {RENDER_VALIDATOR} from '../../common/validation/render-validator'; +// +// export class JobConfigurationStepView implements js.IView { +// template = TEMPLATE; +// +// init(dom: js.IDom, viewModel: JobConfigurationStep): void { +// const jobTypes = [ +// { code: JobType.Existing, dom: dom('.js_jobTypeExisting') }, +// { code: JobType.New, dom: dom('.js_jobTypeNew') } +// ]; +// +// dom('.js_jobKindSelect').observes(viewModel.jobTypeOptions, SelectOptionView); +// dom('.js_jobKindSelect').observes(viewModel.jobType, { bidirectional: true }); +// +// dom('.js_existingJobSelect').observes(viewModel.existingJobs, SelectOptionView); +// //dom('.js_existingJobSelect').observes(viewModel.selectedJob, { bidirectional: true }); +// +// dom.manager.manage(viewModel.jobType.listen(currentJobType => { +// CHANGE_DOM_DISPLAY(jobTypes, currentJobType); +// })); +// +// dom('.js_newJobClassSelect').observes(viewModel.allowedJobTypes, SelectOptionView); +// +// dom('.js_newJobNameInput').observes(viewModel.newJobName, { bidirectional: true }); +// +// RENDER_VALIDATOR( +// dom('.js_existingJobSelect'), +// dom('.js_existingJobContainer'), +// viewModel.selectedJob, +// viewModel.validators); +// +// RENDER_VALIDATOR( +// dom('.js_newJobClassSelect'), +// dom('.js_newJobClassContainer'), +// viewModel.newJobClass, +// viewModel.validators); +// } +// } diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/job-configuration-step.tmpl.html b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/job-configuration-step.tmpl.html new file mode 100644 index 0000000..f156ef7 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/job-configuration-step.tmpl.html @@ -0,0 +1,34 @@ +

Job Configuration

+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/job-configuration-step.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/job-configuration-step.ts new file mode 100644 index 0000000..8abb718 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/job-configuration-step.ts @@ -0,0 +1,154 @@ +// import { ConfigurationStep, ConfigurationStepData } from './configuration-step'; +// import { SelectOption } from '../../common/select-option'; +// +// import __map from 'lodash/map'; +// import __find from 'lodash/find'; +// import { SchedulerExplorer } from '../../../scheduler-explorer'; +// import { TypeInfo } from '../../../api'; +// import { Validators } from '../../common/validation/validators'; +// import { MAP } from '../../../global/map'; +// import { JobGroupType } from './group-configuration-step'; +// import { ValidatorsFactory } from '../../common/validation/validators-factory'; +// import {Owner} from '../../../global/owner'; +// +// export class JobType { +// static Existing = 'existing'; +// static New = 'new'; +// } +// +// export class JobConfigurationStep extends Owner implements ConfigurationStep { +// code = 'job'; +// navigationLabel = 'Configure Job'; +// +// jobType = new js.ObservableValue(); +// jobTypeOptions = new js.ObservableList(); +// existingJobs = new js.ObservableList(); +// selectedJob = new js.ObservableValue(); +// newJobName = new js.ObservableValue(); +// newJobClass = new js.ObservableValue(); +// allowedJobTypes = new js.ObservableList(); +// +// validators = new Validators(); +// +// constructor( +// private schedulerExplorer: SchedulerExplorer, +// allowedJobTypes: TypeInfo[]) { +// +// super(); +// +// const values = __map( +// allowedJobTypes, +// type => { +// const formattedType = type.Namespace + '.' + type.Name + ', ' + type.Assembly; +// return ({ value: formattedType, title: formattedType }); +// }); +// +// this.allowedJobTypes.setValue([ +// { value: '', title: '- Select a Job Class -' }, +// ...values]); +// +// this.validators.register( +// { +// source: this.selectedJob, +// condition: this.own(MAP(this.jobType, x => x === JobType.Existing)) +// }, +// ValidatorsFactory.required('Please select a Job')); +// +// this.validators.register( +// { +// source: this.newJobClass, +// condition: this.own(MAP(this.jobType, x => x === JobType.New)) +// }, +// ValidatorsFactory.required('Please select a Job Class')); +// +// this.own(this.validators); +// } +// +// onEnter(data: ConfigurationStepData): ConfigurationStepData { +// const +// selectedJobGroupName = data.groupName || 'Default', +// jobGroup = __find(this.schedulerExplorer.listGroups(), x => x.Name === selectedJobGroupName), +// options: SelectOption[] = [ +// { title: 'Define new job', value: JobType.New } +// ]; +// +// const canUseExistingJob = jobGroup && jobGroup.Jobs && jobGroup.Jobs.length > 0; +// if (canUseExistingJob) { +// options.push({ title: 'Use existing job', value: JobType.Existing }); +// +// const existingJobsOption = [ +// { value: '', title: '- Select a Job -' }, +// ...__map(jobGroup.Jobs, j => ({ value: j.Name, title: j.Name }))]; +// +// this.existingJobs.setValue(existingJobsOption); +// this.selectedJob.setValue(this.selectedJob.getValue()); +// } +// +// const +// currentJobType = this.jobType.getValue(), +// existingJobType = currentJobType === JobType.Existing, +// shouldResetSelectedJob = existingJobType && +// ((!jobGroup) || +// !__find(jobGroup.Jobs, j => j.Name === this.selectedJob.getValue())); +// +// if (shouldResetSelectedJob) { +// this.selectedJob.setValue(null); +// } +// +// this.jobTypeOptions.setValue(options); +// +// const shouldResetJobType = currentJobType == null || (!canUseExistingJob && existingJobType); +// +// if (shouldResetJobType) { +// this.jobType.setValue(JobType.New); +// } else { +// this.jobType.setValue(currentJobType); +// } +// +// return data; +// } +// +// onLeave(data: ConfigurationStepData): ConfigurationStepData { +// return { +// groupName: data.groupName, +// jobName: this.getJobName(), +// jobClass: this.getJobClass() +// }; +// } +// +// getJobName(): string { +// const jobType = this.jobType.getValue(); +// +// switch (jobType) { +// case JobType.New: { +// return this.newJobName.getValue(); +// } +// case JobType.Existing: { +// return this.selectedJob.getValue(); +// } +// default: { +// throw new Error('Unknown job type ' + jobType); +// } +// } +// } +// +// getJobClass(): string | null { +// const jobType = this.jobType.getValue(); +// +// switch (jobType) { +// case JobType.New: { +// return this.newJobClass.getValue(); +// } +// case JobType.Existing: { +// return null; +// } +// default: { +// throw new Error('Unknown job type ' + jobType); +// } +// } +// } +// +// releaseState() { +// this.dispose(); +// } +// } diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/trigger-configuration-step-view.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/trigger-configuration-step-view.ts new file mode 100644 index 0000000..7e07fa5 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/trigger-configuration-step-view.ts @@ -0,0 +1,112 @@ +// // import TEMPLATE from './trigger-configuration-step.tmpl.html'; +// import { SelectOptionView } from '../../common/select-option-view'; +// import { CHANGE_DOM_DISPLAY } from './view-commons'; +// import { JobConfigurationStep } from './job-configuration-step'; +// import { JobType } from './job-configuration-step'; +// import {TriggerConfigurationStep} from './trigger-configuration-step'; +// import {JobDataMapItemView} from '../../common/job-data-map-item-view'; +// import {RENDER_VALIDATOR} from '../../common/validation/render-validator'; +// +// export class TriggerConfigurationStepView implements js.IView { +// template = TEMPLATE; +// +// init(dom: js.IDom, viewModel: TriggerConfigurationStep): void { +// dom('.triggerName').observes(viewModel.triggerName); +// dom('.triggerType').observes(viewModel.triggerType); +// dom('.repeatForever').observes(viewModel.repeatForever); +// +// var $repeatCount = dom('.repeatCount'); +// dom('.repeatIntervalType').observes(viewModel.repeatIntervalType); +// +// RENDER_VALIDATOR( +// dom('.cronExpression'), +// dom('.cronExpressionContainer'), +// viewModel.cronExpression, +// viewModel.validators); +// +// RENDER_VALIDATOR( +// dom('.repeatInterval'), +// dom('.repeatIntervalContainer'), +// viewModel.repeatInterval, +// viewModel.validators); +// +// RENDER_VALIDATOR( +// dom('.repeatCount'), +// dom('.repeatCountContainer'), +// viewModel.repeatCount, +// viewModel.validators); +// +// var $simpleTriggerDetails = dom('.simpleTriggerDetails'); +// var $cronTriggerDetails = dom('.cronTriggerDetails'); +// +// var triggersUi = [ +// { code: 'Simple', dom: $simpleTriggerDetails }, +// { code: 'Cron', dom: $cronTriggerDetails } +// ]; +// +// dom.manager.manage(viewModel.triggerType.listen(value => { +// CHANGE_DOM_DISPLAY(triggersUi, value); +// +// // for (var i = 0; i < triggersUi.length; i++) { +// // var triggerData = triggersUi[i]; +// // if (triggerData.code === value) { +// // triggerData.element.show(); +// // } else { +// // triggerData.element.hide(); +// // } +// // } +// })); +// +// // const $saveButton = dom('.save'); +// // dom('.cancel').on('click').react(viewModel.cancel); +// // $saveButton.on('click').react(() => { +// // if (viewModel.isSaving.getValue()) { +// // return; +// // } +// // +// // const isValid = viewModel.save(); +// // if (!isValid) { +// // $saveButton.$.addClass("effects-shake"); +// // setTimeout(() => { +// // $saveButton.$.removeClass("effects-shake"); +// // }, 2000); +// // } +// // }); +// +// dom.manager.manage(viewModel.repeatForever.listen(value => { +// $repeatCount.$.prop('disabled', value); +// })); +// +// // var saveText; +// // viewModel.isSaving.listen((value: boolean) => { +// // if (value) { +// // saveText = $saveButton.$.text(); +// // $saveButton.$.text('...'); +// // } else if (saveText) { +// // $saveButton.$.text(saveText); +// // } +// // }); +// +// viewModel.repeatIntervalType.setValue('Milliseconds'); // todo: move to vm +// viewModel.triggerType.setValue('Simple'); +// +// RENDER_VALIDATOR( +// dom('.js_jobDataKey'), +// dom('.js_jobDataKeyContainer'), +// viewModel.newJobDataKey, +// viewModel.validators, +// { bidirectional: true, event: 'keyup' }); +// +// let $jobDataMapSection = dom('.js_jobDataMapSection'); +// let $jobDataMap = dom('.js_jobDataMap'); +// $jobDataMap.observes(viewModel.jobDataMap, JobDataMapItemView); +// +// let $addJobDataKeyButton = dom('.js_addJobDataMapItem'); +// $addJobDataKeyButton.on('click').react(viewModel.addJobDataMapItem); +// +// dom.manager.manage(viewModel.canAddJobDataKey.listen(value => $addJobDataKeyButton.$.prop('disabled', !value))); +// dom.manager.manage(viewModel.jobDataMap.count().listen(itemsCount => { +// $jobDataMapSection.$.css('display', itemsCount > 0 ? 'block' : 'none'); +// })); +// } +// } diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/trigger-configuration-step.tmpl.html b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/trigger-configuration-step.tmpl.html new file mode 100644 index 0000000..451ebe7 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/trigger-configuration-step.tmpl.html @@ -0,0 +1,105 @@ +

Trigger Configuration

+
+
+ +
+ + +

+ Optional trigger friendly name. Quartz will generate a guid if empty. +

+
+
+ +
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+
+
+ +
+
+
+ +
+ + +
+ +
+ +
+ +
+
+
+ +
+
+
+ +
+ +
+

+ Read more about cron format at Quartz.NET docs +

+
+
+
+
+ +
+ +
+ + +
+ +
+ +
+ +
+
+ +
+
+
+ + + + + + + + + + +
KeyTypeValue
+
+
+
\ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/trigger-configuration-step.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/trigger-configuration-step.ts new file mode 100644 index 0000000..dac1560 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/trigger-configuration-step.ts @@ -0,0 +1,195 @@ +// import {ConfigurationStep} from './configuration-step'; +// import {JobDataMapItem} from '../../common/job-data-map'; +// import {Validators} from '../../common/validation/validators'; +// import {InputType} from '../../../api'; +// import {InputTypeVariant} from '../../../api'; +// +// import __some from 'lodash/some'; +// import __map from 'lodash/map'; +// import __each from 'lodash/each'; +// import __forOwn from 'lodash/forOwn'; +// +// import {MAP} from '../../../global/map'; +// import {ValidatorsFactory} from '../../common/validation/validators-factory'; +// import {Owner} from '../../../global/owner'; +// import {CommandService} from '../../../services'; +// import {GetInputTypesCommand} from '../../../commands/job-data-map-commands'; +// import { ObservableValue } from 'john-smith/reactive'; +// +// export interface TriggerStepData { +// name: string; +// triggerType: string; +// cronExpression?: string; +// repeatForever?: boolean; +// repeatCount?: number; +// repeatInterval?: number; +// jobDataMap: { key: string; value: string; inputTypeCode: string; }[]; +// } +// +// export class TriggerConfigurationStep extends Owner implements ConfigurationStep { +// code = 'trigger'; +// navigationLabel = ''; +// +// triggerName = new ObservableValue(); +// triggerType = new ObservableValue(); +// cronExpression = js.observableValue(); +// repeatForever = js.observableValue(); +// repeatCount = js.observableValue(); +// repeatInterval = js.observableValue(); +// repeatIntervalType = js.observableValue(); +// +// jobDataMap = new js.ObservableList(); +// +// newJobDataKey = new js.ObservableValue(); +// canAddJobDataKey: js.ObservableValue; +// +// validators = new Validators(); +// +// private _inputTypes: InputType[]; +// private _inputTypesVariants: { [inputTypeCode: string]: InputTypeVariant[] } = {}; +// +// constructor( +// private commandService: CommandService) { +// +// super(); +// +// const isSimpleTrigger = this.own(MAP(this.triggerType, x => x === 'Simple')); +// +// this.validators.register( +// { +// source: this.cronExpression, +// condition: this.own(MAP(this.triggerType, x => x === 'Cron')) +// }, +// ValidatorsFactory.required('Please enter cron expression')); +// +// this.validators.register( +// { +// source: this.repeatCount, +// condition: this.own(js.dependentValue( +// (isSimple: boolean, repeatForever: boolean) => isSimple && !repeatForever, +// isSimpleTrigger, this.repeatForever)) +// }, +// ValidatorsFactory.required('Please enter repeat count'), +// ValidatorsFactory.isInteger('Please enter an integer number')); +// +// this.validators.register( +// { +// source: this.repeatInterval, +// condition: isSimpleTrigger +// }, +// ValidatorsFactory.required('Please enter repeat interval'), +// ValidatorsFactory.isInteger('Please enter an integer number')); +// +// const newJobDataKeyValidationModel = this.validators.register( +// { +// source: this.newJobDataKey +// }, +// (value: string) => { +// if (value && __some(this.jobDataMap.getValue(), x => x.key === value)) { +// return ['Key ' + value + ' has already been added']; +// } +// +// return null; +// }); +// +// this.canAddJobDataKey = this.own(MAP(newJobDataKeyValidationModel.validated, x => x.data && x.data.length > 0 && x.errors.length === 0)); +// +// this.own(this.validators); +// } +// +// addJobDataMapItem() { +// const payload = (inputTypes: InputType[]) => { +// const +// jobDataMapItem = new JobDataMapItem(this.newJobDataKey.getValue(), inputTypes, this._inputTypesVariants, this.commandService), +// removeWire = jobDataMapItem.onRemoved.listen(() => { +// this.jobDataMap.remove(jobDataMapItem); +// this.newJobDataKey.setValue(this.newJobDataKey.getValue()); +// +// removeWire.dispose(); +// }); +// +// this.jobDataMap.add(jobDataMapItem); +// this.newJobDataKey.setValue(''); +// }; +// +// if (this._inputTypes) { +// payload(this._inputTypes); +// } else { +// this.commandService +// .executeCommand(new GetInputTypesCommand()) +// .then(inputTypes => { +// this._inputTypes = inputTypes; +// payload(this._inputTypes); +// }); +// } +// } +// +// composeTriggerStepData(): TriggerStepData { +// const result: TriggerStepData = { +// name: this.triggerName.getValue(), +// triggerType: this.triggerType.getValue(), +// jobDataMap: __map( +// this.jobDataMap.getValue(), +// x => ({ +// key: x.key, +// value: x.getActualValue(), +// inputTypeCode: x.inputTypeCode.getValue() +// })) +// }; +// +// if (this.triggerType.getValue() === 'Simple') { +// const repeatForever = this.repeatForever.getValue(); +// +// result.repeatForever = repeatForever; +// +// if (!repeatForever) { +// result.repeatCount = +this.repeatCount.getValue(); +// } +// +// const repeatInterval = +this.repeatInterval.getValue(); +// +// result.repeatInterval = repeatInterval * this.getIntervalMultiplier(); +// +// } else if (this.triggerType.getValue() === 'Cron') { +// result.cronExpression = this.cronExpression.getValue(); +// } +// +// return result; +// } +// +// displayValidationErrors(jobDataMapErrors: { [key: string]: string }) { +// __forOwn(jobDataMapErrors, (value, key) => { +// __each(this.jobDataMap.getValue(), (jobDataMapItem: JobDataMapItem) => { +// if (jobDataMapItem.key === key) { +// jobDataMapItem.error.setValue(value); +// } +// }); +// }); +// } +// +// releaseState() { +// this.dispose(); +// } +// +// private getIntervalMultiplier() { +// const intervalCode = this.repeatIntervalType.getValue(); +// +// if (intervalCode === 'Seconds') { +// return 1000; +// } +// +// if (intervalCode === 'Minutes') { +// return 1000 * 60; +// } +// +// if (intervalCode === 'Hours') { +// return 1000 * 60 * 60; +// } +// +// if (intervalCode === 'Days') { +// return 1000 * 60 * 60 * 24; +// } +// +// return 1; +// } +// } diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/view-commons.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/view-commons.ts new file mode 100644 index 0000000..beae33e --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/schedule-job/steps/view-commons.ts @@ -0,0 +1,7 @@ +// import __each from 'lodash/each'; +// +// export const CHANGE_DOM_DISPLAY = (blocks: { code: string, dom: js.IListenerDom }[], currentCode: string) => { +// __each(blocks, block => { +// block.dom.$.css('display', currentCode === block.code ? 'block' : 'none'); +// }); +// } diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/scheduler-details/scheduler-details-view-model.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/scheduler-details/scheduler-details-view-model.ts new file mode 100644 index 0000000..25b20f2 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/scheduler-details/scheduler-details-view-model.ts @@ -0,0 +1,50 @@ +import { DialogViewModel } from '../dialog-view-model'; +import { CommandService } from '../../services'; +import { SchedulerDetails } from '../../api'; +import { GetSchedulerDetailsCommand } from '../../commands/scheduler-commands'; + +import { Property, PropertyType } from '../common/property'; +import { ObservableList } from 'john-smith/reactive'; + +export default class SchedulerDetailsViewModel extends DialogViewModel { + summary = new ObservableList(); + status = new ObservableList(); + jobStore = new ObservableList(); + threadPool = new ObservableList(); + + constructor( + private commandService: CommandService) { + super(); + } + + loadDetails() { + this.commandService + .executeCommand(new GetSchedulerDetailsCommand()) + .then((response) => { + const data = response; + + this.summary.add( + new Property('Scheduler name', data.SchedulerName, PropertyType.String), + new Property('Scheduler instance id', data.SchedulerInstanceId, PropertyType.String), + new Property('Scheduler remote', data.SchedulerRemote, PropertyType.Boolean), + new Property('Scheduler type', data.SchedulerType, PropertyType.Type), + new Property('Version', data.Version, PropertyType.String)); + + this.status.add( + new Property('In standby mode', data.InStandbyMode, PropertyType.Boolean), + new Property('Shutdown', data.Shutdown, PropertyType.Boolean), + new Property('Started', data.Started, PropertyType.Boolean), + new Property('Jobs executed', data.NumberOfJobsExecuted, PropertyType.Numeric), + new Property('Running since', data.RunningSince, PropertyType.Date)); // todo + + this.jobStore.add( + new Property('Job store clustered', data.JobStoreClustered, PropertyType.Boolean), + new Property('Job store supports persistence', data.JobStoreSupportsPersistence, PropertyType.Boolean), + new Property('Job store type', data.JobStoreType, PropertyType.Type)); // todo + + this.threadPool.add( + new Property('Thread pool size', data.ThreadPoolSize, PropertyType.Numeric), + new Property('Thread pool type', data.ThreadPoolType, PropertyType.Type)); // todo + }); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/scheduler-details/scheduler-details-view.tsx b/src/CrystalQuartz.Application.Client2/src/dialogs/scheduler-details/scheduler-details-view.tsx new file mode 100644 index 0000000..9351bb4 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/scheduler-details/scheduler-details-view.tsx @@ -0,0 +1,53 @@ +import ViewModel from './scheduler-details-view-model'; + +import ViewBase from '../dialog-view-base'; +import { PropertyView } from '../common/property-view'; +import SchedulerDetailsViewModel from './scheduler-details-view-model'; +import { List } from 'john-smith/view/components'; +import { DomElement } from 'john-smith/view'; +import { DomEngine } from 'john-smith/view/dom-engine'; +import { OptionalDisposables } from 'john-smith/common'; + +export default class SchedulerDetailsView extends ViewBase { + constructor(viewModel: SchedulerDetailsViewModel) { + super(viewModel, 'Scheduler Details'); + } + + public onInit(root: DomElement | null, domEngine: DomEngine): OptionalDisposables { + this.viewModel.loadDetails(); + + return super.onInit(root, domEngine); + } + + protected getBodyContent(): JSX.IElement { + return
+
+
Summary
+ + +
+
+ +
+
Status
+ + +
+
+ +
+
Job Store
+ + +
+
+ +
+
Thread Pool
+ + +
+
+
; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/scheduler-details/scheduler-details.tmpl.html b/src/CrystalQuartz.Application.Client2/src/dialogs/scheduler-details/scheduler-details.tmpl.html new file mode 100644 index 0000000..c1294f5 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/scheduler-details/scheduler-details.tmpl.html @@ -0,0 +1,34 @@ +
+
+
+ × +

Scheduler details

+
+ +
+
+
Summary
+
+
+ +
+
Status
+
+
+ +
+
Job Store
+
+
+ +
+
Thread Pool
+
+
+
+ + +
+
\ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/show-schedule-job-dialog.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/show-schedule-job-dialog.ts new file mode 100644 index 0000000..03583f8 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/show-schedule-job-dialog.ts @@ -0,0 +1,23 @@ +import {ScheduleJobViewModel} from './schedule-job/schedule-job-view-model'; +import {ApplicationModel} from '../application-model'; +import {CommandService} from '../services'; +import { ScheduleJobOptions } from './schedule-job/schedule-job-view-model'; +import {IDialogManager} from './dialog-manager'; + +export const SHOW_SCHEDULE_JOB_DIALOG = ( + dialogManager: IDialogManager, + application: ApplicationModel, + commandService: CommandService, + options?: ScheduleJobOptions) => { + + dialogManager.showModal( + new ScheduleJobViewModel( + application, + commandService, + options), + result => { + if (result) { + application.invalidateData(); + } + }); +}; \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/trigger-details/trigger-details-view-model.ts b/src/CrystalQuartz.Application.Client2/src/dialogs/trigger-details/trigger-details-view-model.ts new file mode 100644 index 0000000..f13369c --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/trigger-details/trigger-details-view-model.ts @@ -0,0 +1,99 @@ +import { DialogViewModel } from '../dialog-view-model'; +import { CommandService, ErrorInfo } from '../../services'; +import {CronTriggerType, PropertyValue, SimpleTriggerType, Trigger, TriggerDetails} from '../../api'; +import {GetTriggerDetailsCommand} from '../../commands/trigger-commands'; +import {Property, PropertyType} from '../common/property'; +import { ObservableList, ObservableValue } from 'john-smith/reactive'; + +export class TriggerDetailsViewModel extends DialogViewModel { + summary = new ObservableList(); + identity = new ObservableList(); + schedule = new ObservableList(); + jobDataMap = new ObservableValue(null); + + constructor( + private trigger: Trigger, + private commandService: CommandService) { + + super(); + + this.state.setValue('unknown'); + } + + loadDetails() { + this.commandService + .executeCommand(new GetTriggerDetailsCommand(this.trigger.GroupName, this.trigger.Name), true) + .then(details => { + const trigger = details.trigger; + + if (!trigger) { + this.goToErrorState('No details found, the trigger no longer available.') + return; + } + + const identityProperties = [ + new Property('Name', trigger.Name, PropertyType.String), + new Property('Group', trigger.GroupName, PropertyType.String) + ]; + + if (details.secondaryData) { + identityProperties.push(new Property('Description', details.secondaryData.description, PropertyType.String)); + } + + this.identity.setValue(identityProperties); + let scheduleProperties = [ + new Property('Trigger Type', trigger.TriggerType.Code, PropertyType.String) + ]; + + switch (trigger.TriggerType.Code) { + case 'simple': + const simpleTrigger = trigger.TriggerType; + + scheduleProperties.push(new Property( + 'Repeat Count', + simpleTrigger.RepeatCount === -1 ? 'forever' : simpleTrigger.RepeatCount, + PropertyType.String)); + scheduleProperties.push(new Property('Repeat Interval', simpleTrigger.RepeatInterval, PropertyType.String)); + scheduleProperties.push(new Property('Times Triggered', simpleTrigger.TimesTriggered, PropertyType.Numeric)); + break; + case 'cron': + const cronTrigger = trigger.TriggerType; + + scheduleProperties.push(new Property('Cron Expression', cronTrigger.CronExpression, PropertyType.String)); + break; + } + + this.schedule.setValue(scheduleProperties); + + if (details.secondaryData) { + this.summary.setValue([ + new Property('Priority', details.secondaryData.priority, PropertyType.Numeric), + new Property( + 'Misfire Instruction', + this.getFriendlyMisfireInstruction(details.secondaryData.misfireInstruction, trigger), + PropertyType.String), + ]); + } + + this.jobDataMap.setValue(details.jobDataMap); + + this.state.setValue('ready'); + }) + .catch((error: ErrorInfo) => { + console.log('error', error); + this.goToErrorState(error.errorMessage); + }); + } + + private getFriendlyMisfireInstruction(misfireInstruction: number, trigger: Trigger){ + if (!trigger) { + return misfireInstruction.toString(); + } + + if (misfireInstruction === 0) { + return 'Not Set'; + } + + return (trigger.TriggerType.supportedMisfireInstructions[misfireInstruction] || 'Unknown') + ' (' + misfireInstruction + ')'; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/trigger-details/trigger-details-view.tsx b/src/CrystalQuartz.Application.Client2/src/dialogs/trigger-details/trigger-details-view.tsx new file mode 100644 index 0000000..d392e3e --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/trigger-details/trigger-details-view.tsx @@ -0,0 +1,88 @@ +import DialogViewBase from '../dialog-view-base'; + +import { TriggerDetailsViewModel } from './trigger-details-view-model'; +import { PropertyView } from '../common/property-view'; +import { Value } from 'john-smith/view/components'; +import { OnInit } from 'john-smith/view/hooks'; +import { DomElement } from 'john-smith/view'; +import { DomEngine } from 'john-smith/view/dom-engine'; +import { OptionalDisposables } from 'john-smith/common'; + +// import TEMPLATE from './trigger-details.tmpl.html'; + +// import { RENDER_PROPERTIES } from '../common/object-browser'; +// import { CHANGE_DOM_DISPLAY } from "../schedule-job/steps/view-commons"; + +export class TriggerDetailsView extends DialogViewBase implements OnInit{ + + constructor(viewModel: TriggerDetailsViewModel) { + super(viewModel, 'Trigger Details'); + } + + + onInit(root: DomElement | null, domEngine: DomEngine): OptionalDisposables { + this.viewModel.loadDetails(); + + return super.onInit(root, domEngine); + } + + protected getBodyContent(): JSX.IElement { + return
+ { + if (state === 'ready') { + return
+
+
Identity
+
+
+
+
Summary
+
+
+
+
Schedule
+
+
+
+
Job Data Map
+
+
+
; + } + + if (state === 'error') { + return
{this.viewModel.errorMessage}
; + } + + return
+ Loading trigger details... +
; + }} model={this.viewModel.state}>
+
; + } + +// template = TEMPLATE; + // + // init(dom: js.IDom, viewModel:TriggerDetailsViewModel) { + // super.init(dom, viewModel); + // + // const stateUi = [ + // { code: 'unknown', dom: dom('.js_stateUnknown') }, + // { code: 'error', dom: dom('.js_stateError') }, + // { code: 'ready', dom: dom('.js_stateReady') } + // ]; + // + // dom.manager.manage(viewModel.state.listen(state => { + // CHANGE_DOM_DISPLAY(stateUi, state.toString()); + // })); + // + // dom('.js_summary').observes(viewModel.summary, PropertyView); + // dom('.js_identity').observes(viewModel.identity, PropertyView); + // dom('.js_schedule').observes(viewModel.schedule, PropertyView); + // dom('.js_stateError').observes(viewModel.errorMessage); + // + // RENDER_PROPERTIES(dom('.js_jobDataMap'), viewModel.jobDataMap); + // + // viewModel.loadDetails(); + // } +} diff --git a/src/CrystalQuartz.Application.Client2/src/dialogs/trigger-details/trigger-details.tmpl.html b/src/CrystalQuartz.Application.Client2/src/dialogs/trigger-details/trigger-details.tmpl.html new file mode 100644 index 0000000..2032ead --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/dialogs/trigger-details/trigger-details.tmpl.html @@ -0,0 +1,41 @@ +
+
+
+ × + +

Trigger details

+
+ +
+
+ Loading trigger details... +
+ +
+
+ +
+
+
Identity
+
+
+
+
Summary
+
+
+
+
Schedule
+
+
+
+
Job Data Map
+
+
+
+
+ + +
+
\ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/global-activities-synchronizer.ts b/src/CrystalQuartz.Application.Client2/src/global-activities-synchronizer.ts new file mode 100644 index 0000000..7826789 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/global-activities-synchronizer.ts @@ -0,0 +1,104 @@ +import { SchedulerData, SchedulerEventScope, JobGroup, Job, Trigger } from './api'; + +import Timeline from './timeline/timeline'; +import { TimelineGlobalActivity } from './timeline/timeline-global-activity'; +import TimelineSlot from './timeline/timeline-slot'; + +export default class GlobalActivitiesSynchronizer { + private _currentData: SchedulerData | null = null; + private _currentFlatData: { scope: number, key: string, size: number }[] | null = null; + + constructor(private timeline: Timeline) { + } + + updateFrom(data: SchedulerData) { + this._currentData = data; + this._currentFlatData = null; + + const globalTimelineActivities = this.timeline.getGlobalActivities(); + + if (globalTimelineActivities.length > 0) { + this.ensureHaveFlattenData(); + + for (let i = 0; i < globalTimelineActivities.length; i++) { + this.internalUpdateActivity(globalTimelineActivities[i]); + } + } + } + + updateActivity(activity: TimelineGlobalActivity) { + if (!this._currentData) { + return; + } + + this.ensureHaveFlattenData(); + this.internalUpdateActivity(activity); + } + + getSlotIndex(slot: TimelineSlot, reverse?: boolean) { + this.ensureHaveFlattenData(); + + const totalItems = this._currentFlatData === null ? 0 : this._currentFlatData.length; + for (let j = 0; j < totalItems; j++) { + const item = this._currentFlatData![j]; + + if (slot.key === item.key) { + return reverse ? totalItems - j : j; + } + } + + return null; + } + + makeSlotKey(scope: SchedulerEventScope, key: string) { + return scope + ':' + key; + } + + private internalUpdateActivity(activity: TimelineGlobalActivity) { + if (activity.scope === SchedulerEventScope.Scheduler) { + /** + * Scheduler global activity fills the entire timeline area, + * so we just update edges. + */ + activity.updateVerticalPostion(0, this._currentFlatData!.length); + return; + } + + for (let j = 0; j < this._currentFlatData!.length; j++) { + const item = this._currentFlatData![j]; + + if (activity.scope === item.scope && activity.itemKey === item.key) { + activity.updateVerticalPostion(j, item.size); + } + } + } + + private ensureHaveFlattenData() { + if (this._currentFlatData) { + return; + } + + this._currentFlatData = + this._currentData!.JobGroups.flatMap( + (jobGroup: JobGroup) => { + const flattenJobs = + jobGroup.Jobs.flatMap( + (job: Job) => { + const flattenTriggers = job.Triggers.map( + (t:Trigger) => ({ scope: SchedulerEventScope.Trigger, key: this.makeSlotKey(SchedulerEventScope.Trigger, t.UniqueTriggerKey), size: 1 })); + + return [ + { scope: SchedulerEventScope.Job, key: this.makeSlotKey(SchedulerEventScope.Job, jobGroup.Name) + '.' + job.Name, size: flattenTriggers.length + 1 }, // todo: fix key concatenation logic + ...flattenTriggers + ]; + } + ); + + return [ + { scope: SchedulerEventScope.Group, key: this.makeSlotKey(SchedulerEventScope.Group, jobGroup.Name), size: flattenJobs.length + 1 }, + ...flattenJobs + ]; + }); + } + +} diff --git a/src/CrystalQuartz.Application.Client2/src/global/actions/action-view.tsx b/src/CrystalQuartz.Application.Client2/src/global/actions/action-view.tsx new file mode 100644 index 0000000..6b8bb25 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/global/actions/action-view.tsx @@ -0,0 +1,43 @@ +import Action from './action'; +import { HtmlDefinition, View } from 'john-smith/view'; +import Separator from './separator'; + +export default class ActionView implements View { + constructor(private readonly action: Action | Separator) { + } + + template(): HtmlDefinition { + const action = this.action; + if (action instanceof Separator) { + return ; + } else { + return
  • + { + if (!action.disabled.getValue()) { + action.execute(); + } + }}>{action.title} +
  • ; + } + } + + // init(dom: js.IDom, action: Action) { + // const container = dom('li'); + // const link = dom('a'); + // + // dom('li').className('disabled').observes(action.disabled); + // + // dom('span').observes(action.title); + // + // if (action.isDanger) { + // //link.$.prepend('!'); + // container.$.addClass('danger'); + // } + // + // link.on('click').react(() => { + // if (!container.$.is('.disabled')) { + // action.execute(); + // } + // }); + // } +} diff --git a/src/CrystalQuartz.Application.Client2/src/global/actions/action.ts b/src/CrystalQuartz.Application.Client2/src/global/actions/action.ts new file mode 100644 index 0000000..428af3f --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/global/actions/action.ts @@ -0,0 +1,24 @@ +import { ObservableValue } from 'john-smith/reactive'; + +export default class Action { + disabled = new ObservableValue(false); + + constructor( + public title: string, + private callback: () => void, + private confirmMessage?: string) { } + + set enabled(value: boolean) { + this.disabled.setValue(!value); + } + + get isDanger() { + return !!this.confirmMessage; + } + + execute() { + if (!this.confirmMessage || confirm(this.confirmMessage)) { + this.callback(); + } + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/global/actions/actions-utils.ts b/src/CrystalQuartz.Application.Client2/src/global/actions/actions-utils.ts new file mode 100644 index 0000000..6e51875 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/global/actions/actions-utils.ts @@ -0,0 +1,13 @@ +import Separator from './separator'; +import Action from './action'; + +import SeparatorView from './separator-view'; +import ActionView from './action-view'; + +export default class ActionsUtils { + // static render(dom: js.IListenerDom, actions: (Separator|Action)[]) { + // dom.observes( + // actions, + // item => item instanceof Separator ? SeparatorView : ActionView); + // } +} diff --git a/src/CrystalQuartz.Application.Client2/src/global/actions/separator-view.tsx b/src/CrystalQuartz.Application.Client2/src/global/actions/separator-view.tsx new file mode 100644 index 0000000..fbdeefb --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/global/actions/separator-view.tsx @@ -0,0 +1,5 @@ +import { View } from 'john-smith/view'; + +export default class SeparatorView implements View { + template = () => ; +} diff --git a/src/CrystalQuartz.Application.Client2/src/global/actions/separator.ts b/src/CrystalQuartz.Application.Client2/src/global/actions/separator.ts new file mode 100644 index 0000000..879e36c --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/global/actions/separator.ts @@ -0,0 +1 @@ +export default class Separator {} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/global/activities/activity-state-view.tsx b/src/CrystalQuartz.Application.Client2/src/global/activities/activity-state-view.tsx new file mode 100644 index 0000000..f064f9a --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/global/activities/activity-state-view.tsx @@ -0,0 +1,25 @@ +import { ActivityState } from './activity-state'; +import { HtmlDefinition, View } from 'john-smith/view'; + +const statusData: Record = { + [ActivityState.InProgress]: { title: 'In progress', className: 'in-progress' }, + [ActivityState.Failure]: { title: 'Failed', className: 'failed' }, + [ActivityState.Success]: { title: 'Success', className: 'success' } +}; + +export class ActivityStateView implements View { + + constructor( + private readonly viewModel: ActivityState + ) { + } + + template(): HtmlDefinition { + const statusDataValue = statusData[this.viewModel]; + + return + + {statusDataValue.title} + ; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/global/activities/activity-state.ts b/src/CrystalQuartz.Application.Client2/src/global/activities/activity-state.ts new file mode 100644 index 0000000..114a71d --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/global/activities/activity-state.ts @@ -0,0 +1,5 @@ +export enum ActivityState { + InProgress, + Success, + Failure +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/global/activities/index.scss b/src/CrystalQuartz.Application.Client2/src/global/activities/index.scss new file mode 100644 index 0000000..d2da7a4 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/global/activities/index.scss @@ -0,0 +1,73 @@ +.runnable-state { + .title { + font-weight: bold; + } + + $icon-size: 16px; + $icon-error-cross-size: 10px; + $icon-inner-weight: 2px; + + &.in-progress { + .icon { + border: 2px solid rgba(102, 102, 102, 0.3); + border-left: 2px solid rgba(102, 102, 102, 1); + -webkit-animation: load8 1.1s infinite linear; + animation: load8 1.1s infinite linear; + border-radius: 50%; + } + } + + &.success { + color: $green; + + .icon::after { + border-bottom: 2px solid #38C049; + border-left: 2px solid #38C049; + position: absolute; + left: 5px; + top: 4px; + transform: rotate(-45deg); + content: ''; + width: 10px; + height: 5px; + } + } + + &.failed { + color: $error; + + .icon { + background: $error; + border-radius: 50%; + } + + .icon::after, + .icon::before { + background: #FFFFFF; + position: absolute; + left: ($icon-size - $icon-error-cross-size)/2; + top: ($icon-size - $icon-inner-weight)/2; + transform: rotate(-45deg); + transform-origin: 50% 50%; + content: ''; + width: $icon-error-cross-size; + height: $icon-inner-weight; + } + + .icon::after { + transform: rotate(45deg); + } + } + + .icon { + width: $icon-size; + height: $icon-size; + float: left; + margin-right: 5px; + position: relative; + } +} + +.icon-only .runnable-state .title { + display: none; +} diff --git a/src/CrystalQuartz.Application.Client2/src/global/constants.scss b/src/CrystalQuartz.Application.Client2/src/global/constants.scss new file mode 100644 index 0000000..8ac5c6e --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/global/constants.scss @@ -0,0 +1,24 @@ +/* Constants -> size */ + +$data-row-height: 20px; +$aside-width: 70px; +$main-header-primary-height: 30px; +$main-header-secondary-height: $data-row-height; +$main-footer-height: 20px; +$loading-image-width: 24px; +$timeline-row-height-sm: 10px; + +/* Constants -> colors */ + +$green: #38C049; +$green-dark: #5CB85C; + +$active-light: #E0EFF6; +$active: #058dc7; + +$error: #cb4437; + +$bg-activity-normal: $green-dark; +$bg-activity-normal-hover: darken($bg-activity-normal, 10%); +$bg-activity-error: lighten($error, 10%); +$bg-activity-error-hover: darken($bg-activity-error, 10%); diff --git a/src/CrystalQuartz.Application.Client2/src/global/data-state.ts b/src/CrystalQuartz.Application.Client2/src/global/data-state.ts new file mode 100644 index 0000000..80350fe --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/global/data-state.ts @@ -0,0 +1 @@ +export type DataState = 'unknown'|'error'|'ready'; \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/global/duration.ts b/src/CrystalQuartz.Application.Client2/src/global/duration.ts new file mode 100644 index 0000000..3d58568 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/global/duration.ts @@ -0,0 +1,100 @@ +import { Timer } from "./timers/timer"; +import { Disposable } from 'john-smith/common'; +import { ObservableValue } from 'john-smith/reactive'; + +export class DurationFormatter { + private static _ranges = [ + { title: 'sec', edge: 1000 }, + { title: 'min', edge: 60 }, + { title: 'hours', edge: 60 }, + { title: 'days', edge: 24 } + ]; + + static format(durationMilliseconds: number) { + let ratio = 1; + + for (var i = 0; i < this._ranges.length; i++) { + const rangeItem = this._ranges[i]; + ratio *= rangeItem.edge; + + const ratioUnits = durationMilliseconds / ratio, + isLastItem = i === this._ranges.length - 1; + + if (isLastItem || this.isCurrentRange(durationMilliseconds, i, ratio)) { + return { + value: Math.floor(ratioUnits).toString(), + unit: rangeItem.title, + ratio: ratio + }; + } + } + + throw new Error('could not format provided duration'); // should not ever get here + } + + private static isCurrentRange(uptimeMilliseconds: number, index: number, ratioMultiplier: number) { + return (uptimeMilliseconds / (this._ranges[index + 1].edge * ratioMultiplier)) < 1; + } +} + +export class Duration implements Disposable { + value = new ObservableValue(null); + measurementUnit = new ObservableValue(''); + + private _timer = new Timer(); + + constructor( + private startDate?: number, + private endDate?: number) { + + const waitingText = '...'; + + this.value.setValue(waitingText); + } + + init() { + this.calculate(); + } + + setStartDate(date: number | undefined) { + this.startDate = date; + this.calculate(); + } + + setEndDate(date: number) { + this.endDate = date; + this.calculate(); + } + + dispose() { + this.releaseTimer(); + } + + private releaseTimer() { + this._timer.reset(); + } + + private calculate() { + this.releaseTimer(); + + if (!this.startDate) { + this.value.setValue(null); + this.measurementUnit.setValue(''); + + return; + } + + const + durationMilliseconds = (this.endDate || new Date().getTime()) - this.startDate, + formattedDuration = DurationFormatter.format(durationMilliseconds); + + if (formattedDuration) { + this.value.setValue(formattedDuration.value); + this.measurementUnit.setValue(' ' + formattedDuration.unit); + + if (!this.endDate) { + this._timer.schedule(() => this.calculate(), formattedDuration.ratio / 2); + } + } + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/global/layers.scss b/src/CrystalQuartz.Application.Client2/src/global/layers.scss new file mode 100644 index 0000000..baee6fe --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/global/layers.scss @@ -0,0 +1,32 @@ +/* + * Layers (z-indexes) + */ + +.main-aside { + z-index: 1000; +} + +.main-footer, +.main-header { + z-index: 2000; +} + +.dialogs-overlay { + z-index: 9000; +} + +.dialog-container { + z-index: 9500; +} + +.main-container .dropdown-menu { + z-index: 9800; +} + +.offline-mode { + z-index: 9990; +} + +.notifications { + z-index: 10000; +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/global/timers/countdown-timer.ts b/src/CrystalQuartz.Application.Client2/src/global/timers/countdown-timer.ts new file mode 100644 index 0000000..91ed47c --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/global/timers/countdown-timer.ts @@ -0,0 +1,40 @@ +import { Timer } from './timer'; +import { Disposable } from 'john-smith/common' +import { ObservableValue } from 'john-smith/reactive'; + +export class CountdownTimer implements Disposable { + private _timer = new Timer(); + + public countdownValue = new ObservableValue(null); + + constructor(private action: () => void) { + } + + schedule(delaySeconds: number) { + if (delaySeconds <= 0) { + this.performAction(); + } else { + this.countdownValue.setValue(delaySeconds); + this._timer.schedule(() => this.schedule(delaySeconds - 1), 1000); + } + } + + reset() { + this._timer.reset(); + } + + force() { + this.reset(); + this.performAction(); + } + + dispose() { + this._timer.dispose(); + } + + private performAction() { + if (this.action) { + this.action(); + } + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/global/timers/retry-timer.ts b/src/CrystalQuartz.Application.Client2/src/global/timers/retry-timer.ts new file mode 100644 index 0000000..b972980 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/global/timers/retry-timer.ts @@ -0,0 +1,95 @@ +import { CountdownTimer } from './countdown-timer'; +import { DurationFormatter } from "../duration"; +import { Disposable } from 'john-smith/common' +import { ObservableValue } from 'john-smith/reactive'; + +export class RetryTimer implements Disposable { + timer: CountdownTimer; + message = new ObservableValue(''); + isInProgress = new ObservableValue(false); + + private _currentRetryInterval: number = 0; + private _messageWire: Disposable; + private _isRetry = false; + private _currentResult: { + resolve: (value: (PromiseLike | TResult)) => void; + reject: (reason?: any) => void + } | null = null; + + constructor( + private payload: (isRetry: boolean) => Promise, + private minInterval: number = 5, + private maxInterval: number = 60, + private onFailed?: (error: any) => void) { + + this.timer = new CountdownTimer(() => this.performRetry()); + this._messageWire = this.timer.countdownValue.listen((countdownValue: number | null) => { + if (countdownValue) { + let formattedDuration = DurationFormatter.format(countdownValue * 1000); + this.message.setValue(`in ${formattedDuration.value} ${formattedDuration.unit}`); + } + }); + } + + start(sleepBeforeFirstCall: boolean): Promise { + this.timer.reset(); + this._currentRetryInterval = this.minInterval; + + const result = new Promise((resolve, reject) => { + this._currentResult = { resolve: resolve, reject: reject } + }); + + if (sleepBeforeFirstCall) { + this.scheduleRetry(); + } else { + this.performRetry(); + } + + return result; + } + + force() { + this.timer.reset(); + this.performRetry(); + } + + reset() { + this.timer.reset(); + } + + dispose() { + this.timer.dispose(); + this._messageWire.dispose(); + } + + private performRetry() { + const payloadPromise = this.payload(this._isRetry); + + this.isInProgress.setValue(true); + this.message.setValue('in progress...'); + + // this timeout is for UI only + setTimeout(() => { + payloadPromise + .then(response => { + this._currentResult?.resolve(response); + }) + .catch(error => { + this._isRetry = true; + if (this.onFailed) { + this.onFailed(error); + } + + this.scheduleRetry(); + }) + .finally(() => { + this.isInProgress.setValue(false); + }); + }, 10); + } + + private scheduleRetry() { + this.timer.schedule(this._currentRetryInterval); + this._currentRetryInterval = Math.min(this._currentRetryInterval * 2, this.maxInterval); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/global/timers/timer.ts b/src/CrystalQuartz.Application.Client2/src/global/timers/timer.ts new file mode 100644 index 0000000..b143b77 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/global/timers/timer.ts @@ -0,0 +1,21 @@ +import { Disposable } from 'john-smith/common' + +export class Timer implements Disposable { + private _ref: number | null = null; + + schedule(action: () => void, delay: number) { + this.reset(); + this._ref = setTimeout(action, delay) as unknown as number; + } + + reset() { + if (this._ref) { + clearTimeout(this._ref); + this._ref = null; + } + } + + dispose() { + this.reset(); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/index.scss b/src/CrystalQuartz.Application.Client2/src/index.scss new file mode 100644 index 0000000..479f2b4 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/index.scss @@ -0,0 +1,7 @@ +@import "./startup/startup.view"; +@import "./main/index"; +@import "./global/layers"; +@import "./global/activities"; +@import "./dialogs/index"; +@import "./timeline/index"; +@import "./common"; diff --git a/src/CrystalQuartz.Application.Client2/src/index.ts b/src/CrystalQuartz.Application.Client2/src/index.ts new file mode 100644 index 0000000..ae746cf --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/index.ts @@ -0,0 +1,49 @@ +import { Application } from 'john-smith'; +import { ApplicationView } from './application.view'; +import { ApplicationViewModel } from './application.view-model'; +import { StartupView } from './startup/startup.view'; +import { FaviconStatus, StartupViewModel } from './startup/startup.view-model'; +import { ANALYZE_LOCATION } from './startup/headers-extractor'; + +import { ApplicationModel } from './application-model'; +import { CommandService } from './services'; +import { DefaultNotificationService } from './notification/notification-service'; +import { FaviconRenderer } from './startup/favicon-renderer'; +import { MainView } from './main/main.view'; +import { MainViewModel } from './main/main.view-model'; + +const application = new Application(); + +const body = document.body; + +const { headers, url } = ANALYZE_LOCATION(); +const commandService = new CommandService(url, headers); +const applicationModel = new ApplicationModel(); +const notificationService = new DefaultNotificationService() + +const startupViewModel = new StartupViewModel(commandService, applicationModel, notificationService); + +const faviconRenderer = new FaviconRenderer(); +startupViewModel.favicon.listen((faviconStatus:FaviconStatus | null/*, oldFaviconStatus: FaviconStatus */) => { + if (faviconStatus !== null /* && faviconStatus !== undefined && faviconStatus !== oldFaviconStatus */) { + faviconRenderer.render(faviconStatus); + } +}); + +startupViewModel.title.listen(title => { + if (title) { + document.title = title; + } +}); + +startupViewModel.complete.listen(data => { + if (data) { + application.render(body, MainView, new MainViewModel(applicationModel, commandService, data.environmentData, notificationService, data.timelineInitializer)); + startupView.dispose(); + } +}) + +const startupView = application.render(body, StartupView, startupViewModel); + + +startupViewModel.start(); diff --git a/src/CrystalQuartz.Application.Client2/src/main/index.scss b/src/CrystalQuartz.Application.Client2/src/main/index.scss new file mode 100644 index 0000000..3aa0aa4 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/index.scss @@ -0,0 +1,3 @@ +@import "./main-aside/aside"; +@import "./main-header"; +@import "./main-content"; diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-aside/aside.less b/src/CrystalQuartz.Application.Client2/src/main/main-aside/aside.less new file mode 100644 index 0000000..f800b56 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-aside/aside.less @@ -0,0 +1,122 @@ +@aside-value-height-ratio: 0.4; + +@aside-dashbord-height: 100px; +@aside-value-height: @aside-dashbord-height * @aside-value-height-ratio; +@aside-vertical-space: (@aside-dashbord-height - @aside-value-height) / 2; + +@gauge-size: 50px; + +.main-aside { + position: fixed; + left: 0; + top: 0; + bottom: 0; + width: @aside-width; + background: #333333; + + ul, ul li { + margin: 0; + padding: 0; + list-style-type: none; + list-style-image: none; + } + + ul { + margin-top: @main-header-primary-height + @main-header-secondary-height; + border-top: 1px solid #444444; + } + + ul li { + color: #FFFFFF; + height: @aside-dashbord-height; + border-bottom: 1px solid #444444; + position: relative; + overflow: hidden; + + .aside-value-title { + position: absolute; + left: 0; + right: 0; + bottom: @aside-dashbord-height - @aside-vertical-space; + font-size: 11px; + color: #AAAAAA; + display: block; + text-align: center; + line-height: 1; + } + + .aside-value { + display: block; + text-align: center; + font-size: 20px; + margin-top: @aside-vertical-space; + line-height: @aside-value-height; + height: @aside-value-height; + overflow: hidden; + } + + .aside-value .empty { + font-size: 14px; + color: #666666; + } + + .value-measurement-unit { + font-size: 12px; + display: block; + text-align: center; + } + + .highlight { + animation: valueHighlight 1s ease; + } + } +} + +.main-aside { + .gauge { + height: @gauge-size / 2; + overflow: hidden; + margin-left: (@aside-width - @gauge-size)/2; + margin-top: 5px; + } + + .gauge-body { + width: @gauge-size; + height: @gauge-size; + position: relative; + transform: rotate(0deg); + transition: transform 0.5s; + } + + .gauge-scale { + background: #444444; + height: @gauge-size / 2; + width: @gauge-size; + border-radius: 25px 25px 0 0; + } + + .gauge-value { + background: #AAAAAA; + height: @gauge-size / 2; + width: @gauge-size; + border-radius: 0 0 25px 25px; + } + + .gauge-legend { + line-height: 1; + position: relative; + z-index: 100; + margin-top: -10px; + } + + .gauge-center { + position: absolute; + top: 4px; + left: 4px; + height: 42px; + width: 42px; + background: #333333; + border-radius: 21px; + z-index: 50; + } +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-aside/aside.scss b/src/CrystalQuartz.Application.Client2/src/main/main-aside/aside.scss new file mode 100644 index 0000000..4be94f4 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-aside/aside.scss @@ -0,0 +1,124 @@ +@import "../../global/constants"; + +$aside-value-height-ratio: 0.4; + +$aside-dashbord-height: 100px; +$aside-value-height: $aside-dashbord-height * $aside-value-height-ratio; +$aside-vertical-space: ($aside-dashbord-height - $aside-value-height) / 2; + +$gauge-size: 50px; + +.main-aside { + position: fixed; + left: 0; + top: 0; + bottom: 0; + width: $aside-width; + background: #333333; + + ul, ul li { + margin: 0; + padding: 0; + list-style-type: none; + list-style-image: none; + } + + ul { + margin-top: $main-header-primary-height + $main-header-secondary-height; + border-top: 1px solid #444444; + } + + ul li { + color: #FFFFFF; + height: $aside-dashbord-height; + border-bottom: 1px solid #444444; + position: relative; + overflow: hidden; + + .aside-value-title { + position: absolute; + left: 0; + right: 0; + bottom: $aside-dashbord-height - $aside-vertical-space; + font-size: 11px; + color: #AAAAAA; + display: block; + text-align: center; + line-height: 1; + } + + .aside-value { + display: block; + text-align: center; + font-size: 20px; + margin-top: $aside-vertical-space; + line-height: $aside-value-height; + height: $aside-value-height; + overflow: hidden; + } + + .aside-value .empty { + font-size: 14px; + color: #666666; + } + + .value-measurement-unit { + font-size: 12px; + display: block; + text-align: center; + } + + .highlight { + animation: valueHighlight 1s ease; + } + } +} + +.main-aside { + .gauge { + height: $gauge-size / 2; + overflow: hidden; + margin-left: ($aside-width - $gauge-size) / 2; + margin-top: 5px; + } + + .gauge-body { + width: $gauge-size; + height: $gauge-size; + position: relative; + transform: rotate(0deg); + transition: transform 0.5s; + } + + .gauge-scale { + background: #444444; + height: $gauge-size / 2; + width: $gauge-size; + border-radius: 25px 25px 0 0; + } + + .gauge-value { + background: #AAAAAA; + height: $gauge-size / 2; + width: $gauge-size; + border-radius: 0 0 25px 25px; + } + + .gauge-legend { + line-height: 1; + position: relative; + z-index: 100; + margin-top: -10px; + } + + .gauge-center { + position: absolute; + top: 4px; + left: 4px; + height: 42px; + width: 42px; + background: #333333; + border-radius: 21px; + z-index: 50; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-aside/aside.tmpl.html b/src/CrystalQuartz.Application.Client2/src/main/main-aside/aside.tmpl.html new file mode 100644 index 0000000..4f7d0c5 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-aside/aside.tmpl.html @@ -0,0 +1,37 @@ + \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-aside/aside.view-model.ts b/src/CrystalQuartz.Application.Client2/src/main/main-aside/aside.view-model.ts new file mode 100644 index 0000000..cdbe105 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-aside/aside.view-model.ts @@ -0,0 +1,34 @@ +import { ApplicationModel } from '../../application-model'; +import { SchedulerData } from '../../api'; + +import NumberUtils from '../../utils/number'; +import {Duration} from "../../global/duration"; +import { ObservableValue } from 'john-smith/reactive'; + +export class MainAsideViewModel { + uptime: Duration = new Duration() + jobsTotal = new ObservableValue(null); + jobsExecuted = new ObservableValue(null); + + inProgressCount: ObservableValue; + + constructor( + private application: ApplicationModel) { + + const waitingText = '...'; + + this.inProgressCount = this.application.inProgressCount; + + this.jobsTotal.setValue(waitingText); + this.jobsExecuted.setValue(waitingText); + + application.onDataChanged.listen(data => this.updateAsideData(data)); + } + + private updateAsideData(data: SchedulerData) { + this.uptime.setStartDate(data.RunningSince ?? undefined); + + this.jobsTotal.setValue(NumberUtils.formatLargeNumber(data.JobsTotal)); + this.jobsExecuted.setValue(NumberUtils.formatLargeNumber(data.JobsExecuted)); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-aside/aside.view.tsx b/src/CrystalQuartz.Application.Client2/src/main/main-aside/aside.view.tsx new file mode 100644 index 0000000..4f94775 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-aside/aside.view.tsx @@ -0,0 +1,82 @@ +import { MainAsideViewModel } from './aside.view-model'; +import { DomElement, HtmlDefinition, View } from 'john-smith/view'; +import { map } from 'john-smith/reactive/transformers/map'; + +export class MainAsideView implements View { + + constructor(private readonly viewModel: MainAsideViewModel) { + } + + template(): HtmlDefinition { + const gaugeStyle = map(this.viewModel.inProgressCount, value => { + const angle = 180 * value / parseInt(this.viewModel.jobsTotal.getValue() ?? '0', 10); + + return 'transform: rotate(' + Math.min(angle, 180) + 'deg)'; + }); + + return + } + + // init(dom: js.IDom, viewModel: MainAsideViewModel) { + // //dom('.js_uptimeValue').observes(viewModel.uptimeValue, { encode: false }); + // dom('.js_uptimeMeasurementUnit').observes(viewModel.uptime.measurementUnit); + // + // dom('.js_totalJobs').observes(viewModel.jobsTotal); + // dom('.js_executedJobs').observes(viewModel.jobsExecuted); + // dom('.js_inProgressCount').observes(viewModel.inProgressCount); + // + // const $gaugeBody = dom('.gauge-body').$; + // dom.manager.manage(viewModel.inProgressCount.listen(value => { + // const angle = 180 * value / parseInt(viewModel.jobsTotal.getValue(), 10); + // + // $gaugeBody.css('transform', 'rotate(' + Math.min(angle, 180) + 'deg)'); + // })); + // + // const $uptimeValue = dom('.js_uptimeValue').$; + // dom.manager.manage(viewModel.uptime.value.listen(value => { + // if (value === null) { + // $uptimeValue.addClass('empty'); + // $uptimeValue.text('none'); + // } else { + // $uptimeValue.removeClass('empty'); + // $uptimeValue.text(value); + // } + // })); + // } +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/activities-synschronizer.ts b/src/CrystalQuartz.Application.Client2/src/main/main-content/activities-synschronizer.ts new file mode 100644 index 0000000..8dd98be --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/activities-synschronizer.ts @@ -0,0 +1,47 @@ +import { ManagableActivity } from '../../api'; +import { ManagableActivityViewModel } from './activity-view-model'; +import { ObservableList } from 'john-smith/reactive'; + +export default class ActivitiesSynschronizer> { + constructor( + private identityChecker: (activity: TActivity, activityViewModel: TActivityViewModel) => boolean, + private mapper: (activity: TActivity) => TActivityViewModel, + private list: ObservableList) { + } + + sync(activities: TActivity[]) { + const + existingActivities: TActivityViewModel[] = this.list.getValue(), + + deletedActivities = + existingActivities.filter( + viewModel => activities.every(activity => this.areNotEqual(activity, viewModel))), + + addedActivities = + activities.filter( + activity => existingActivities.every(viewModel => this.areNotEqual(activity, viewModel))), + + updatedActivities = + existingActivities.filter( + viewModel => activities.some(activity => this.areEqual(activity, viewModel))), + + addedViewModels = addedActivities.map(this.mapper), + + finder = (viewModel: TActivityViewModel) => activities.find(activity => this.areEqual(activity, viewModel)); + + deletedActivities.forEach(viewModel => this.list.remove(viewModel)); + addedViewModels.forEach(viewModel => { + viewModel.updateFrom(finder(viewModel)); + this.list.add(viewModel); + }); + updatedActivities.forEach(viewModel => viewModel.updateFrom(finder(viewModel))); + } + + private areEqual(activity: TActivity, activityViewModel: TActivityViewModel) { + return this.identityChecker(activity, activityViewModel); + } + + private areNotEqual(activity: TActivity, activityViewModel: TActivityViewModel) { + return !this.identityChecker(activity, activityViewModel); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/activity-status-view.tsx b/src/CrystalQuartz.Application.Client2/src/main/main-content/activity-status-view.tsx new file mode 100644 index 0000000..bdeb5be --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/activity-status-view.tsx @@ -0,0 +1,22 @@ +import { ActivityStatus } from '../../api'; +import { View } from 'john-smith/view'; +import { ObservableValue } from 'john-smith/reactive'; +import { map } from 'john-smith/reactive/transformers/map'; + +interface IStatusAware { + status: ObservableValue; +} + +export class ActivityStatusView implements View { + + constructor( + private readonly statusAware: IStatusAware + ) { + } + + template = () => + 'Status: ' + status)} $className={map(this.statusAware.status, status => status.code)}> + + +; +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/activity-status.scss b/src/CrystalQuartz.Application.Client2/src/main/main-content/activity-status.scss new file mode 100644 index 0000000..2cf0a54 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/activity-status.scss @@ -0,0 +1,134 @@ +/* Content -> Activity Status */ + +.status { + position: relative; +} + +.cq-activity-status { + float: left; + margin: 5px 0 0 10px; +} + +.cq-activity-status::after { + content: ''; + opacity: 0; + transition: opacity 1s; + border-radius: 50%; + width: 14px; + height: 14px; + position: absolute; + top: 3px; + right: -2px; + border: 2px solid rgba(56, 192, 73, 1); +} + +.executing { + .cq-activity-status::after { + opacity: 1; + + border-top: 2px solid rgba(56, 192, 73, 0.3); + border-right: 2px solid rgba(56, 192, 73, 0.3); + border-bottom: 2px solid rgba(56, 192, 73, 0.3); + border-left: 2px solid rgba(56, 192, 73, 1); + + -webkit-transform: translateZ(0); + -ms-transform: translateZ(0); + transform: translateZ(0); + -webkit-animation: load8 1.1s infinite linear; + animation: load8 1.1s infinite linear; + + cursor: pointer; + } + + .cq-activity-status { + margin-top: 6px; + margin-left: 11px; + } + + .cq-activity-status .cq-activity-status-primary, + .cq-activity-status .cq-activity-status-secondary { + height: 8px; + width: 8px; + border-radius: 4px; + -moz-border-radius: 4px; + -ms-border-radius: 4px; + -webkit-border-radius: 4px; + } +} + +@-webkit-keyframes load8 { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes load8 { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +.cq-activity-status { + transition: margin-top 1s, margin-left 1s; +} + +.cq-activity-status .cq-activity-status-primary, +.cq-activity-status .cq-activity-status-secondary { + float: left; + height: 10px; + width: 10px; + border-radius: 5px; + -moz-border-radius: 5px; + -ms-border-radius: 5px; + -webkit-border-radius: 5px; + transition: background 1s, width 1s, height 1s, background 1s, border-radius 1s, border-width 1s; +} + +.cq-activity-status .cq-activity-status-secondary { + width: 0; +} + +.cq-activity-status.active .cq-activity-status-primary { + background: #38C049; +} + +.cq-activity-status.paused .cq-activity-status-primary { + background: #E5D45B; +} + +.cq-activity-status.complete .cq-activity-status-primary { + background: #CCCCCC; +} + +.cq-activity-status.mixed { + .cq-activity-status-secondary { + display: block; + width: 5px; + background: #E5D45B; + border-radius: 0 5px 5px 0; + -moz-border-radius: 0 5px 5px 0; + -ms-border-radius: 0 5px 5px 0; + -webkit-border-radius: 0 5px 5px 0; + } + + .cq-activity-status-primary { + width: 5px; + background: #38C049; + border-radius: 5px 0 0 5px; + -moz-border-radius: 5px 0 0 5px; + -ms-border-radius: 5px 0 0 5px; + -webkit-border-radius: 5px 0 0 5px; + } +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/activity-view-model.ts b/src/CrystalQuartz.Application.Client2/src/main/main-content/activity-view-model.ts new file mode 100644 index 0000000..449bc24 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/activity-view-model.ts @@ -0,0 +1,52 @@ +import { ManagableActivity, ActivityStatus, SchedulerData } from '../../api'; +import { CommandService } from '../../services'; +import { ApplicationModel } from '../../application-model'; +import { ICommand } from '../../commands/contracts'; + +import Action from '../../global/actions/action'; +import CommandAction from '../../command-action'; +import { ObservableValue } from 'john-smith/reactive'; + +export interface IActionInfo { + title: string; + command: () => ICommand; +} + +export abstract class ManagableActivityViewModel { + name: string; + status = new ObservableValue(ActivityStatus.Active /* todo */); + + resumeAction: Action; + pauseAction: Action; + deleteAction: Action; + + constructor( + activity: ManagableActivity, + public commandService: CommandService, + public applicationModel: ApplicationModel) { + + this.name = activity.Name; + + const + resumeActionInfo = this.getResumeAction(), + pauseActionInfo = this.getPauseAction(), + deleteActionInfo = this.getDeleteAction(); + + this.resumeAction = new CommandAction(this.applicationModel, this.commandService, resumeActionInfo.title, resumeActionInfo.command); + this.pauseAction = new CommandAction(this.applicationModel, this.commandService, pauseActionInfo.title, pauseActionInfo.command); + this.deleteAction = new CommandAction(this.applicationModel, this.commandService, deleteActionInfo.title, deleteActionInfo.command, this.getDeleteConfirmationsText()); + } + + updateFrom(activity: TActivity) { + this.status.setValue(activity.Status); + + this.resumeAction.enabled = activity.Status === ActivityStatus.Paused || activity.Status === ActivityStatus.Mixed; + this.pauseAction.enabled = activity.Status === ActivityStatus.Active || activity.Status === ActivityStatus.Mixed; + this.deleteAction.enabled = true; + } + + abstract getDeleteConfirmationsText(): string; + abstract getResumeAction(): IActionInfo; + abstract getPauseAction(): IActionInfo; + abstract getDeleteAction(): IActionInfo; +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/dates.scss b/src/CrystalQuartz.Application.Client2/src/main/main-content/dates.scss new file mode 100644 index 0000000..4468934 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/dates.scss @@ -0,0 +1,3 @@ +.cq-date .cq-none { + color: #CCCCCC; +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/index.scss b/src/CrystalQuartz.Application.Client2/src/main/main-content/index.scss new file mode 100644 index 0000000..9d09c50 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/index.scss @@ -0,0 +1,113 @@ +@import "../../global/constants"; +@import 'activity-status.scss'; +@import 'dates.scss'; +@import 'job-group/index'; +@import 'job/index'; +@import 'trigger/index'; + +/* Content */ + +.main-container { + min-height: 100%; + overflow: hidden; + position: relative; +} + +.scrollable-area { + background: #FFFFFF; + position: relative; + margin-left: $aside-width; + margin-top: $main-header-primary-height + $main-header-secondary-height; + margin-bottom: $main-footer-height - 1; + border-bottom: 1px solid #DDDDDD; +} + +.data-row { + width: 100%; + height: $data-row-height; +} + +.data-row .actions-toggle { + display: block; + height: 100%; + text-align: center; +} + +.primary-data { + width: 50%; + height: $data-row-height; + border-right: 1px solid #DDDDDD; + float: left; +} + +.timeline-data { + width: 50%; + height: 20px; + float: left; + position: relative; + border-bottom: 1px dashed #DDDDDD; +} + +.data-container { + margin-left: 20px; + margin-right: 20px; + height: 100%; + overflow: hidden; + border-right: 1px solid #DDDDDD; +} + +.status { + width: 20px; + height: 20px; + float: left; +} + +.actions { + width: 20px; + height: 100%; + float: right; + + .danger a { + color: #993333; + } +} + +.ellipsis-link { + color: #333333; + cursor: pointer; + + &:hover { + text-decoration: none; + color: #111111; + position: relative; + + &::after { + content: '...'; + position: absolute; + right: 4px; + top: 4px; + bottom: 0; + + height: 12px; + padding: 0 5px 5px; + font-size: 12px; + font-weight: bold; + line-height: 6px; + color: #555555; + text-decoration: none; + vertical-align: middle; + background: #DDDDDD; + border: 0; + border-radius: 1px; + } + } + + &:focus { + text-decoration: none; + color: #333333; + } + + &:active { + color: #666666; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/index_sm.scss b/src/CrystalQuartz.Application.Client2/src/main/main-content/index_sm.scss new file mode 100644 index 0000000..4cf5d19 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/index_sm.scss @@ -0,0 +1,7 @@ +.triggers:last-child .data-row-trigger:last-child .primary-data { + border-bottom: 1px solid #DDDDDD; +} + +.jobs:last-child .triggers:last-child .data-row-trigger:last-child .timeline-data { + border-bottom: none; +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/job-group/index.scss b/src/CrystalQuartz.Application.Client2/src/main/main-content/job-group/index.scss new file mode 100644 index 0000000..c473e2b --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/job-group/index.scss @@ -0,0 +1,32 @@ +/* Job Group */ + +.data-row-job-group { + .primary-data { + border-right-color: #666666; + } + + .primary-data { + background: #666666; + } + + .data-container { + border-color: #444444; + } + + .data-container .data-group { + color: #FFFFFF; + font-size: 12px; + line-height: 20px; + font-weight: bold; + padding: 0 10px; + } + + .actions-toggle { + color: #CCCCCC; + } + + .actions-toggle:hover { + color: #FFFFFF; + background: #444444; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/job-group/job-group-view-model.ts b/src/CrystalQuartz.Application.Client2/src/main/main-content/job-group/job-group-view-model.ts new file mode 100644 index 0000000..0fe934f --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/job-group/job-group-view-model.ts @@ -0,0 +1,77 @@ +import { JobGroup, Job } from '../../../api'; +import { CommandService } from '../../../services'; +import { PauseGroupCommand, ResumeGroupCommand, DeleteGroupCommand } from '../../../commands/job-group-commands'; +import { ApplicationModel } from '../../../application-model'; +import { ManagableActivityViewModel } from '../activity-view-model'; +import ActivitiesSynschronizer from '../activities-synschronizer'; +import { JobViewModel } from '../job/job-view-model'; +import Timeline from '../../../timeline/timeline'; +import { IDialogManager } from '../../../dialogs/dialog-manager'; +import { ISchedulerStateService } from '../../../scheduler-state-service'; +import Action from '../../../global/actions/action'; + +import {SHOW_SCHEDULE_JOB_DIALOG} from '../../../dialogs/show-schedule-job-dialog'; +import { ObservableList } from 'john-smith/reactive'; + +export class JobGroupViewModel extends ManagableActivityViewModel { + jobs = new ObservableList(); + + scheduleJobAction = new Action( + 'Schedule Job', + () => { + SHOW_SCHEDULE_JOB_DIALOG( + this.dialogManager, + this.applicationModel, + this.commandService, + { + predefinedGroup: this.group.Name + }); + }); + + private jobsSynchronizer: ActivitiesSynschronizer = new ActivitiesSynschronizer( + (job: Job, jobViewModel: JobViewModel) => job.Name === jobViewModel.name, + (job: Job) => new JobViewModel(job, this.name, this.commandService, this.applicationModel, this.timeline, this.dialogManager, this.schedulerStateService), + this.jobs); + + constructor( + public group: JobGroup, + commandService: CommandService, + applicationModel: ApplicationModel, + private timeline: Timeline, + private dialogManager: IDialogManager, + private schedulerStateService: ISchedulerStateService) { + + super(group, commandService, applicationModel); + } + + updateFrom(group: JobGroup) { + super.updateFrom(group); + + this.jobsSynchronizer.sync(group.Jobs); + } + + getDeleteConfirmationsText(): string { + return 'Are you sure you want to delete all jobs?'; + } + + getPauseAction() { + return { + title: 'Pause all jobs', + command: () => new PauseGroupCommand(this.name) + }; + } + + getResumeAction() { + return { + title: 'Resume all jobs', + command: () => new ResumeGroupCommand(this.name) + }; + } + + getDeleteAction() { + return { + title: 'Delete all jobs', + command: () => new DeleteGroupCommand(this.name) + }; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/job-group/job-group-view.tsx b/src/CrystalQuartz.Application.Client2/src/main/main-content/job-group/job-group-view.tsx new file mode 100644 index 0000000..07a4a18 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/job-group/job-group-view.tsx @@ -0,0 +1,71 @@ +import { JobGroup } from '../../../api'; +import { JobGroupViewModel } from './job-group-view-model'; + +import { JobView } from '../job/job-view'; + +import Action from '../../../global/actions/action'; +import Separator from '../../../global/actions/separator'; + +import { View } from 'john-smith/view'; +import { List, Value } from 'john-smith/view/components'; +import { ActivityStatusView } from '../activity-status-view'; +import ActionView from '../../../global/actions/action-view'; + +export class JobGroupView implements View { + + constructor(private readonly viewModel: JobGroupViewModel) { + } + + // todo single root + template() { + const actions = [ + this.viewModel.pauseAction, + this.viewModel.resumeAction, + new Separator(), + this.viewModel.scheduleJobAction, + new Separator(), + this.viewModel.deleteAction + ]; + return
    +
    +
    +
    + + + +
    +
    {this.viewModel.name}
    +
    +
    + +
    +
    + +
    + +
    +
    ; + } + + // init(dom: js.IDom, viewModel: JobGroupViewModel) { + // super.init(dom, viewModel); + // dom('.js_jobs').observes(viewModel.jobs, JobView); + // } + // + // composeActions(viewModel: JobGroupViewModel): (Action | Separator)[] { + // return [ + // viewModel.pauseAction, + // viewModel.resumeAction, + // new Separator(), + // viewModel.scheduleJobAction, + // new Separator(), + // viewModel.deleteAction + // ]; + // } +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/job-group/job-group.tmpl.html b/src/CrystalQuartz.Application.Client2/src/main/main-content/job-group/job-group.tmpl.html new file mode 100644 index 0000000..d5485f3 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/job-group/job-group.tmpl.html @@ -0,0 +1,18 @@ +
    +
    +
    + + + +
    +
    +
    +
    + +
    +
    + +
    \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/job/index.scss b/src/CrystalQuartz.Application.Client2/src/main/main-content/job/index.scss new file mode 100644 index 0000000..cf212e7 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/job/index.scss @@ -0,0 +1,30 @@ +/* Job */ + +.data-row-job { + .primary-data { + border-bottom: 1px solid #DDDDDD; + background: #E9E9E9; + } + + .data-container { + border-right-color: #CCCCCC; + } + + .data-item { + width: auto; + min-width: 16.667%; + + // As job data-item is the only element at the row so + // give some additional for the ellipsis. + padding-right: 27px; + } + + .actions-toggle { + color: #666666; + } + + .actions-toggle:hover { + color: #333333; + background: #CCCCCC; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/job/job-view-model.ts b/src/CrystalQuartz.Application.Client2/src/main/main-content/job/job-view-model.ts new file mode 100644 index 0000000..8ccb974 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/job/job-view-model.ts @@ -0,0 +1,88 @@ +import { Job, Trigger } from '../../../api'; +import { PauseJobCommand, ResumeJobCommand, DeleteJobCommand, ExecuteNowCommand } from '../../../commands/job-commands'; +import { CommandService } from '../../../services'; +import { ApplicationModel } from '../../../application-model'; +import { ManagableActivityViewModel } from '../activity-view-model'; +import ActivitiesSynschronizer from '../activities-synschronizer'; +import { TriggerViewModel } from '../trigger/trigger-view-model'; +import Timeline from '../../../timeline/timeline'; + +import { IDialogManager } from '../../../dialogs/dialog-manager'; +import JobDetailsViewModel from '../../../dialogs/job-details/job-details-view-model'; + +import { ISchedulerStateService } from '../../../scheduler-state-service'; + +import CommandAction from '../../../command-action'; +import Action from '../../../global/actions/action'; +import {SHOW_SCHEDULE_JOB_DIALOG} from '../../../dialogs/show-schedule-job-dialog'; +import { ObservableList } from 'john-smith/reactive'; + +export class JobViewModel extends ManagableActivityViewModel { + triggers = new ObservableList(); + + executeNowAction = new CommandAction(this.applicationModel, this.commandService, 'Execute Now', () => new ExecuteNowCommand(this.group, this.name)); + addTriggerAction = new Action('Add Trigger', () => this.addTrigger()); + + private triggersSynchronizer: ActivitiesSynschronizer = new ActivitiesSynschronizer( + (trigger: Trigger, triggerViewModel: TriggerViewModel) => trigger.Name === triggerViewModel.name, + (trigger: Trigger) => new TriggerViewModel(trigger, this.commandService, this.applicationModel, this.timeline, this.dialogManager, this.schedulerStateService), + this.triggers); + + constructor( + private job: Job, + private group: string, + commandService: CommandService, + applicationModel: ApplicationModel, + private timeline: Timeline, + private dialogManager: IDialogManager, + private schedulerStateService: ISchedulerStateService) { + + super(job, commandService, applicationModel); + } + + loadJobDetails() { + this.dialogManager.showModal(new JobDetailsViewModel(this.job, this.commandService), result => {}); + } + + updateFrom(job: Job) { + super.updateFrom(job); + + this.triggersSynchronizer.sync(job.Triggers); + } + + getDeleteConfirmationsText(): string { + return 'Are you sure you want to delete job?'; + } + + getPauseAction() { + return { + title: 'Pause all triggers', + command: () => new PauseJobCommand(this.group, this.name) + }; + } + + getResumeAction() { + return { + title: 'Resume all triggers', + command: () => new ResumeJobCommand(this.group, this.name) + }; + } + + getDeleteAction() { + return { + title: 'Delete job', + command: () => new DeleteJobCommand(this.group, this.name) + }; + } + + private addTrigger() { + SHOW_SCHEDULE_JOB_DIALOG( + this.dialogManager, + this.applicationModel, + this.commandService, + { + predefinedGroup: this.job.GroupName, + predefinedJob: this.job.Name + }); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/job/job-view.tsx b/src/CrystalQuartz.Application.Client2/src/main/main-content/job/job-view.tsx new file mode 100644 index 0000000..9296376 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/job/job-view.tsx @@ -0,0 +1,66 @@ +import { JobViewModel } from './job-view-model'; +import { TriggerView } from '../trigger/trigger-view'; +import Action from '../../../global/actions/action'; +import Separator from '../../../global/actions/separator'; +import { View } from 'john-smith/view'; +import { List, Value } from 'john-smith/view/components'; +import { ActivityStatusView } from '../activity-status-view'; +import ActionView from '../../../global/actions/action-view'; + +export class JobView implements View { + + constructor(private readonly viewModel: JobViewModel) { + } + + // todo single root + template() { + const actions = [ + this.viewModel.pauseAction, + this.viewModel.resumeAction, + new Separator(), + this.viewModel.executeNowAction, + this.viewModel.addTriggerAction, + new Separator(), + this.viewModel.deleteAction + ]; + + return
    +
    +
    +
    + +
    + + + +
    +
    +
    + +
    + +
    +
    ; + } + + // init(dom: js.IDom, viewModel: JobViewModel) { + // super.init(dom, viewModel); + // + // dom('.triggers').observes(viewModel.triggers, TriggerView); + // dom('.js_viewDetails').on('click').react(viewModel.loadJobDetails); + // } + // + // composeActions(viewModel: JobViewModel): (Action | Separator)[] { + // return ; + // } +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/job/job.tmpl.html b/src/CrystalQuartz.Application.Client2/src/main/main-content/job/job.tmpl.html new file mode 100644 index 0000000..703a4ba --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/job/job.tmpl.html @@ -0,0 +1,18 @@ +
    +
    +
    + + +
    + + + +
    +
    +
    +
    + +
    diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/nullable-date-view.tsx b/src/CrystalQuartz.Application.Client2/src/main/main-content/nullable-date-view.tsx new file mode 100644 index 0000000..d6fb894 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/nullable-date-view.tsx @@ -0,0 +1,23 @@ +import { NullableDate } from '../../api'; +import { View } from 'john-smith/view'; +import DateUtils from '../../utils/date'; + +export class NullableDateView implements View { + + constructor(private readonly viewModel: NullableDate) { + } + + template = () => { + this.viewModel.isEmpty() ? + [none] + : DateUtils.smartDateFormat(this.viewModel.getDate()!) || ' ' + }; + + // init(dom: js.IDom, value: NullableDate) { + // if (value.isEmpty()) { + // dom.$.append('[none]'); + // } else { + // dom.$.append(DateUtils.smartDateFormat(value.getDate()) || ' '); + // } + // } +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/trigger/index.scss b/src/CrystalQuartz.Application.Client2/src/main/main-content/trigger/index.scss new file mode 100644 index 0000000..94d3720 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/trigger/index.scss @@ -0,0 +1,59 @@ +/* Trigger */ + +.data-row-trigger { + .primary-data { + border-bottom: 1px solid #DDDDDD; + } + + .actions-toggle { + color: #666666; + } + + .actions-toggle:hover { + color: #333333; + background: #DDDDDD; + } + + .data-container > a { + //color: #333333; + //cursor: pointer; + + //&:hover { + // text-decoration: none; + // color: #555555; + // position: relative; + // + // &::after { + // content: '...'; + // position: absolute; + // right: 4px; + // top: 4px; + // bottom: 0; + // + // height: 12px; + // padding: 0 5px 5px; + // font-size: 12px; + // font-weight: bold; + // line-height: 6px; + // color: #555555; + // text-decoration: none; + // vertical-align: middle; + // background: #DDDDDD; + // border: 0; + // border-radius: 1px; + // } + //} + } +} + +.data-row-trigger:hover { + background: $active-light; +} + +.triggers:last-child .data-row-trigger:last-child .primary-data { + border-bottom: none; +} + +.jobs:last-child .triggers:last-child .data-row-trigger:last-child .timeline-data { + border-bottom: none; +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/trigger/trigger-view-model.ts b/src/CrystalQuartz.Application.Client2/src/main/main-content/trigger/trigger-view-model.ts new file mode 100644 index 0000000..97bd4c2 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/trigger/trigger-view-model.ts @@ -0,0 +1,165 @@ +import { Trigger, NullableDate, SimpleTriggerType, CronTriggerType } from '../../../api'; +import { PauseTriggerCommand, ResumeTriggerCommand, DeleteTriggerCommand } from '../../../commands/trigger-commands'; +import { TriggerDetailsViewModel } from '../../../dialogs/trigger-details/trigger-details-view-model'; +import { CommandService } from '../../../services'; +import { ApplicationModel } from '../../../application-model'; +import { ManagableActivityViewModel } from '../activity-view-model'; + +import TimelineSlot from '../../../timeline/timeline-slot'; +import Timeline from '../../../timeline/timeline'; + +import { IDialogManager } from '../../../dialogs/dialog-manager'; + +import { ISchedulerStateService, EventType } from '../../../scheduler-state-service'; +import { ObservableValue } from 'john-smith/reactive'; +import { Disposable } from 'john-smith/common'; + +interface TimespanPart { + multiplier: number; + pluralLabel: string; + label: string; +} + +export class TriggerViewModel extends ManagableActivityViewModel { + startDate = new ObservableValue(new NullableDate(null)); + endDate = new ObservableValue(new NullableDate(null)); + previousFireDate = new ObservableValue(new NullableDate(null)); + nextFireDate = new ObservableValue(new NullableDate(null)); + triggerType = new ObservableValue(''); + executing = new ObservableValue(false); + + timelineSlot: TimelineSlot; + + private _group: string; + private _realtimeWire: Disposable; + + constructor( + private trigger: Trigger, + commandService: CommandService, + applicationModel: ApplicationModel, + private timeline: Timeline, + private dialogManager: IDialogManager, + private schedulerStateService: ISchedulerStateService) { + + super(trigger, commandService, applicationModel); + + const slotKey = 3 + ':' + trigger.UniqueTriggerKey; + + this._group = trigger.GroupName; + this.timelineSlot = timeline.findSlotBy(slotKey) || timeline.addSlot({ key: slotKey }); + this._realtimeWire = schedulerStateService.realtimeBus.listen(event => { + if (event.uniqueTriggerKey === trigger.UniqueTriggerKey) { + if (event.eventType === EventType.Fired) { + this.executing.setValue(true); + } else { + this.executing.setValue(false); + } + } + }); + } + + releaseState() { + this.timeline.removeSlot(this.timelineSlot); + } + + updateFrom(trigger: Trigger) { + super.updateFrom(trigger); + + this.startDate.setValue(new NullableDate(trigger.StartDate)); + this.endDate.setValue(new NullableDate(trigger.EndDate)); + this.previousFireDate.setValue(new NullableDate(trigger.PreviousFireDate)); + this.nextFireDate.setValue(new NullableDate(trigger.NextFireDate)); + + var triggerType = trigger.TriggerType; + var triggerTypeMessage = 'unknown'; + if (triggerType.Code === 'simple') { + var simpleTriggerType = triggerType; + + triggerTypeMessage = 'repeat '; + if (simpleTriggerType.RepeatCount === -1) { + } else { + triggerTypeMessage += simpleTriggerType.RepeatCount + ' times '; + } + + triggerTypeMessage += 'every '; + + var parts: TimespanPart[] = [ + { + label: 'day', + pluralLabel: 'days', + multiplier: 1000 * 60 * 60 * 24 + }, + { + label: 'hour', + pluralLabel: 'hours', + multiplier: 1000 * 60 * 60 + }, + { + label: 'minute', + pluralLabel: 'min', + multiplier: 1000 * 60 + }, + { + label: 'second', + pluralLabel: 'sec', + multiplier: 1000 + } + ]; + + var diff = simpleTriggerType.RepeatInterval; + var messagesParts: string[] = []; + for (var i = 0; i < parts.length; i++) { + var part = parts[i]; + var currentPartValue = Math.floor(diff / part.multiplier); + diff -= currentPartValue * part.multiplier; + + if (currentPartValue === 1) { + messagesParts.push(part.label); + } else if (currentPartValue > 1) { + messagesParts.push(currentPartValue + ' ' + part.pluralLabel); + } + } + + triggerTypeMessage += messagesParts.join(', '); + } else if (triggerType.Code === 'cron') { + var cronTriggerType = triggerType; + + triggerTypeMessage = cronTriggerType.CronExpression; + } + + this.triggerType.setValue(triggerTypeMessage); + } + + getDeleteConfirmationsText(): string { + return 'Are you sure you want to unschedue the trigger?'; + } + + getPauseAction() { + return { + title: 'Pause trigger', + command: () => new PauseTriggerCommand(this._group, this.name) + }; + } + + getResumeAction() { + return { + title: 'Resume trigger', + command: () => new ResumeTriggerCommand(this._group, this.name) + }; + } + + getDeleteAction() { + return { + title: 'Delete trigger', + command: () => new DeleteTriggerCommand(this._group, this.name) + }; + } + + requestCurrentActivityDetails() { + this.timelineSlot.requestCurrentActivityDetails(); + } + + showDetails() { + this.dialogManager.showModal(new TriggerDetailsViewModel(this.trigger, this.commandService), () => {}); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-content/trigger/trigger-view.tsx b/src/CrystalQuartz.Application.Client2/src/main/main-content/trigger/trigger-view.tsx new file mode 100644 index 0000000..79fe7ca --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-content/trigger/trigger-view.tsx @@ -0,0 +1,64 @@ +import { Trigger } from '../../../api'; + +import { TriggerViewModel } from './trigger-view-model'; + +import { NullableDateView } from '../nullable-date-view'; + + +import { View } from 'john-smith/view'; +import { TimelineSlotView } from '../../../timeline/timeline-slot-view'; +import Action from '../../../global/actions/action'; +import Separator from '../../../global/actions/separator'; +import { ActivityStatusView } from '../activity-status-view'; +import { List, Value } from 'john-smith/view/components'; +import ActionView from '../../../global/actions/action-view'; + +export class TriggerView implements View { + + constructor(private readonly viewModel: TriggerViewModel) { + } + + template() { + const actions = [ + this.viewModel.pauseAction, + this.viewModel.resumeAction, + new Separator(), + this.viewModel.deleteAction + ]; + + return
    +
    +
    + +
    + + +
    + {this.viewModel.name} +
    {this.viewModel.triggerType}
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    ; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-header/header-view-model.ts b/src/CrystalQuartz.Application.Client2/src/main/main-header/header-view-model.ts new file mode 100644 index 0000000..123dcac --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-header/header-view-model.ts @@ -0,0 +1,65 @@ +import { SchedulerData } from '../../api'; +import { CommandService } from '../../services'; +import { StartSchedulerCommand, StopSchedulerCommand, PauseSchedulerCommand, ResumeSchedulerCommand, StandbySchedulerCommand } from '../../commands/scheduler-commands'; +import { ApplicationModel } from '../../application-model'; +import CommandProgressViewModel from '../../command-progress/command-progress-view-model'; +// import Timeline from '../../timeline/timeline'; + +// import { IDialogManager } from '../dialogs/dialog-manager'; +import SchedulerDetails from '../../dialogs/scheduler-details/scheduler-details-view-model'; +// +import Action from '../../global/actions/action'; +import CommandAction from '../../command-action'; +import {SHOW_SCHEDULE_JOB_DIALOG} from '../../dialogs/show-schedule-job-dialog'; +import { ObservableValue } from 'john-smith/reactive'; +import Timeline from '../../timeline/timeline'; +import { IDialogManager } from '../../dialogs/dialog-manager'; + +export default class MainHeaderViewModel { + name = new ObservableValue(null); + instanceId = new ObservableValue(null); + + status = new ObservableValue(null); + + startAction = new CommandAction(this.application, this.commandService, 'Start', () => new StartSchedulerCommand()); + pauseAllAction = new CommandAction(this.application, this.commandService, 'Pause All', () => new PauseSchedulerCommand()); + resumeAllAction = new CommandAction(this.application, this.commandService, 'Resume All', () => new ResumeSchedulerCommand()); + standbyAction = new CommandAction(this.application, this.commandService, 'Standby', () => new StandbySchedulerCommand()); + shutdownAction = new CommandAction(this.application, this.commandService, 'Shutdown', () => new StopSchedulerCommand(), 'Are you sure you want to shutdown scheduler?'); + + scheduleJobAction = new Action( + '+', + () => { this.scheduleJob(); }); + + commandProgress = new CommandProgressViewModel(this.commandService); + + constructor( + public timeline: Timeline, + private commandService: CommandService, + private application: ApplicationModel, + private dialogManager: IDialogManager) { } + + updateFrom(data: SchedulerData) { + this.name.setValue(data.Name); + this.instanceId.setValue(data.InstanceId); + this.status.setValue(data.Status); + + this.startAction.enabled = data.Status === 'ready'; + this.shutdownAction.enabled = (data.Status !== 'shutdown'); + this.standbyAction.enabled = data.Status === 'started'; + this.pauseAllAction.enabled = data.Status === 'started'; + this.resumeAllAction.enabled = data.Status === 'started'; + this.scheduleJobAction.enabled = data.Status !== 'shutdown'; + } + + showSchedulerDetails() { + this.dialogManager.showModal(new SchedulerDetails(this.commandService), result => {}); + } + + private scheduleJob() { + SHOW_SCHEDULE_JOB_DIALOG( + this.dialogManager, + this.application, + this.commandService); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-header/header-view.tsx b/src/CrystalQuartz.Application.Client2/src/main/main-header/header-view.tsx new file mode 100644 index 0000000..de8be55 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-header/header-view.tsx @@ -0,0 +1,115 @@ +import { Value } from 'john-smith/view/components'; +import ViewModel from './header-view-model'; + +// import TimelineCaptionsView from '../timeline/timeline-captions-view'; +// import CommandProgressView from '../command-progress/command-progress-view'; +// import ActionView from '../global/actions/action-view'; +// import ActionsUtils from '../global/actions/actions-utils'; +// import Action from '../global/actions/action'; +// import Separator from '../global/actions/separator'; + +// import TEMPLATE from './header.tmpl.html'; +import { HtmlDefinition, View } from 'john-smith/view'; +import CommandProgressView from '../../command-progress/command-progress-view'; +import { map } from 'john-smith/reactive/transformers/map'; +import ActionView from '../../global/actions/action-view'; +import TimelineCaptionsView from '../../timeline/timeline-captions-view'; +// import Error = types.Error; +// import TimelineCaptionsView from '../../timeline/timeline-captions-view'; +// import CommandProgressView from '../../command-progress/command-progress-view'; +// import Action from '../../global/actions/action'; +// import Separator from '../../global/actions/separator'; +// import ActionsUtils from '../../global/actions/actions-utils'; +// import ActionView from '../../global/actions/action-view'; + +export default class MainHeaderView implements View { + constructor( + private readonly viewModel: ViewModel + ) { + } + + template(): HtmlDefinition { + const schedulerStatusTitle = map( + this.viewModel.status, + status => status === null + ? '' + : 'Scheduler is ' + status + ); + + return
    +
    + + +
    +
      + +
    + +
      + +
    + +
      +
      + +
      + +
      +
      + +
      +
      +
      +
      + +
      +
      Trigger
      +
      Schedule
      +
      Start Date
      +
      End Date
      +
      Previous Fire Date
      +
      Next Fire Date
      +
      +
      + +
      + +
      +
      +
      ; + } + + // init(dom: js.IDom, viewModel: ViewModel) { + // dom('.ticks-container').render(TimelineCaptionsView, viewModel.timeline); + // + // dom('.js_viewDetails').on('click').react(viewModel.showSchedulerDetails); + // + // const actions: [Action | Separator] = [ + // viewModel.pauseAllAction, + // viewModel.resumeAllAction, + // new Separator(), + // viewModel.standbyAction, + // viewModel.shutdownAction + // ]; + // + // ActionsUtils.render(dom('.js_actions'), actions); + // + // dom('.js_primaryActions').render(ActionView, viewModel.startAction); + // + // dom('.js_scheduleJob').render(ActionView, viewModel.scheduleJobAction); + // } +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-header/header.tmpl.html b/src/CrystalQuartz.Application.Client2/src/main/main-header/header.tmpl.html new file mode 100644 index 0000000..d17de9b --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-header/header.tmpl.html @@ -0,0 +1,50 @@ +
      +
      +
      +
      + +
      + + + + ... + +
      + +
      +
        + +
      + +
        + +
          + + +
          + +
          +
          + +
          +
          +
          +
          + +
          +
          Trigger
          +
          Schedule
          +
          Start Date
          +
          End Date
          +
          Previous Fire Date
          +
          Next Fire Date
          +
          +
          + +
          +
          +
          \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-header/index.scss b/src/CrystalQuartz.Application.Client2/src/main/main-header/index.scss new file mode 100644 index 0000000..de06ea0 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-header/index.scss @@ -0,0 +1,165 @@ +@import './scheduler-status'; +@import "../../global/constants"; + +/* Header */ + +.main-header { + position: fixed; + top: 0; + left: $aside-width; + right: 0; + height: $main-header-primary-height + $main-header-secondary-height; +} + +@keyframes buttonHighlight { + from { + background-color: #555555; + } + + to { + background-color: $green-dark; + } +} + +@keyframes valueHighlight { + 0% { + color: #FFFFFF; + } + + 25% { + color: #E5D45B; + } + + 100% { + color: #FFFFFF; + } +} + +/* Header -> Scheduler Header */ + +.scheduler-header { + background: #666666; + color: #FFFFFF; + height: $main-header-primary-height; +} + +.scheduler-caption { + width: 25%; + height: 100%; + overflow: hidden; + float: left; + + .scheduler-name { + padding: 0 0 0 30px; + margin: 0; + font-size: 18px; + line-height: $main-header-primary-height; + text-decoration: none; + display: block; + color: #FFFFFF; + + .ellipsis { + color: #DDDDDD; + background: #555555; + } + } + + .status { + margin-top: 5px; + } +} + +.scheduler-toolbar { + width: 25%; + float: left; + + .primary-actions, + .secondary-actions, + .schedule-job-actions { + margin: 5px 0 0 0; + float: right; + padding: 0; + } + + .primary-actions li a/*, + .schedule-job-actions li a*/ { + float: left; + width: 100px; + height: $main-header-primary-height - 10; + line-height: $main-header-primary-height - 10; + margin-left: 5px; + text-align: center; + font-size: 12px; + text-decoration: none; + background: $green-dark; + color: #FFFFFF; + + &:hover { + background: $green; + } + } + + .primary-actions li.disabled a, + .primary-actions li.disabled a:hover, + .schedule-job-actions li.disabled a, + .schedule-job-actions li.disabled a:hover { + background: #777777; + color: #AAAAAA; + } + + .secondary-actions { + margin-left: 5px; + margin-right: 2px; + } + + .schedule-job-actions li a, + .secondary-actions .actions-toggle { + width: 20px; + background: #555555; + color: #DDDDDD; + display: block; + text-align: center; + text-decoration: none; + + &:hover { + color: #FFFFFF; + background: #444444; + } + } + + /* + .schedule-job-actions li a { + width: 20px; + }*/ +} + +.data-header { + width: 100%; + overflow: hidden; + background: #AAAAAA; + + .primary-data { + width: 50%; + float: left; + border-bottom-color: #666666; + border-right-color: #AAAAAA; + } + + .ticks-container { + width: 50%; + float: left; + background: #E9E9E9; + position: relative; + height: $main-header-secondary-height; + overflow: hidden; + } + + .data-container { + border-right: none; + } + + .data-header-item { + color: #FFFFFF; + border-right-color: #666666; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-header/index_sm.less b/src/CrystalQuartz.Application.Client2/src/main/main-header/index_sm.less new file mode 100644 index 0000000..f751717 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-header/index_sm.less @@ -0,0 +1,20 @@ +.main-header { + height: @main-header-primary-height + 2 * @main-header-secondary-height; +} + +.scheduler-caption, +.scheduler-toolbar { + width: 50%; +} + +.scheduler-caption { + padding-right: @loading-image-width/2; +} + +.scheduler-toolbar { + padding-left: @loading-image-width/2; +} + +.scheduler-header .toolbar { + margin-right: 5px; +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/main/main-header/scheduler-status.scss b/src/CrystalQuartz.Application.Client2/src/main/main-header/scheduler-status.scss new file mode 100644 index 0000000..03e46de --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main-header/scheduler-status.scss @@ -0,0 +1,31 @@ +/* Scheduler status */ + +/* todo: share styles with activity-status */ + +.scheduler-status { + float: left; + height: 10px; + width: 10px; + margin: 5px 0 0 10px; + transition: background 1s; + border-radius: 50%; + -moz-border-radius: 50%; + -ms-border-radius: 50%; + -webkit-border-radius: 50%; + + &.empty { + background: #EEEEEE; + } + + &.ready { + background: #E5D45B; + } + + &.started { + background: #38C049; + } + + &.shutdown { + background: #AAAAAA; + } +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/main/main.view-model.ts b/src/CrystalQuartz.Application.Client2/src/main/main.view-model.ts new file mode 100644 index 0000000..47dc893 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main.view-model.ts @@ -0,0 +1,104 @@ +import { CommandService } from '../services'; +import { ApplicationModel } from '../application-model'; +import { MainAsideViewModel } from './main-aside/aside.view-model'; + +import ActivitiesSynschronizer from './main-content/activities-synschronizer'; +import { JobGroup, SchedulerData, EnvironmentData } from '../api'; +// import { JobGroupViewModel } from './main-content/job-group/job-group-view-model'; +import MainHeaderViewModel from './main-header/header-view-model'; + +import Timeline from '../timeline/timeline'; + +// import { DialogManager } from './dialogs/dialog-manager'; + +import { INotificationService, DefaultNotificationService } from '../notification/notification-service'; +// import { SchedulerStateService } from './scheduler-state-service'; + +// import GlobalActivitiesSynchronizer from './global-activities-synchronizer'; +// import {OfflineModeViewModel} from "./offline-mode/offline-mode-view-model"; +import DateUtils from "../utils/date"; +import { TimelineInitializer } from "../timeline/timeline-initializer"; + +import ActivityDetailsViewModel from '../dialogs/activity-details/activity-details-view-model'; +import { ObservableList, ObservableValue } from 'john-smith/reactive'; +import { DialogManager } from '../dialogs/dialog-manager'; +import { JobGroupViewModel } from './main-content/job-group/job-group-view-model'; +import GlobalActivitiesSynchronizer from '../global-activities-synchronizer'; +import { SchedulerStateService } from '../scheduler-state-service'; + +export class MainViewModel { + private groupsSynchronizer: ActivitiesSynschronizer; + private _schedulerStateService = new SchedulerStateService(); + private _serverInstanceMarker: number|null = null; + + dialogManager = new DialogManager(); + + timeline: Timeline; + + mainAside: MainAsideViewModel; + mainHeader: MainHeaderViewModel; + + jobGroups = new ObservableList(); + // offlineMode = new ObservableValue(null); + + globalActivitiesSynchronizer: GlobalActivitiesSynchronizer; + + constructor( + private application: ApplicationModel, + private commandService: CommandService, + public environment: EnvironmentData, + public notificationService: DefaultNotificationService, + timelineInitializer: TimelineInitializer) { + + this.timeline = timelineInitializer.timeline; + this.globalActivitiesSynchronizer = timelineInitializer.globalActivitiesSynchronizer; + + this.mainAside = new MainAsideViewModel(this.application); + this.mainHeader = new MainHeaderViewModel( + this.timeline, + this.commandService, + this.application, + this.dialogManager); + + commandService.onCommandFailed.listen(error => notificationService.showError(error.errorMessage)); + commandService.onDisconnected.listen(() => application.goOffline()); + + this.groupsSynchronizer = new ActivitiesSynschronizer( + (group: JobGroup, groupViewModel: JobGroupViewModel) => group.Name === groupViewModel.name, + (group: JobGroup) => new JobGroupViewModel(group, this.commandService, this.application, this.timeline, this.dialogManager, this._schedulerStateService), + this.jobGroups); + + application.onDataChanged.listen(data => this.setData(data)); + + // application.isOffline.listen(isOffline => { + // const offlineModeViewModel = isOffline ? + // new OfflineModeViewModel(this.application.offlineSince, this.commandService, this.application) : + // null; + // + // this.offlineMode.setValue(offlineModeViewModel); + // }); + + this.timeline.detailsRequested.listen(activity => { + this.dialogManager.showModal(new ActivityDetailsViewModel(activity), _ => {}); + }); + } + + get autoUpdateMessage() { + return this.application.autoUpdateMessage; + } + + private setData(data: SchedulerData) { + if (this._serverInstanceMarker !== null && this._serverInstanceMarker !== data.ServerInstanceMarker) { + this.notificationService.showError('Server restart detected at ' + DateUtils.smartDateFormat(new Date().getTime())); + this.commandService.resetEvents(); + this.timeline.clearSlots(); + } + + this._serverInstanceMarker = data.ServerInstanceMarker; + + this.groupsSynchronizer.sync(data.JobGroups); + this.mainHeader.updateFrom(data); + this._schedulerStateService.synsFrom(data); + this.globalActivitiesSynchronizer.updateFrom(data); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/main/main.view.tsx b/src/CrystalQuartz.Application.Client2/src/main/main.view.tsx new file mode 100644 index 0000000..8556ca4 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/main/main.view.tsx @@ -0,0 +1,108 @@ +import { DomElement, HtmlDefinition, View } from 'john-smith/view'; +import { MainViewModel } from './main.view-model'; +import { List, Value } from 'john-smith/view/components'; +import 'john-smith/binding/ext/bind'; +import { MainAsideView } from './main-aside/aside.view'; +import MainHeaderView from './main-header/header-view'; +import { JobGroupView } from './main-content/job-group/job-group-view'; +import SchedulerDetailsViewModel from '../dialogs/scheduler-details/scheduler-details-view-model'; +import SchedulerDetailsView from '../dialogs/scheduler-details/scheduler-details-view'; +import JobDetailsView from '../dialogs/job-details/job-details-view'; +import ActivityDetailsView from '../dialogs/activity-details/activity-details-view'; +import { TriggerDetailsView } from '../dialogs/trigger-details/trigger-details-view'; +import { ScheduleJobView } from '../dialogs/schedule-job/schedule-job-view'; +import JobDetailsViewModel from '../dialogs/job-details/job-details-view-model'; +import ActivityDetailsViewModel from '../dialogs/activity-details/activity-details-view-model'; +import { TriggerDetailsViewModel } from '../dialogs/trigger-details/trigger-details-view-model'; +import { ScheduleJobViewModel } from '../dialogs/schedule-job/schedule-job-view-model'; +import DialogsViewFactory, { IDialogConfig } from '../dialogs/dialogs-view-factory'; +import { TimelineTooltipsView } from '../timeline/timeline-tooltips-view'; +import { NativeDomEngine } from 'john-smith/view/dom-engine-native'; + +export class MainView implements View { + constructor(private readonly viewModel: MainViewModel) { + } + + template(): HtmlDefinition { + let schedulerDetailsDialog: IDialogConfig = { viewModel: SchedulerDetailsViewModel, view: SchedulerDetailsView }; + + const dialogsConfig: IDialogConfig[] = [ + schedulerDetailsDialog, + { viewModel: JobDetailsViewModel, view: JobDetailsView }, + { viewModel: ActivityDetailsViewModel, view: ActivityDetailsView }, + { viewModel: TriggerDetailsViewModel, view: TriggerDetailsView }, + { viewModel: ScheduleJobViewModel, view: ScheduleJobView } + ]; + + // dom('.js_offline_mode').observes(viewModel.offlineMode, OfflineModeView); + + // dom('.js_notifications').render(new NotificationsView(), viewModel.notificationService.notifications); + + let dialogManagerView = new DialogsViewFactory().createView(dialogsConfig); + + let tooltipsContainer: DomElement | null = null; + + const tooltipsContainerWidthCalculator = (): number => { + if (tooltipsContainer === null) { + return 0; + } + + return ((tooltipsContainer as any).element as HTMLElement).clientWidth; + } + + return
          +
          + +
          + +
          + +
          + +
          +
          +
          { + tooltipsContainer = element + }}> + +
          +
          + +
          +
          +
          + +
          +
          +
          + CrystalQuartz Panel {this.viewModel.environment.SelfVersion} +
          +
          + Quartz.NET {this.viewModel.environment.QuartzVersion} +
          +
          + Host Platform {this.viewModel.environment.DotNetVersion} +
          +
          + +
          + {this.viewModel.autoUpdateMessage} +
          +
          + +
          + +
          + +
          + +
          +
          + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/notification/index.scss b/src/CrystalQuartz.Application.Client2/src/notification/index.scss new file mode 100644 index 0000000..7c10303 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/notification/index.scss @@ -0,0 +1,45 @@ +@import "../global/constants"; + +ul.notifications, +ul.notifications li { + list-style-type: none; + list-style-image: none; +} + +.notifications { + margin: 0; + position: fixed; + bottom: $main-footer-height + 20; + left: $aside-width + 20; + width: 500px; + + li { + opacity: 1; + transition: opacity 0.5s, margin-bottom 0.5s; + margin-top: 20px; + + a { + display: block; + background: #cb4437; + padding: 20px; + color: #FFFFFF; + font-size: 18px; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); + height: 90px; + text-decoration: none !important; + } + + a:hover { + background: darken(#cb4437, 10%) + } + } + + li.showing { + opacity: 0; + margin-bottom: -110px; + } + + li.hiding { + opacity: 0; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/notification/notification-service.ts b/src/CrystalQuartz.Application.Client2/src/notification/notification-service.ts new file mode 100644 index 0000000..935b915 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/notification/notification-service.ts @@ -0,0 +1,29 @@ +import Notification from './notification'; +import { ObservableList } from 'john-smith/reactive'; + +export interface INotificationService { + showError(content: string): void; +} + +export class DefaultNotificationService implements INotificationService { + notifications = new ObservableList(); + + constructor() { + (window as any)['showError'] = (m: string) => this.showError(m); // todo is it for testing? + } + + showError(content: string) { + const notification = new Notification(content); + + const toDispose = notification.outdated.listen(() => { + this.hide(notification); + toDispose.dispose(); + }); + + this.notifications.add(notification); + } + + private hide(notification: Notification) { + this.notifications.remove(notification); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/notification/notification.ts b/src/CrystalQuartz.Application.Client2/src/notification/notification.ts new file mode 100644 index 0000000..ca3946c --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/notification/notification.ts @@ -0,0 +1,44 @@ +import { Event } from 'john-smith/reactive/event'; + +export default class Notification { + outdated = new Event(); + + private _timerRef: number | null = null; + + constructor( + public content: string) { + + this.scheduleClosing(); + } + + forceClosing() { + this.clearTimer(); + this.close(); + } + + disableClosing() { + this.clearTimer(); + } + + enableClosing() { + this.scheduleClosing(); + } + + private scheduleClosing() { + this._timerRef = setTimeout(() => { + this.outdated.trigger(null); + this.close(); + }, 7000) as any; + } + + private close() { + this.outdated.trigger(null); + } + + private clearTimer() { + if (this._timerRef) { + clearTimeout(this._timerRef); + this._timerRef = null; + } + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/notification/notifications-view.tsx b/src/CrystalQuartz.Application.Client2/src/notification/notifications-view.tsx new file mode 100644 index 0000000..f2a5846 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/notification/notifications-view.tsx @@ -0,0 +1,50 @@ +import Notification from './notification'; +import { View } from 'john-smith/view'; +import { ObservableList, ObservableValue } from 'john-smith/reactive'; +import { List } from 'john-smith/view/components'; + +class NotificationView implements View { + constructor(private readonly notification: Notification) { + } + + public template() { + const className = new ObservableValue('showing'); + + setTimeout(() => { + className.setValue(''); + }); + + return
        • + {this.notification.content} +
        • + }; + + // todo + // init(dom: js.IDom, ) { + // const wire = dom.onUnrender().listen(() => { + // dom.$.addClass('hiding'); + // setTimeout(() => { + // dom.$.remove(); + // wire.dispose(); + // }, + // 500); + // }); + // } +} + +export class NotificationsView implements View { + constructor( + private readonly notifications: ObservableList) { + } + + template() { + return
            + +
          ; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/scheduler-explorer.ts b/src/CrystalQuartz.Application.Client2/src/scheduler-explorer.ts new file mode 100644 index 0000000..dd770d4 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/scheduler-explorer.ts @@ -0,0 +1,8 @@ +import {JobGroup} from './api'; + +/** + * Provides access to basic scheduler data: groups, jobs, triggers. + */ +export interface SchedulerExplorer { + listGroups(): JobGroup[]; +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/scheduler-state-service.ts b/src/CrystalQuartz.Application.Client2/src/scheduler-state-service.ts new file mode 100644 index 0000000..b780a06 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/scheduler-state-service.ts @@ -0,0 +1,55 @@ +import { SchedulerData } from './api'; +import { Event } from 'john-smith/reactive/event'; + +export enum EventType { + Fired, + Completed +} + +export interface IRealtimeTriggerEvent { + uniqueTriggerKey: string; + eventType: EventType; +} + +interface ITriggersHashSet { + [uniqueTriggerKey: string]: boolean; +} + +export interface ISchedulerStateService { + realtimeBus: Event; +} + +export class SchedulerStateService implements ISchedulerStateService { + private _currentInProgress: ITriggersHashSet = {}; + + realtimeBus = new Event(); + + synsFrom(data: SchedulerData) { + if (data.InProgress) { + const nextInProgress: ITriggersHashSet = {}; + + for (var i = 0; i < data.InProgress.length; i++) { + nextInProgress[data.InProgress[i].UniqueTriggerKey] = true; + } + + const + completed = this.findDiff(this._currentInProgress, nextInProgress), + fired = this.findDiff(nextInProgress, this._currentInProgress), + completedEvents = completed.map((x:string) => ({ uniqueTriggerKey: x, eventType: EventType.Completed })), + firedEvents = fired.map((x: string) => ({ uniqueTriggerKey: x, eventType: EventType.Fired })), + allEvents = completedEvents.concat(firedEvents); + + for (var j = 0; j < allEvents.length; j++) { + this.realtimeBus.trigger(allEvents[j]); + } + + this._currentInProgress = nextInProgress; + } + } + + private findDiff(primary: ITriggersHashSet, secondary: ITriggersHashSet): string[] { + const keys: string[] = Object.keys(primary); + + return keys.filter((key: string) => !secondary[key]); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/services/index.ts b/src/CrystalQuartz.Application.Client2/src/services/index.ts new file mode 100644 index 0000000..be435ce --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/services/index.ts @@ -0,0 +1,96 @@ +import { ICommand } from '../commands/contracts'; +import { Property, SchedulerEvent } from '../api'; +import { Event } from 'john-smith/reactive/event'; + +export interface CommandResult { + _ok: boolean; + _err: string; +} + +export interface ErrorInfo { + errorMessage: string; + details?: Property[]; + disconnected?: boolean; +} + +export class CommandService { + onCommandStart = new Event>(); + onCommandComplete = new Event>(); + onCommandFailed = new Event(); + onEvent = new Event(); + onDisconnected = new Event(); + + private _minEventId = 0; + + constructor( + private readonly _url: string, + private readonly _headers: { [key: string]: string } | null + ) { + } + + resetEvents() { + this._minEventId = 0; + } + + executeCommand(command: ICommand, suppressError: boolean = false): Promise { + const data = { + ...command.data, + ...{ command: command.code, minEventId: this._minEventId } + }; + + const formData = new FormData(); + + Object.keys(data).forEach((key) => { + formData.append(key, data[key]); + }) + + this.onCommandStart.trigger(command); + + return fetch(this._url, { method: 'POST', body: new URLSearchParams(data), headers: this._headers ?? undefined }) + .catch(() => { + this.onDisconnected.trigger(null); + + return Promise.reject({ + disconnected: true, + errorMessage: 'Server is not available' + }); + }) + .then(res => res.json()) + .then(response => { + var comandResult = response; + if (comandResult._ok) { + const mappedResult = command.mapper ? command.mapper(response) : response; + + /* Events handling */ + var eventsResult: any = mappedResult, + events: SchedulerEvent[] = eventsResult.Events; + + if (events && events.length > 0) { + for (var i = 0; i < events.length; i++) { + this.onEvent.trigger(events[i]); + } + + this._minEventId = events.reduce((acc, current) => Math.max(acc, current.id), 0); + } + + return mappedResult; + } else { + return Promise.reject({ + errorMessage: comandResult._err, + details: null + }); + } + }) + .catch(reason => { + if (!suppressError || reason.disconnected) { + this.onCommandFailed.trigger(reason); + } + + return Promise.reject(reason); + }) + .finally(() => { + this.onCommandComplete.trigger(command); + }); + } +} + diff --git a/src/CrystalQuartz.Application.Client2/src/startup/favicon-renderer.ts b/src/CrystalQuartz.Application.Client2/src/startup/favicon-renderer.ts new file mode 100644 index 0000000..bee81d4 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/startup/favicon-renderer.ts @@ -0,0 +1,105 @@ +import { FaviconStatus } from './startup.view-model'; + +interface IFaviconStatusRenderer { + draw(context: CanvasRenderingContext2D): void; +} + +const + COLOR_PRIMARY = '#38C049', + COLOR_SECONDARY = '#E5D45B', + COLOR_ERROR = '#CB4437', + COLOR_WHITE = '#FFFFFF'; + +function drawCircle(context: CanvasRenderingContext2D, color: string, angleStart: number, angleEnd: number, radius: number = 5){ + context.beginPath(); + context.arc(8, 8, radius, angleStart, angleEnd); + context.fillStyle = color; + context.fill(); +} + +class LoadingFaviconRenderer implements IFaviconStatusRenderer { + draw(context: CanvasRenderingContext2D): void { + drawCircle(context, COLOR_WHITE, 0, Math.PI * 2, 6); + drawCircle(context, COLOR_PRIMARY, Math.PI / 2, Math.PI * 1.5); + drawCircle(context, COLOR_SECONDARY, Math.PI * 1.5, Math.PI / 2); + } +} + +class SolidFaviconRenderer implements IFaviconStatusRenderer { + constructor(private color: string){} + + draw(context: CanvasRenderingContext2D): void { + drawCircle(context, this.color, 0, 2 * Math.PI); + + context.strokeStyle = COLOR_WHITE; + context.stroke(); + } +} + +class BrokenFaviconRenderer implements IFaviconStatusRenderer { + constructor(){} + + draw(context: CanvasRenderingContext2D): void { + drawCircle(context, COLOR_PRIMARY, Math.PI / 2, Math.PI * 1.5); + drawCircle(context, COLOR_SECONDARY, Math.PI * 1.5, Math.PI / 2); + + context.beginPath(); + + const + crossOriginX = 9, + crossOriginY = 8, + crossWidth = 5; + + context.beginPath(); + context.strokeStyle = COLOR_ERROR; + context.lineWidth = 2; + context.moveTo(crossOriginX, crossOriginY); + context.lineTo(crossOriginX + crossWidth, crossOriginY + crossWidth); + context.moveTo(crossOriginX, crossOriginY + crossWidth); + context.lineTo(crossOriginX + crossWidth, crossOriginY); + context.stroke(); + } +} + +export class FaviconRenderer { + _factory: {[key: string]: () => IFaviconStatusRenderer } = {}; + + constructor(){ + this._factory[FaviconStatus.Loading] = () => new LoadingFaviconRenderer(); + this._factory[FaviconStatus.Ready] = () => new SolidFaviconRenderer(COLOR_SECONDARY); + this._factory[FaviconStatus.Active] = () => new SolidFaviconRenderer(COLOR_PRIMARY); + this._factory[FaviconStatus.Broken] = () => new BrokenFaviconRenderer(); + } + + render(faviconStatus: FaviconStatus) { + const + $canvas = document.createElement('canvas'), + $link = document.createElement('link'), // $(''), + canvas:any = $canvas; + + $link.setAttribute('class', 'cq-favicon'); + $link.setAttribute('rel', 'icon'); + $link.setAttribute('type', 'image'); + + if (typeof canvas.getContext == 'function') { + $canvas.setAttribute('width', '16'); + $canvas.setAttribute('height', '16'); + + const context = canvas.getContext('2d'); + + this._factory[faviconStatus]().draw(context); + + $link.setAttribute('href', canvas.toDataURL('image/png')); + + const prevFavicon = document.getElementsByClassName('cq-favicon'); + if (prevFavicon !== undefined && prevFavicon.length > 0) { + prevFavicon.item(0)!.remove(); + } + + document.getElementsByTagName('head')![0]!.append($link); + // const $head = $('head'); + // $head.find('.cq-favicon').remove(); + // $head.append($link); + } + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/startup/headers-extractor.ts b/src/CrystalQuartz.Application.Client2/src/startup/headers-extractor.ts new file mode 100644 index 0000000..a7ec7cf --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/startup/headers-extractor.ts @@ -0,0 +1,64 @@ +export type LocationAnalyzingResult = { + headers: null | { [key: string]: string}; + url: string; +} + +type CrystalQuartzParentOptions = { + headers: Record | undefined; +} + +type CrystalQuartzParentOptionsHolder = { + ['CQ_OPTIONS']: CrystalQuartzParentOptions | undefined; +} + +export const ANALYZE_LOCATION = () : LocationAnalyzingResult => { + try { + if (window && window.parent) { + const options: CrystalQuartzParentOptions | undefined = (window.parent as unknown as CrystalQuartzParentOptionsHolder)['CQ_OPTIONS']; + + if (options !== undefined) { + return { + url: '', + headers: options.headers ?? null + } + } + } + } catch (ex) { + } + + try { + const result: Record = {}; + let found = false; + + if (location.search) { + const queryStringParts = location.search.substr(1).split("&"); + const restParameters: string[] = []; + + for (let i = 0; i < queryStringParts.length; i++) { + const parts = queryStringParts[i].split("="); + const paramName = parts[0]; + + if (paramName.startsWith('h_')) { + result[paramName.substr(2)] = parts[1] && decodeURIComponent(parts[1]); + found = true; + } else { + restParameters.push(paramName + '=' + parts[1]); + } + } + + if (found) { + return { + headers: result, + url: location.pathname + (restParameters.length === 0 ? '' : '?') + + restParameters.join('&') + }; + } + } + } catch (ex) { + } + + return { + headers: null, + url: '' + }; +} diff --git a/src/CrystalQuartz.Application.Client2/src/startup/startup.view-model.ts b/src/CrystalQuartz.Application.Client2/src/startup/startup.view-model.ts new file mode 100644 index 0000000..907e8b4 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/startup/startup.view-model.ts @@ -0,0 +1,219 @@ +import { ObservableValue } from 'john-smith/reactive'; +import { ApplicationViewModel } from '../application.view-model'; +import { RetryTimer } from '../global/timers/retry-timer'; +import { ApplicationModel } from '../application-model'; +import { EnvironmentData, SchedulerData, SchedulerStatus } from '../api'; +import { CommandService, ErrorInfo } from '../services'; +import { INotificationService } from '../notification/notification-service'; +import { DataLoader } from '../data-loader'; +import { TimelineInitializer } from '../timeline/timeline-initializer'; +import { GetDataCommand, GetEnvironmentDataCommand } from '../commands/global-commands'; +import { combine } from 'john-smith/reactive/transformers/combine'; +import DateUtils from '../utils/date'; + + +export enum FaviconStatus { + Loading, + Ready, + Active, + Broken +} + +export class StartupViewModel { + statusMessage = new ObservableValue(null); + status = new ObservableValue(false); + complete = new ObservableValue<{ environmentData: EnvironmentData, timelineInitializer: TimelineInitializer } | null>(null); + favicon = new ObservableValue(null); + title = new ObservableValue(''); + failed = new ObservableValue(false); + errorMessage = new ObservableValue(null); + retryIn = new ObservableValue(null); + customStylesUrl = new ObservableValue(null); + + private _currentTimer: RetryTimer | null = null; + + private _dataLoader: DataLoader; + private _timelineInitializer: TimelineInitializer | null = null; + private _initialData: SchedulerData | null = null; + + + constructor( + private _commandService:CommandService, + private _applicationModel:ApplicationModel, + private _notificationService:INotificationService) { + + this._dataLoader = new DataLoader(this._applicationModel, this._commandService); + } + + start() { + this.initialSetup(); + this.performLoading(); + } + + onAppRendered() { + this.setupFaviconListeners(); + + const offlineAndSchedulerName = combine( + this._applicationModel.isOffline, + this._applicationModel.schedulerName, + (isOffline, schedulerName ) => ({ isOffline: isOffline as boolean, schedulerName }) + ); + + combine( + this._applicationModel.inProgressCount, + offlineAndSchedulerName, + (left, right) => { + const isOffline = right.isOffline; + const schedulerName = right.schedulerName; + const inProgressCount = left as number; + + /** + * Compose title here + */ + + if (isOffline) { + return (schedulerName ? schedulerName + ' - ' : '') + 'Disconnected since ' + DateUtils.smartDateFormat(this._applicationModel.offlineSince!); + } + + const suffix = inProgressCount == 0 ? '' : ` - ${inProgressCount} ${inProgressCount === 1 ? 'job' : 'jobs'} in progress`; + + return schedulerName + suffix; + } + ).listen(composedTitle => this.title.setValue(composedTitle)); + } + + private initialSetup() { + this.favicon.setValue(FaviconStatus.Loading); + this.title.setValue('Loading...'); + } + + private setupFaviconListeners() { + this._applicationModel.isOffline.listen(isOffline => { + if (isOffline) { + this.favicon.setValue(FaviconStatus.Broken); + } + }) + + const syncFaviconWithSchedulerData = (data: SchedulerData | null) => { + if (data) { + const schedulerStatus = SchedulerStatus.findByCode(data.Status); + + if (schedulerStatus === SchedulerStatus.Started) { + this.favicon.setValue(FaviconStatus.Active); + } else { + this.favicon.setValue(FaviconStatus.Ready); + } + } + }; + + this._applicationModel.onDataChanged.listen(syncFaviconWithSchedulerData); + syncFaviconWithSchedulerData(this._initialData); + } + + private performLoading() { + const + stepEnvironment = this.wrapWithRetry( + () => { + this.statusMessage.setValue('Loading environment settings'); + return this._commandService.executeCommand(new GetEnvironmentDataCommand()); + }), + + stepData = stepEnvironment.then( + (envData: EnvironmentData) => this.wrapWithRetry( + () => { + let timelineInitializer = new TimelineInitializer(envData.TimelineSpan); + /** + * We need to initialize the timeline before first call + * to getData method to handle event from this call. + */ + this._timelineInitializer = timelineInitializer; + this._timelineInitializer.start(this._commandService.onEvent); + + if (envData.CustomCssUrl) { + this.statusMessage.setValue('Loading custom styles'); + this.customStylesUrl.setValue((envData.CustomCssUrl)); + } + + this.statusMessage.setValue('Loading initial scheduler data'); + + return this._commandService.executeCommand(new GetDataCommand()).then(schedulerData => { + this.statusMessage.setValue('Done'); + + return { + envData: envData, + schedulerData: schedulerData, + timelineInitializer: timelineInitializer + }; + }); + } + )); + + stepData.then(data => { + // this.applicationViewModel = new ApplicationViewModel( + // this._applicationModel, + // this._commandService, + // data.envData, + // this._notificationService, + // this._timelineInitializer); + + this._initialData = data.schedulerData; + + /** + * That would trigger application services. + */ + this.complete.setValue({ environmentData: data.envData, timelineInitializer: data.timelineInitializer }); + + this._applicationModel.setData(data.schedulerData); + this.status.setValue(true); + + this.onAppRendered(); + }); + } + + private wrapWithRetry(payload: () => Promise) : Promise { + const + errorHandler = (error: ErrorInfo) => { + this.failed.setValue(true); + this.errorMessage.setValue(error.errorMessage); + }, + + actualPayload = (isRetry: boolean) => { + this.failed.setValue(false); + + if (isRetry) { + this.statusMessage.setValue('Retry...'); + } + + return payload(); + }, + + timer = new RetryTimer(actualPayload, 5, 60, errorHandler), + + disposables = [ + timer.message.listen(message => this.retryIn.setValue(message)), + timer + ]; + + this._currentTimer = timer; + + return timer + .start(false) + .finally(() => { + disposables.forEach(x => x.dispose()); + }); + } + + cancelAutoRetry() { + if (this._currentTimer) { + this._currentTimer.reset(); + } + + this.retryIn.setValue('canceled'); + } + + retryNow() { + if (this._currentTimer) { + this._currentTimer.force(); + } + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/startup/startup.view.scss b/src/CrystalQuartz.Application.Client2/src/startup/startup.view.scss new file mode 100644 index 0000000..3671356 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/startup/startup.view.scss @@ -0,0 +1,185 @@ +.app-loading-container, +.app-loading-error-container { + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + position: fixed; + left: 0; + width: 100%; + top: 50%; + transition: opacity 1s ease, width 0.5s ease; +} + +.app-loading-container { + height: 70px; + margin-top: -35px; + z-index: 9500; + + main { + width: 300px; + height: 70px; + transition: width 0.5s ease, opacity 0.3s ease; + border-radius: 35px; + overflow: hidden; + opacity: 1; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); + } + + ul, + li { + list-style-type: none; + list-style-image: none; + height: 26px; + overflow: hidden; + white-space: nowrap; + } + + &--collapsed { + main { + width: 70px; + opacity: 0; + } + + .app-loading-status { + display: none; + } + } +} + +.app-loading-container main, +.app-loading-error-container main { + margin: 0 auto; + border: 5px solid #DDDDDD; + background: #FFFFFF; +} + +.app-loading-status { + padding-left: 60px; +} + +.app-loading-container div { + +} + +.app-loading-status h1 { + color: #444444; + margin: 9px 0 0 0; + padding: 0; + height: 25px; + line-height: 25px; + font-size: 18px; + font-weight: normal; +} + +.app-loading-status li { + color: #666666; + font-size: 12px; + font-weight: normal; + transition: height 0.5s ease, opacity 0.5s ease; +} + +.app-loading-status li.sliding { + height: 0; + opacity: 0; +} + +.app-loading-container .logo { + float: left; + margin-top: 20px; + margin-left: 20px; + animation: rotate 1s infinite ease-in; +} + +.app-loading-container .logo span { + float: left; + width: 10px; + height: 20px; +} + +.app-loading-container .logo .logo-1 { + background: #38C049; + border-radius: 10px 0 0 10px; +} + +.app-loading-container .logo .logo-2 { + background: #E5D45B; + border-radius: 0 10px 10px 0; +} + +@keyframes rotate { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +.app-loading-error-container { + height: 300px; + margin-top: -150px; + z-index: 9400; +} +.app-loading-error-container div { + position: relative; + width: 600px; + height: 300px; +} + +.app-loading-error-container header { + position: absolute; + top: 0; + right: 0; + left: 0; + border-bottom: 1px solid #DDDDDD; + + height: 40px; +} + +.app-loading-error-container footer { + position: absolute; + bottom: 0; + right: 0; + left: 0; + border-top: 1px solid #DDDDDD; + + height: 40px; + padding: 0 15px; + line-height: 40px; +} + +.app-loading-error-container footer a { + float: right; +} + +.app-loading-error-container h1 { + color: #444444; + height: 40px; + line-height: 40px; + font-size: 18px; + font-weight: normal; + margin: 0; + padding: 0 15px; +} + +.app-loading-error-container section { + position: absolute; + top: 40px; + bottom: 40px; + left: 0; + right: 0; +} + +.app-loading-error-container textarea { + padding: 15px; + box-sizing: border-box; + width: 100%; + height: 100%; + border: none; + resize: none; + font-size: 12px; + color: #993333; +} + +.app-loading-error-container section pre { + margin: 15px; +} diff --git a/src/CrystalQuartz.Application.Client2/src/startup/startup.view.tsx b/src/CrystalQuartz.Application.Client2/src/startup/startup.view.tsx new file mode 100644 index 0000000..fda6cce --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/startup/startup.view.tsx @@ -0,0 +1,114 @@ +import { DomElement, HtmlDefinition, View } from 'john-smith/view'; +import { StartupViewModel } from './startup.view-model'; +import { OnInit, OnUnrender } from 'john-smith/view/hooks'; +import { OptionalDisposables } from 'john-smith/common'; +import { DomEngine } from 'john-smith/view/dom-engine'; +import { ObservableList, ObservableValue } from 'john-smith/reactive'; +import { List } from 'john-smith/view/components'; +import { Timer } from '../global/timers/timer'; +import 'john-smith/view/jsx'; + +class MessageView implements View, OnUnrender { + constructor(private readonly viewModel: string) { + } + + public template(): HtmlDefinition { + return
        • {this.viewModel}
        • + } + + public onUnrender(unrender: () => void, root: DomElement | null, domEngine: DomEngine): void { + if (root === null) { + unrender(); + return; + } + + root.createClassNames().add('sliding'); + setTimeout(() => { + unrender(); + }, 600); + } +} + +export class StartupView implements View, OnInit, OnUnrender { + private _collapsed = new ObservableValue(true); + + constructor(private readonly viewModel: StartupViewModel) { + } + + public onInit(root: DomElement | null, domEngine: DomEngine): OptionalDisposables { + setTimeout(() => { + this._collapsed.setValue(false); + }, 50); + } + + public onUnrender(unrender: () => void, root: DomElement | null, domEngine: DomEngine): void { + if (root === null) { + unrender(); + return; + } + + const loadingOverlay = document.getElementById('app-loading-overlay')!; + loadingOverlay.style.opacity = '0'; + + setTimeout(() => { + unrender(); + loadingOverlay.remove(); + }, 600); + } + + template(): HtmlDefinition { + const messages = new ObservableList(); + + const messageHandleTimer = new Timer(); + const messageHandler = () => { + if (messages.currentCount() === 1 && this.viewModel.status.getValue()) { + /** + * Pre-loading stage is complete. + * Application is ready for rendering. + */ + + messageHandleTimer.dispose(); + + setTimeout(() => { + // js.dom('#application').render(ApplicationView, viewModel.applicationViewModel); + // + // viewModel.onAppRendered(); + // + // this.fadeOut($loadingError.$); + // this.fadeOut($overlay); + // this.fadeOut($root); + }, 10); + } else { + if (messages.currentCount() > 1) { + messages.remove(messages.getValue()[0]); + } + + messageHandleTimer.schedule(messageHandler, 600); + } + }; + + this.viewModel.statusMessage.listen(message => { + if (message !== null) { + messages.add(message); + } + }); + + messageHandler(); + + return
          +
          + + +
          +

          Loading...

          +
            + +
          +
          +
          +
          + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/timeline/common.ts b/src/CrystalQuartz.Application.Client2/src/timeline/common.ts new file mode 100644 index 0000000..86314f5 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/timeline/common.ts @@ -0,0 +1,45 @@ +import {ErrorMessage} from "../api"; + +export enum ActivityInteractionRequest { + ShowTooltip, + HideTooltip, + ShowDetails +} + +export interface IActivitySize { + left: number; + width: number; +} + +export interface IRange { + start: number; + end: number; +} + +export interface ITimelineTickItem { + tickDate: number; + width: number; +} + +export interface ITimelineActivityOptions { + key: string | null; + startedAt?: number; + completedAt?: number; +} + +export interface ITimelineGlobalActivityOptions { + occurredAt: number; + itemKey: string; + scope: number; + typeCode: string; +} + +export interface ITimelineSlotOptions { + key: string; +} + +export interface TimelineActivityCompletionOptions { + faulted: boolean; + errors: ErrorMessage[]; +} + diff --git a/src/CrystalQuartz.Application.Client2/src/timeline/constants.scss b/src/CrystalQuartz.Application.Client2/src/timeline/constants.scss new file mode 100644 index 0000000..0ffe6be --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/timeline/constants.scss @@ -0,0 +1,3 @@ +$timeline-global-pick-height: 6px; +$timeline-global-pick-width: 6px; +$timeline-body-width: 1px; diff --git a/src/CrystalQuartz.Application.Client2/src/timeline/index.scss b/src/CrystalQuartz.Application.Client2/src/timeline/index.scss new file mode 100644 index 0000000..e600c20 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/timeline/index.scss @@ -0,0 +1,226 @@ +@import "tooltips.scss"; +@import "constants"; + +/* Content -> Timeline */ + +ul.timeline-captions, +ul.timeline-captions li { + list-style-type: none; + list-style-image: none; +} + +.timeline-captions { + position: absolute; + left: 0; + top: 0; + bottom: 0; + margin: 0; + padding: 0; +} + +.timeline-tick { + float: left; + width: 16.6667%; + height: $main-header-secondary-height - 1; + color: #333333; + text-align: center; + font-size: 12px; + line-height: $main-header-secondary-height; + position: relative; + transition: color 1s ease; +} + +.timeline-tick:first-child { + color: #AAAAAA; +} + +.timeline-tick:hover { + background: #E9E9E9; +} + +.timeline-tick span::before { + content: ''; + display: block; + position: absolute; + height: 3px; + width: 50%; + left: 0; + bottom: 0; + border-right: 1px solid #AAAAAA; +} + +.timeline-grid { + position: absolute; + left: 50%; + top: 0; + bottom: 0; + right: 0; +} + +.timeline-grid-item { + float: left; + position: relative; + width: 16.6667%; + height: 100%; + + .major, .minor { + border-right: 1px dashed #DDDDDD; + position: absolute; + top: 0; + bottom: 0; + } + + .major { + right: 0; + left: 50%; + } + + .minor { + left: 0; + right: 50%; + } + + &.active { + background: $active-light; + + .minor { + border-right: 1px dashed $active; + } + } +} + +.timeline-grid-item div { + float: left; + height: 100%; + width: 50%; + border-right: 1px dashed #DDDDDD; +} + +.timeline-item { + position: absolute; + top: 3px; + height: 14px; + background: $bg-activity-normal; + min-width: 2px; + cursor: pointer; + + &:hover { + background: $bg-activity-normal-hover; + } + + &.faulted { + background: $bg-activity-error; + } + + &.faulted:hover { + background: $bg-activity-error-hover; + } +} + +.timeline-global-item { + position: absolute; + width: $timeline-body-width; + z-index: 10; + + /* + .timeline-tooltip { + top: 0; + left: $timeline-global-pick-width + $timeline-global-pick-height + $timeline-global-pick-height / 2; + }*/ + + .timeline-marker-title { + display: none; + } + + .timeline-marker-pick { + position: absolute; + top: ($data-row-height - $timeline-global-pick-height) / 2; + left: 0; + width: $timeline-global-pick-width; + height: $timeline-global-pick-height; + } + + .timeline-marker-arrow { + content: ''; + display: block; + width: 0; + height: 0; + border-top: $timeline-global-pick-width / 2 solid transparent; + border-bottom: $timeline-global-pick-width / 2 solid transparent; + border-left: $timeline-global-pick-height / 2 solid darken(#38C049, 10%); + position: absolute; + top: ($data-row-height - $timeline-global-pick-height) / 2; + left: $timeline-global-pick-width; + } + + .timeline-marker-body { + position: absolute; + top: ($data-row-height + $timeline-global-pick-height) / 2; + bottom: 0; + left: 0; + right: 0; + } + + .timeline-marker-pick, + .timeline-marker-body { + background: darken(#38C049, 10%); + } +} + +.timeline-global-item.paused, +.timeline-global-item.standby, +.timeline-global-item.shutdown { + .timeline-tooltip { + left: auto; + right: -5px - $timeline-global-pick-width - $timeline-global-pick-height / 2; + } + + .timeline-marker-pick { + right: 0; + left: auto; + } + + .timeline-marker-arrow { + border-right: $timeline-global-pick-height / 2 solid darken(#E5D45B, 10%); + border-left: none; + left: auto; + right: $timeline-global-pick-width; + } + + .timeline-marker-pick, + .timeline-marker-body { + background: darken(#E5D45B, 10%); + } +} + +.timeline-global-item.shutdown { + .timeline-marker-arrow { + border-right-color: #933; + } + + .timeline-marker-pick, + .timeline-marker-body { + background: #933; + } +} + +.timeline-global-item.standby { + .timeline-marker-arrow { + border-right-color: #DDDDDD; + } + + .timeline-marker-pick, + .timeline-marker-body { + background: #DDDDDD; + } +} + +/* Back layer (tooltips, global objects) */ + +.timeline-back-layer { + position: absolute; + top: 0; + bottom: 0; + right: 0; + width: 50%; +} diff --git a/src/CrystalQuartz.Application.Client2/src/timeline/index_sm.less b/src/CrystalQuartz.Application.Client2/src/timeline/index_sm.less new file mode 100644 index 0000000..84593a6 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/timeline/index_sm.less @@ -0,0 +1,37 @@ +.timeline-grid { + left: 0; + + .timeline-grid-item { + border: none; + + div { + border: none; + } + } +} + +.timeline-data { + width: 100%; + float: none; + border-bottom-style: solid; + height: @timeline-row-height-sm; +} + +.timeline-data-filler { + display: none; +} + +.ticks-container { + left: 0; +} + +.timeline-item { + height: 4px; + background: lighten(@green-dark, 20%); +} + +/* Back layer (tooltips, global objects) */ + +.timeline-back-layer { + display: none; +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/timeline/timeline-activity-view-model.ts b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-activity-view-model.ts new file mode 100644 index 0000000..e643f2b --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-activity-view-model.ts @@ -0,0 +1,66 @@ +import { Duration } from '../global/duration'; +import TimelineActivity from './timeline-activity'; +import { ErrorMessage, NullableDate } from '../api'; +import { ActivityState } from '../global/activities/activity-state'; +import { Disposable } from 'john-smith/common'; +import { ObservableValue } from 'john-smith/reactive'; + +/** + * A view model for timeline activity to share + * some logic between tooltip and details dialog. + */ +export class TimelineActivityViewModel implements Disposable { + private _disposables: Disposable[]; + + duration: Duration; + startedAt: NullableDate; + completedAt: ObservableValue; + status: ObservableValue; + errors: ObservableValue; + + constructor(private activity: TimelineActivity) { + this.duration = new Duration(activity.startedAt, activity.completedAt); + this.startedAt = new NullableDate(activity.startedAt ?? null); + this.completedAt = new ObservableValue(new NullableDate(activity.completedAt ?? null)); + this.status = new ObservableValue(null); + this.errors = new ObservableValue(null); + + this.refreshStatus(activity); + + this._disposables = [ + this.duration, + activity.completed.listen(() => { + this.duration.setEndDate(activity.completedAt!); + this.completedAt.setValue(new NullableDate(activity.completedAt ?? null)); + this.errors.setValue(activity.errors); + this.refreshStatus(activity); + }) + ]; + } + + init() { + this.duration.init(); + this.completedAt.setValue(new NullableDate(this.activity.completedAt ?? null)); + this.errors.setValue(this.activity.errors); + } + + dispose() { + this._disposables.forEach(x => x.dispose()); + } + + private refreshStatus(activity: TimelineActivity) { + this.status.setValue(this.calculateStatus(activity)); + } + + private calculateStatus(activity: TimelineActivity) { + if (!activity.completedAt) { + return ActivityState.InProgress; + } + + if (activity.faulted) { + return ActivityState.Failure; + } + + return ActivityState.Success; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/timeline/timeline-activity.ts b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-activity.ts new file mode 100644 index 0000000..5a81e09 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-activity.ts @@ -0,0 +1,70 @@ +import { + IActivitySize, + ITimelineActivityOptions, + TimelineActivityCompletionOptions, + ActivityInteractionRequest +} from './common'; +import { Event } from 'john-smith/reactive/event'; + +import { ErrorMessage } from '../api'; +import { ObservableValue } from 'john-smith/reactive'; + +export default class TimelineActivity { + position = new ObservableValue(null); + completed = new Event(); + + key: string | null; + startedAt: number | undefined; + completedAt: number | undefined; + + faulted: boolean = false; + errors: ErrorMessage[] | null = null; + + constructor(private options: ITimelineActivityOptions, private requestSelectionCallback: (requestType: ActivityInteractionRequest) => void) { + this.key = options.key; + + this.startedAt = options.startedAt; + this.completedAt = options.completedAt; + } + + complete(date: number, options: TimelineActivityCompletionOptions) { + this.completedAt = date; + this.errors = options.errors; + this.faulted = options.faulted; + this.completed.trigger(null); + }; + + recalculate(rangeStart: number, rangeEnd: number) { + const rangeWidth = rangeEnd - rangeStart, + + activityStart = this.startedAt!, + activityComplete = this.completedAt || rangeEnd, + + isOutOfViewport = activityStart <= rangeStart && activityComplete <= rangeStart; + + if (isOutOfViewport) { + return false; + } + + const viewPortActivityStart = activityStart < rangeStart ? rangeStart : activityStart; + + this.position.setValue({ + left: 100 * (viewPortActivityStart - rangeStart) / rangeWidth, + width: 100 * (activityComplete - viewPortActivityStart) / rangeWidth + }); + + return true; + }; + + requestSelection() { + this.requestSelectionCallback(ActivityInteractionRequest.ShowTooltip); + } + + requestDeselection() { + this.requestSelectionCallback(ActivityInteractionRequest.HideTooltip); + } + + requestDetails() { + this.requestSelectionCallback(ActivityInteractionRequest.ShowDetails); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/timeline/timeline-backlayer.ts b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-backlayer.ts new file mode 100644 index 0000000..b7ae122 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-backlayer.ts @@ -0,0 +1,3 @@ +export default class TimelineBacklayer { + constructor() {} +} \ No newline at end of file diff --git a/src/CrystalQuartz.Application.Client2/src/timeline/timeline-captions-view.tsx b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-captions-view.tsx new file mode 100644 index 0000000..f26cddc --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-captions-view.tsx @@ -0,0 +1,25 @@ +import { List } from 'john-smith/view/components'; +import Timeline from './timeline'; +import TimelineTickView from './timeline-tick-view'; +import { View } from 'john-smith/view'; +import { map } from 'john-smith/reactive/transformers/map'; + +export default class TimelineCaptionsView implements View { + + constructor( + private timeline: Timeline + ) { + } + + template() { + const width = (100 + 100 * this.timeline.ticks.millisecondsPerTick / this.timeline.timelineSizeMilliseconds) + '%'; + const styles = map( + this.timeline.ticks.shift, + shiftPercent => `width: ${width}; left: ${-shiftPercent}%;` + ); + + return
            + +
          ; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/timeline/timeline-global-activity-view.tsx b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-global-activity-view.tsx new file mode 100644 index 0000000..e385f8d --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-global-activity-view.tsx @@ -0,0 +1,61 @@ +import TimelineSlot from './timeline-slot'; +import { + IActivityVerticalPosition, + TimelineGlobalActivity +} from './timeline-global-activity'; +import { View } from 'john-smith/view'; + +export default class TimelineGlobalActivityView implements View { + + constructor( + private readonly activity: TimelineGlobalActivity + ) { + } + + template = () => +
          + + + +
          ; + + // init(dom: js.IDom, activity: TimelineGlobalActivity) { + // const $root = dom.root.$; + // + // dom.$.addClass(activity.typeCode); + // + // dom('.js_tooltip_trigger, .js_tooltip').on('mouseenter').react(() => { + // activity.requestSelection(); + // }); + // + // dom('.js_tooltip_trigger, .js_tooltip').on('mouseleave').react(() => { + // activity.requestDeselection(); + // }); + // + // dom.manager.manage( + // activity.position.listen( + // position => { + // if (!position) { + // return; + // } + // + // $root.css('left', position.left + '%'); + // } + // ) + // ); + // + // dom.manager.manage( + // activity.verticalPosition.listen( + // position => { + // if (!position) { + // return; + // } + // + // $root + // .css('top', (position.top * 20) + 'px') + // .css('height', (position.height * 20) + 'px'); + // } + // ) + // ); + // }; +}; diff --git a/src/CrystalQuartz.Application.Client2/src/timeline/timeline-global-activity.ts b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-global-activity.ts new file mode 100644 index 0000000..e62ccde --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-global-activity.ts @@ -0,0 +1,30 @@ +import TimelineActivity from './timeline-activity'; +import { ITimelineGlobalActivityOptions, ActivityInteractionRequest } from './common'; +import { ObservableValue } from 'john-smith/reactive'; + +export interface IActivityVerticalPosition { + top: number; + height: number; +} + +export class TimelineGlobalActivity extends TimelineActivity { + verticalPosition = new ObservableValue(null); + + constructor( + private globalOptions: ITimelineGlobalActivityOptions, + requestSelectionCallback: (requestType: ActivityInteractionRequest) => void) { + + super({ startedAt: globalOptions.occurredAt, completedAt: globalOptions.occurredAt, key: null }, requestSelectionCallback); + } + + get typeCode() { return this.globalOptions.typeCode; } + get scope() { return this.globalOptions.scope; } + get itemKey() { return this.globalOptions.itemKey; } + + updateVerticalPostion(top: number, height: number) { + this.verticalPosition.setValue({ + top: top, + height: height + }); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/timeline/timeline-initializer.ts b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-initializer.ts new file mode 100644 index 0000000..79708d5 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-initializer.ts @@ -0,0 +1,77 @@ +import Timeline from "./timeline"; +// import {SchedulerEvent, SchedulerEventScope, SchedulerEventType} from "../api"; +// import GlobalActivitiesSynchronizer from "../global-activities-synchronizer"; + +import { SchedulerEvent, SchedulerEventScope, SchedulerEventType } from '../api'; +import { Event } from 'john-smith/reactive/event'; +import GlobalActivitiesSynchronizer from '../global-activities-synchronizer'; + +export class TimelineInitializer { + timeline: Timeline; + globalActivitiesSynchronizer: GlobalActivitiesSynchronizer; + + constructor(timelineSizeMilliseconds: number) { + this.timeline = new Timeline(timelineSizeMilliseconds); + this.globalActivitiesSynchronizer = new GlobalActivitiesSynchronizer(this.timeline); + } + + start(eventsSource: Event) { + this.timeline.init(); + eventsSource.listen(event => this.handleEvent(event)); + } + + private handleEvent(event: SchedulerEvent) { + const + scope = event.scope, + eventType = event.eventType, + isGlobal = !(scope === SchedulerEventScope.Trigger && (eventType === SchedulerEventType.Fired || eventType === SchedulerEventType.Complete)); + + if (isGlobal) { + const + typeCode = SchedulerEventType[eventType].toLowerCase(), + options = { + occurredAt: event.date, + typeCode: typeCode, + itemKey: this.globalActivitiesSynchronizer.makeSlotKey(scope, event.itemKey), + scope: scope + }, + globalActivity = this.timeline.addGlobalActivity(options); + + this.globalActivitiesSynchronizer.updateActivity(globalActivity); + } else { + const + slotKey = this.globalActivitiesSynchronizer.makeSlotKey(scope, event.itemKey), + activityKey = event.fireInstanceId; + + if (eventType === SchedulerEventType.Fired) { + const + slot = this.timeline.findSlotBy(slotKey) || this.timeline.addSlot({ key: slotKey }), + existingActivity = slot.findActivityBy(activityKey); + + if (!existingActivity) { + this.timeline.addActivity( + slot, + { + key: activityKey, + startedAt: event.date + }); + } + } else if (eventType === SchedulerEventType.Complete) { + const + completeSlot = this.timeline.findSlotBy(slotKey), + activity = !!completeSlot ? + completeSlot.findActivityBy(activityKey) : + (this.timeline.preservedActivity && this.timeline.preservedActivity.key === activityKey ? this.timeline.preservedActivity : null); + + if (activity) { + activity.complete( + event.date, + { + faulted: event.faulted, + errors: event.errors + }); + } + } + } + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/timeline/timeline-slot-view.tsx b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-slot-view.tsx new file mode 100644 index 0000000..4b4c435 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-slot-view.tsx @@ -0,0 +1,42 @@ +import TimelineSlot from './timeline-slot'; +import TimelineActivity from './timeline-activity'; +import { List } from 'john-smith/view/components'; +import { View } from 'john-smith/view'; +import { map } from 'john-smith/reactive/transformers/map'; + +class TimelineActivityView implements View { + + constructor(private readonly activity: TimelineActivity) { + } + + template() { + const style = map( + this.activity.position, + position => { + if (position === null) { + return ''; + } + + return 'left: ' + position.left + '%; width: ' + position.width + '%;'; + } + ) + return
          this.activity.faulted) + }} + style={style} + _mouseenter={this.activity.requestSelection} + _mouseleave={this.activity.requestDeselection} + _click={this.activity.requestDetails} + >
          ; + } +} + +export const TimelineSlotView = (slot: TimelineSlot) => +
          +
          + +
          +
          ; diff --git a/src/CrystalQuartz.Application.Client2/src/timeline/timeline-slot.ts b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-slot.ts new file mode 100644 index 0000000..d8206e4 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-slot.ts @@ -0,0 +1,96 @@ +import { + IRange, + ITimelineSlotOptions, + ITimelineActivityOptions, + ActivityInteractionRequest +} from './common'; + +import TimelineActivity from './timeline-activity'; +import { ObservableList } from 'john-smith/reactive'; + +export default class TimelineSlot { + activities = new ObservableList(); + + key: string; + + constructor(options:ITimelineSlotOptions) { + this.key = options.key; + } + + add(activity:ITimelineActivityOptions, selectionRequestCallback: (activity: TimelineActivity, requestType: ActivityInteractionRequest) => void) { + const result: TimelineActivity = new TimelineActivity(activity, requestType => selectionRequestCallback(result, requestType)); + this.activities.add(result); + + return result; + }; + + remove(activity: TimelineActivity) { + this.activities.remove(activity); + }; + + /** + * Removes all activities from the slot + */ + clear() { + this.activities.clear(); + } + + isEmpty() { + return this.activities.getValue().length === 0; + }; + + isBusy() { + return !!this.findCurrentActivity(); + } + + recalculate(range: IRange) : { isEmpty: boolean, removedActivities: TimelineActivity[] } { + const activities = this.activities.getValue(), + rangeStart = range.start, + rangeEnd = range.end, + removed = []; + + for (let i = 0; i < activities.length; i++) { + const activity = activities[i]; + + if (!activity.recalculate(rangeStart, rangeEnd)) { + this.activities.remove(activity); + removed.push(activity); + } + } + + return { + isEmpty: this.isEmpty(), + removedActivities: removed + }; + } + + findActivityBy(key: string) { + const activities = this.activities.getValue(); + for (let i = 0; i < activities.length; i++) { + if (activities[i].key === key) { + return activities[i]; + } + } + + return null; + } + + requestCurrentActivityDetails() { + const currentActivity = this.findCurrentActivity(); + if (currentActivity) { + currentActivity.requestDetails(); + } + } + + private findCurrentActivity(): TimelineActivity | null { + const activities = this.activities.getValue(); + + for (var i = activities.length - 1; i >= 0; i--) { + if (!activities[i].completedAt) { + return activities[i]; + } + } + + return null; + } +}; diff --git a/src/CrystalQuartz.Application.Client2/src/timeline/timeline-tick-view.tsx b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-tick-view.tsx new file mode 100644 index 0000000..5052873 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-tick-view.tsx @@ -0,0 +1,21 @@ +import { ITimelineTickItem } from './common'; +import DateUtils from '../utils/date'; +import { View } from 'john-smith/view'; + +export default class TimelineTickView implements View { + + constructor( + private readonly viewModel: ITimelineTickItem + ) { + } + + template() { + return
        • + {this.formatDate(new Date(this.viewModel.tickDate))} +
        • ; + } + + private formatDate(date: number | Date) { + return DateUtils.timeFormat(date); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/timeline/timeline-ticks.ts b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-ticks.ts new file mode 100644 index 0000000..43d68bc --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-ticks.ts @@ -0,0 +1,86 @@ +import { ITimelineTickItem } from './common'; +import { ObservableList, ObservableValue } from 'john-smith/reactive'; + +export default class TimelineTicks { + items = new ObservableList(); + shift = new ObservableValue(0); + + millisecondsPerTick: number; + tickWidthPercent: number; + + constructor( + private ticksCount: number, + private timelineSizeMilliseconds: number) { + + this.millisecondsPerTick = timelineSizeMilliseconds / (ticksCount); + this.tickWidthPercent = 100 / (ticksCount + 1); + } + + init() { + var now = Math.ceil(new Date().getTime() / this.millisecondsPerTick) * this.millisecondsPerTick, + items = []; + + for (var i = 0; i < this.ticksCount + 1; i++) { + var tickDate = now - this.millisecondsPerTick * (this.ticksCount - i + 1); + + items.push({ + tickDate: tickDate, + width: this.tickWidthPercent + }); + } + + this.items.setValue(items); + this.calculateShift(now); + }; + + update(start: number, end: number) { + var currentItems = this.items.getValue(); + if (!currentItems) { + return; + } + + this.removeOutdatedTicks(start, currentItems); + + if (this.items.getValue().length === 0) { + this.init(); + } else { + var edgeTick = this.items.getValue()[this.items.getValue().length - 1]; + + while (edgeTick.tickDate + this.millisecondsPerTick / 2 < end) { + var newTick = { + tickDate: edgeTick.tickDate + this.millisecondsPerTick, + width: this.tickWidthPercent + }; + + this.items.add(newTick); + + edgeTick = newTick; + } + } + + this.calculateShift(new Date().getTime()); + }; + + private getEdgeTick() { + return this.items.getValue()[this.items.getValue().length - 1]; + } + + private calculateShift(endDate: number) { + var edgeTick = this.getEdgeTick(), + shiftMilliseconds = endDate - edgeTick.tickDate + this.millisecondsPerTick / 2, + shiftPercent = 100 * shiftMilliseconds / this.timelineSizeMilliseconds; + + this.shift.setValue(shiftPercent); + }; + + private removeOutdatedTicks(startDate: number, items: ITimelineTickItem[]) { + for (var i = 0; i < items.length; i++) { + var item = items[i]; + if (item.tickDate + this.millisecondsPerTick / 2 < startDate) { + this.items.remove(item); + } else { + return; + } + } + } +}; diff --git a/src/CrystalQuartz.Application.Client2/src/timeline/timeline-tooltips-view.tsx b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-tooltips-view.tsx new file mode 100644 index 0000000..c461d83 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/timeline/timeline-tooltips-view.tsx @@ -0,0 +1,378 @@ +import { Activity, NullableDate, SchedulerEventScope } from '../api'; + +import Timeline, { ISelectedActivityData } from './timeline'; +import { TimelineGlobalActivity } from './timeline-global-activity'; +import TimelineActivity from './timeline-activity'; +import GlobalActivitiesSynchronizer from '../global-activities-synchronizer'; +import { NullableDateView } from '../main/main-content/nullable-date-view'; +import {TimelineActivityViewModel} from "./timeline-activity-view-model"; +import {ActivityStateView} from "../global/activities/activity-state-view"; +import { DomElement, HtmlDefinition, View } from 'john-smith/view'; +import { Disposable, OptionalDisposables } from 'john-smith/common'; +import { Value } from 'john-smith/view/components'; +import { map } from 'john-smith/reactive/transformers/map'; +import { OnInit } from 'john-smith/view/hooks'; +import { DomEngine } from 'john-smith/view/dom-engine'; +import TimelineSlot from './timeline-slot'; +import { Listenable } from 'john-smith/reactive'; +import { ActivityStatusView } from '../main/main-content/activity-status-view'; + +class RenderedTooltip implements Disposable { + constructor( + public $root: any, // todo + private positionListener: Disposable, + private renderedView: Disposable + ) { + } + + dispose() { + this.$root.find('.js_actual_content').removeClass('js_actual_content'); + this.$root.addClass('closing'); + + setTimeout(() => { + this.$root.remove(); + this.positionListener.dispose(); + this.renderedView.dispose(); + }, 1000); + } +} + +export class LocalTooltipView implements View, OnInit { + private _activityViewModel: TimelineActivityViewModel; + + constructor( + private readonly viewModel: { + activity: TimelineActivity, + slot: TimelineSlot, + globalActivitiesSynchronizer: GlobalActivitiesSynchronizer, + containerWidthCalculator: () => number + }) { + this._activityViewModel = new TimelineActivityViewModel(viewModel.activity); + } + + template(): HtmlDefinition { + const activity = this.viewModel.activity; + + const + localTooltipWidth = 300, + localTooltipWidthHalf = localTooltipWidth / 2, + localTooltipPickMargin = 6, + localTooltipMinLeftArrowMargin = 6; + + let classes: Record = { 'local': true }; + let styles = 'bottom: ' + (this.viewModel.globalActivitiesSynchronizer.getSlotIndex(this.viewModel.slot, true)! * 20) + 'px'; + + const transformedPosition: Listenable<[number, number]> = map(this.viewModel.activity.position, p => { + if (p === null) { + return [0, 0]; + } + + const + containerWidth = this.viewModel.containerWidthCalculator(), + tooltipPointerOriginPercent = p.left + p.width / 2, + tooltipOrigin = containerWidth * tooltipPointerOriginPercent / 100 - localTooltipPickMargin; + + let contentLeft: number; + + if (tooltipOrigin < localTooltipWidthHalf) { + contentLeft = tooltipOrigin >= localTooltipMinLeftArrowMargin ? -tooltipOrigin : -localTooltipMinLeftArrowMargin; + } else if (tooltipOrigin + localTooltipWidthHalf > containerWidth) { + contentLeft = -(localTooltipWidth - (containerWidth - tooltipOrigin)); + } else { + contentLeft = -localTooltipWidthHalf; + } + + return [tooltipPointerOriginPercent, contentLeft]; + }); + + return
          styles + '; left: ' + p[0] + '%')}> +
          +
          'left: ' + p[1] + 'px')}> + + + + + + + + + + + + + + +
          + + Trigger fired at + +
          Trigger completed at + +
          Duration + {this._activityViewModel.duration.value} + {this._activityViewModel.duration.measurementUnit} +
          +
          +
          ; + } + + /** @inheritdoc */ + onInit(root: DomElement | null, domEngine: DomEngine): OptionalDisposables { + this._activityViewModel.init(); + + setTimeout(() => { + if (root !== null) { + root.createClassNames().add('visible'); + } + }, 100); + + return this._activityViewModel; + } +} + +export class TooltipView implements View, OnInit { + constructor( + private readonly viewModel: { + data: ISelectedActivityData, + globalActivitiesSynchronizer: GlobalActivitiesSynchronizer, + containerWidthCalculator: () => number + } + ) { + } + + template() { + const activity = this.viewModel.data.activity, + isGlobal = this.viewModel.data.slot === null; + + const + localTooltipWidth = 300, + localTooltipWidthHalf = localTooltipWidth / 2, + localTooltipPickMargin = 6, + localTooltipMinLeftArrowMargin = 6; + + let classes: Record = { 'local': !isGlobal, 'global': isGlobal }; + let styles = ''; + + if (isGlobal) { + const globalActivity = activity as TimelineGlobalActivity; + classes[globalActivity.typeCode] = true; + + styles = 'top: ' + (globalActivity.verticalPosition.getValue()!.top * 20) + 'px'; + } else { + styles = 'bottom: ' + (this.viewModel.globalActivitiesSynchronizer.getSlotIndex(this.viewModel.data.slot, true)! * 20) + 'px'; + } + + let actualStyles = map(this.viewModel.data.activity.position, p => { + if (p === null) { + return undefined; + } + + if (isGlobal) { + return styles; + } + + const + containerWidth = this.viewModel.containerWidthCalculator(), + tooltipPointerOriginPercent = p.left + p.width / 2, + tooltipOrigin = containerWidth * tooltipPointerOriginPercent / 100 - localTooltipPickMargin; + + let contentLeft: number; + + if (tooltipOrigin < localTooltipWidthHalf) { + contentLeft = tooltipOrigin >= localTooltipMinLeftArrowMargin ? -tooltipOrigin : -localTooltipMinLeftArrowMargin; + } else if (tooltipOrigin + localTooltipWidthHalf > containerWidth) { + contentLeft = -(localTooltipWidth - (containerWidth - tooltipOrigin)); + } else { + contentLeft = -localTooltipWidthHalf; + } + + //$currentTooltipContent.css('left', contentLeft + 'px'); + + return styles + '; left: ' + tooltipPointerOriginPercent + '%'; + }) + + return
          +
          +
          content goes here
          +
          ; + } + + onInit(root: DomElement | null, domEngine: DomEngine): OptionalDisposables { + setTimeout(() => { + if (root !== null) { + root.createClassNames().add('visible'); + } + }, 100); + } +} + +export class TimelineTooltipsView implements View { + constructor(private viewModel: { globalActivitiesSynchronizer: GlobalActivitiesSynchronizer, timeline: Timeline, containerWidthCalculator: () => number }) { + } + + template(): HtmlDefinition { + return { + if (selectedActivity.slot === null) { + return todo; + } + + return ; + }} model={this.viewModel.timeline.selectedActivity}> + } + + // render(dom: js.IListenerDom, timeline: Timeline) { + // var currentTooltip: RenderedTooltip = null; + // + // var disposeCurrentTooltip = () => { + // if (currentTooltip) { + // currentTooltip.dispose(); + // currentTooltip = null; + // } + // }; + // + // const + // localTooltipWidth = 300, + // localTooltipWidthHalf = localTooltipWidth / 2, + // localTooltipPickMargin = 6, + // localTooltipMinLeftArrowMargin = 6; + // + // timeline.selectedActivity.listen(data => { + // disposeCurrentTooltip(); + // + // if (data) { + // const activity = data.activity, + // isGlobal = data.slot === null; + // + // var $currentTooltip = $( + // `
          + //
          + //
          + //
          `), + // $currentTooltipContent = $currentTooltip.find('.content'); + // + // if (!isGlobal) { + // $currentTooltip.addClass('local'); + // $currentTooltip.css('bottom', (this.globalActivitiesSynchronizer.getSlotIndex(data.slot, true) * 20) + 'px'); + // } else { + // const globalActivity = activity as TimelineGlobalActivity; + // + // $currentTooltip.addClass('global'); + // $currentTooltip.addClass(globalActivity.typeCode); + // $currentTooltip.css('top', (globalActivity.verticalPosition.getValue().top * 20) + 'px'); + // } + // + // var positionListener = activity.position.listen(p => { + // if (p) { + // if (isGlobal) { + // $currentTooltip.css('left', p.left + '%'); + // } else { + // const + // containerWidth = dom.$.width(), + // tooltipPointerOriginPercent = p.left + p.width / 2, + // tooltipOrigin = containerWidth * tooltipPointerOriginPercent / 100 - localTooltipPickMargin; + // + // let contentLeft: number; + // + // if (tooltipOrigin < localTooltipWidthHalf) { + // contentLeft = tooltipOrigin >= localTooltipMinLeftArrowMargin ? -tooltipOrigin : -localTooltipMinLeftArrowMargin; + // } else if (tooltipOrigin + localTooltipWidthHalf > containerWidth) { + // contentLeft = -(localTooltipWidth - (containerWidth - tooltipOrigin)); + // } else { + // contentLeft = -localTooltipWidthHalf; + // } + // + // $currentTooltipContent.css('left', contentLeft + 'px'); + // + // $currentTooltip.css('left', tooltipPointerOriginPercent + '%'); + // } + // } + // }); + // + // dom.$.append($currentTooltip); + // + // var renderedView = js.dom('.js_tooltip .js_actual_content') + // .render(isGlobal ? GlobalActivityTooltipView : TriggerActivityTooltipView, activity); + // + // currentTooltip = new RenderedTooltip($currentTooltip, positionListener, renderedView); + // + // setTimeout(() => { + // if (currentTooltip) { + // currentTooltip.$root.addClass('visible'); + // } + // }, 100); + // + // $currentTooltip.on('mouseenter', () => timeline.preserveCurrentSelection()); + // $currentTooltip.on('mouseleave', () => timeline.resetCurrentSelection()); + // } + // }); + // } + + dispose(): void { + + } +} + +export class GlobalActivityTooltipView implements View { + constructor(private readonly viewModel: TimelineGlobalActivity) { + + } + template = () =>
          +

          at

          +

          +
          ; + + // init(dom: js.IDom, viewModel: TimelineGlobalActivity) { + // dom('.js_message').observes(SchedulerEventScope[viewModel.scope] + ' ' + viewModel.typeCode); + // dom('.js_date').observes(new NullableDate(viewModel.startedAt), NullableDateView); + // } +} + +export class TriggerActivityTooltipView implements View { + constructor( + private readonly viewModel: TimelineActivity + ) { + } + + template = () => + + + + + + + + + + + + + + +
          Trigger fired at
          Trigger completed at
          Duration + + +
          ; + + // init(dom: js.IDom, viewModel: TimelineActivity): void { + // const activityViewModel = new TimelineActivityViewModel(viewModel); + // + // dom.manager.manage(activityViewModel); + // + // const duration = activityViewModel.duration; + // + // dom('.js_durationValue').observes(duration.value); + // dom('.js_durationUnit').observes(duration.measurementUnit); + // + // dom('.js_startedAt').observes(new NullableDate(viewModel.startedAt), NullableDateView); + // dom('.js_completedAt').observes(activityViewModel.completedAt, NullableDateView); + // dom('.js_state').observes(activityViewModel.status, ActivityStateView); + // + // activityViewModel.init(); + // } +} diff --git a/src/CrystalQuartz.Application.Client2/src/timeline/timeline.ts b/src/CrystalQuartz.Application.Client2/src/timeline/timeline.ts new file mode 100644 index 0000000..0e779a1 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/timeline/timeline.ts @@ -0,0 +1,204 @@ +import { + ITimelineSlotOptions, + ITimelineActivityOptions, + ITimelineGlobalActivityOptions, + ActivityInteractionRequest, + IRange +} from './common'; +import TimelineSlot from './timeline-slot'; +import TimelineTicks from './timeline-ticks'; +import TimelineActivity from './timeline-activity'; +import { TimelineGlobalActivity } from './timeline-global-activity'; +import { Timer } from "../global/timers/timer"; +import { ObservableList, ObservableValue } from 'john-smith/reactive'; +import { Event } from 'john-smith/reactive/event'; + +export interface ISelectedActivityData { + activity: TimelineActivity; + slot: TimelineSlot; +} + +export default class Timeline { + private _timeRef: any = null; + private _resetSelectionTimer = new Timer(); + + globalSlot = new TimelineSlot({ key: ' timeline_global' }); + + range = new ObservableValue(null); + slots = new ObservableList(); + ticks = new TimelineTicks(10, this.timelineSizeMilliseconds); + + selectedActivity = new ObservableValue(null); + detailsRequested = new Event(); + + /** + * We remember the activity that is displayed + * in the tooltip or details dialog to update + * it in case of removing of the corresponding + * slot from the timeline. + */ + preservedActivity: TimelineActivity | null = null; + + constructor( + public timelineSizeMilliseconds: number) { } + + init() { + this.ticks.init(); + this.updateInterval(); + this._timeRef = setInterval(() => { + this.updateInterval(); + }, 1000); + } + + private activityInteractionRequestHandler(slot: TimelineSlot | null, activity: TimelineActivity, requestType: ActivityInteractionRequest) { + switch (requestType) { + case ActivityInteractionRequest.ShowTooltip: + { + const currentSelection = this.selectedActivity.getValue(); + if (!currentSelection || currentSelection.activity !== activity) { + this.preservedActivity = activity; + this.selectedActivity.setValue({ + activity: activity, + slot: slot! // todo + }); + } + + this.preserveCurrentSelection(); + + break; + } + + case ActivityInteractionRequest.HideTooltip: + { + this.resetCurrentSelection(); + break; + } + + case ActivityInteractionRequest.ShowDetails: + { + /** + * We hide current tooltip bacause it + * does not make any sense to show it + * if details dialog is visible. + */ + this.hideTooltip(); + + this.preservedActivity = activity; + this.detailsRequested.trigger(activity); + + break; + } + } + } + + preserveCurrentSelection() { + this._resetSelectionTimer.reset(); + } + + resetCurrentSelection() { + this._resetSelectionTimer.schedule(() => { + this.hideTooltip(); + }, 2000); + } + + addSlot(slotOptions: ITimelineSlotOptions) { + const result = new TimelineSlot(slotOptions); + this.slots.add(result); + return result; + }; + + removeSlot(slot: TimelineSlot) { + this.slots.remove(slot); + } + + addActivity(slot: TimelineSlot, activityOptions: ITimelineActivityOptions): TimelineActivity { + var actualActivity = slot.add( + activityOptions, + (activity, requestType) => this.activityInteractionRequestHandler(slot, activity, requestType)); + + this.recalculateSlot(slot, this.range.getValue()); + + return actualActivity; + } + + addGlobalActivity(options: ITimelineGlobalActivityOptions) { + const activity: TimelineGlobalActivity = new TimelineGlobalActivity( + options, + requestType => this.activityInteractionRequestHandler(null, activity, requestType)); + + this.globalSlot.activities.add(activity); + this.recalculateSlot(this.globalSlot, this.range.getValue()); + + return activity; + } + + findSlotBy(key: string): TimelineSlot | null { + var slots = this.slots.getValue(); + for (var i = 0; i < slots.length; i++) { + if (slots[i].key === key) { + return slots[i]; + } + } + + return null; + } + + getGlobalActivities(): TimelineGlobalActivity[] { + return this.globalSlot.activities.getValue(); + } + + clearSlots() { + var slots = this.slots.getValue(); + for (var i = 0; i < slots.length; i++) { + slots[i].clear(); + } + } + + private updateInterval() { + var now = new Date().getTime(), + start = now - this.timelineSizeMilliseconds, + range = { + start: start, + end: now + }; + + this.range.setValue(range); + this.ticks.update(start, now); + + var slots = this.slots.getValue(); + for (var i = 0; i < slots.length; i++) { + this.recalculateSlot(slots[i], range); + } + + this.recalculateSlot(this.globalSlot, range); + } + + private recalculateSlot(slot: TimelineSlot, range: IRange) { + if (!range) { + return; + } + + const + slotRecalculateResult = slot.recalculate(range), + currentTooltipActivityData = this.selectedActivity.getValue(); + + if (currentTooltipActivityData && currentTooltipActivityData.slot === slot) { + /** + * We need to check if visible tooltip's + * activity is not the one that has just been + * removed from the timeline. + */ + + (slotRecalculateResult.removedActivities || []).forEach(x => { + if (currentTooltipActivityData.activity === x) { + this.hideTooltip(); + } + }); + } + } + + private hideTooltip() { + // this.selectedActivity.setValue(null); + this.preservedActivity = null; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/timeline/tooltips.scss b/src/CrystalQuartz.Application.Client2/src/timeline/tooltips.scss new file mode 100644 index 0000000..c574afe --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/timeline/tooltips.scss @@ -0,0 +1,142 @@ +@import "../global/constants"; +@import "constants"; + +.timeline-tooltip { + position: absolute; + z-index: 2010; + + opacity: 0; + + transition: opacity 0.2s ease, transform 0.2s ease; + + .arrow { + z-index: 20; + + display: block; + width: 0; + height: 0; + border-top: $timeline-global-pick-width solid transparent; + border-bottom: $timeline-global-pick-width solid transparent; + border-right: $timeline-global-pick-height solid #333333; + position: absolute; + top: $data-row-height / 2 - $timeline-global-pick-height; + } + + .content { + z-index: 10; + + background: #333333; + color: #FFFFFF; + padding: 5px 10px; + font-size: 11px; + font-weight: bold; + max-width: 300px; + line-height: 1.2; + overflow: hidden; + box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4); + box-sizing: border-box; + + p { + padding: 0; + margin: 0; + } + } + + &.visible { + opacity: 1; + } + + &.closing { + opacity: 0; + } +} + +.timeline-tooltip.local { + width: $timeline-global-pick-width * 2; + margin-left: -$timeline-global-pick-width; + padding-bottom: $timeline-global-pick-width; + + transform: translateY(-20px); + + .arrow { + top: auto; + bottom: 0; + left: 50%; + margin-left: -$timeline-global-pick-width; + border-top: $timeline-global-pick-width solid #333333; + border-left: $timeline-global-pick-width solid transparent; + border-right: $timeline-global-pick-height solid transparent; + border-bottom: none; + } + + .content { + width: 300px; + position: absolute; + bottom: $timeline-global-pick-width; + left: -150px; + } + + &.visible { + transform: translateY(0); + } + + table { + width: 100%; + } + + table td { + padding: 1px 0 1px 10px; + text-align: left; + } + + table th { + padding: 1px 10px 1px 0; + color: #DDDDDD; + } +} + +.timeline-tooltip.global { + .content { + position: absolute; + } + + .tooltip-content { + white-space: nowrap; + } + + &.visible { + transform: translateX(0); + } +} + +.timeline-tooltip.paused, +.timeline-tooltip.standby, +.timeline-tooltip.shutdown { + transform: translateX(-20px); + + .arrow { + right: $timeline-global-pick-width + $timeline-global-pick-height / 2; + left: auto; + + border-top: $timeline-global-pick-width solid transparent; + border-bottom: $timeline-global-pick-width solid transparent; + border-left: $timeline-global-pick-height solid #333333; + border-right: none; + } + + .content { + right: $timeline-global-pick-width + $timeline-global-pick-height / 2 + $timeline-global-pick-width; + } +} + +.timeline-tooltip.resumed { + transform: translateX(20px); + + .arrow { + left: $timeline-global-pick-width + $timeline-global-pick-height / 2; + } + + .content { + left: $timeline-global-pick-width + $timeline-global-pick-height / 2 + $timeline-global-pick-width; + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/utils/date.ts b/src/CrystalQuartz.Application.Client2/src/utils/date.ts new file mode 100644 index 0000000..3bd6af6 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/utils/date.ts @@ -0,0 +1,84 @@ +const getDate = (date: number | Date): Date => { + if (date instanceof Date) { + return date; + } + + return new Date(date); +}; + +interface IFormatter { + (date: number|Date): string; +} + +class DateLocaleEnvironment { + constructor( + public dateFormatter: IFormatter, + public timeFormatter: IFormatter) { } +} + +class LocaleEnvironmentFactory { + private _hours12Formatter = (date: number | Date) => { + const dateObject = getDate(date), + hours = dateObject.getHours(), + minutes = dateObject.getMinutes(), + seconds = dateObject.getSeconds(), + isPm = hours > 12; + + return this.padZeros(isPm ? hours - 12 : hours) + ':' + this.padZeros(minutes) + + (seconds > 0 ? (':' + this.padZeros(seconds)) : '') + ' ' + + (isPm ? 'PM' : 'AM'); + }; + + private _hours24Formatter = (date: number | Date) => { + const dateObject = getDate(date), + hours = dateObject.getHours(), + minutes = dateObject.getMinutes(), + seconds = dateObject.getSeconds(); + + return this.padZeros(hours) + ':' + this.padZeros(minutes) + + (seconds > 0 ? (':' + this.padZeros(seconds)) : ''); + }; + + private padZeros(value: number): string { + if (value < 10) { + return '0' + value; + } + + return value.toString(); + } + + private is24HoursFormat(): boolean { + const markerDate = new Date(2017, 4, 28, 17, 26); + + return markerDate.toLocaleTimeString().indexOf('17') >= 0; + } + + createEnvironment(): DateLocaleEnvironment { + const dateFormatter = (date: number | Date) => getDate(date).toLocaleDateString(); + + return new DateLocaleEnvironment( + dateFormatter, + this.is24HoursFormat() ? this._hours24Formatter : this._hours12Formatter); + } +} + +const localeEnvironment: DateLocaleEnvironment = new LocaleEnvironmentFactory().createEnvironment(); + +export default class DateUtils { + static smartDateFormat(date: number | Date): string { + const + now = new Date(), + today = now.setHours(0, 0, 0, 0), // start time of local date + tomorrow = today + 86400000, + dateObject = getDate(date), + dateTicks = dateObject.getTime(), + shouldOmitDate = dateTicks >= today && dateTicks <= tomorrow; + + return (shouldOmitDate ? '' : localeEnvironment.dateFormatter(dateObject) + ' ') + + localeEnvironment.timeFormatter(dateObject); + } + + static timeFormat(date: number|Date): string { + return localeEnvironment.timeFormatter(date); + } +} diff --git a/src/CrystalQuartz.Application.Client2/src/utils/number.ts b/src/CrystalQuartz.Application.Client2/src/utils/number.ts new file mode 100644 index 0000000..41ea07d --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/src/utils/number.ts @@ -0,0 +1,27 @@ +export default class NumberUtils { + private static THRESHOLDS = [ + { value: 1000000000, suffix: 'B' }, + { value: 1000000, suffix: 'M' }, + { value: 1000, suffix: 'K' } + ]; + + static formatLargeNumber(value: number): string | null { + if (value === null || value === undefined) { + return null; + } + + // just a simple optimization for small numbers as it is a common case + if (value < this.THRESHOLDS[this.THRESHOLDS.length - 1].value) { + return value.toString(); + } + + for (let i = 0; i < this.THRESHOLDS.length; i++) { + const thresholdValue = this.THRESHOLDS[i].value; + if (value > thresholdValue) { + return Math.floor(value / thresholdValue).toString() + this.THRESHOLDS[i].suffix; + } + } + + return value.toString(); + } +} diff --git a/src/CrystalQuartz.Application.Client2/tsconfig.dev-server.json b/src/CrystalQuartz.Application.Client2/tsconfig.dev-server.json new file mode 100644 index 0000000..1c8cf91 --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/tsconfig.dev-server.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +} diff --git a/src/CrystalQuartz.Application.Client2/tsconfig.json b/src/CrystalQuartz.Application.Client2/tsconfig.json new file mode 100644 index 0000000..cf8b02e --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "CommonJS", + "declaration": false, + "sourceMap": true, + "outDir": "./dist", + "strict": true, + "lib": ["dom", "es6"], + "jsx": "react", + "jsxFactory": "JS.d" + }, + "include": ["src"], + "exclude": ["node_modules", "src/**/*.spec.ts"] +} diff --git a/src/CrystalQuartz.Application.Client2/webpack.config.js b/src/CrystalQuartz.Application.Client2/webpack.config.js new file mode 100644 index 0000000..d120c9d --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/webpack.config.js @@ -0,0 +1,64 @@ +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); + +module.exports = (env) => { + const envSpecific = env && env.demo ? + { + entry: ['./index.ts', './demo/index.ts'], + outputPath: path.resolve(__dirname, './../../Artifacts/gh-pages/demo'), + outputPublicPath: '', + indexTemplate: 'demo/index-demo.placeholder.html', + defineVersion: true + } : + { + entry: './index.ts', + outputPath: path.resolve(__dirname, 'dist'), + outputPublicPath: '?v=[hash]&path=', + indexTemplate: 'index.placeholder.html', + defineVersion: false + }; + + return { + entry: ['./src/index.ts', './src/index.scss'], + devtool: 'inline-source-map', + target: ['web', 'es5'], + plugins: [ + new HtmlWebpackPlugin({ + template: './index.html', + }), + new MiniCssExtractPlugin({ + filename: "application.css", + chunkFilename: "[id].css", + }), + new BundleAnalyzerPlugin(), + ], + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + { + test: /\.s[ac]ss$/i, + use: [ + MiniCssExtractPlugin.loader, + "css-loader", + "sass-loader", + ], + }, + ], + }, + resolve: { + extensions: ['.tsx', '.ts', '.js'], + }, + output: { + filename: 'application.js', + path: envSpecific.outputPath, + publicPath: envSpecific.outputPublicPath, + clean: true + } + } +}; diff --git a/src/CrystalQuartz.Application.Client2/webpack.dev-server.config.js b/src/CrystalQuartz.Application.Client2/webpack.dev-server.config.js new file mode 100644 index 0000000..f3ca85c --- /dev/null +++ b/src/CrystalQuartz.Application.Client2/webpack.dev-server.config.js @@ -0,0 +1,33 @@ +var webpack = require("webpack"), + path = require('path'); + +module.exports = function(env) { +// var plugins = [ +// new webpack.ProvidePlugin({$: "jquery", jQuery: "jquery"}), +// ]; + + return { + entry: './dev/dev-server.ts', + target: 'node', + output: { + filename: 'index.js', + path: path.resolve(__dirname, 'dist-dev-server') + //publicPath: envSpecific.outputPublicPath + }, + module: { + rules: [ + { + test: /\.ts$/, + loader: 'ts-loader', + exclude: /node_modules/, + options: { + configFile: path.resolve(__dirname, 'tsconfig.dev-server.json'), + } + } + ] + }, + resolve: { + extensions: [".ts", ".js"] + }, + }; +} diff --git a/src/CrystalQuartz.Build/MainWorkflow.cs b/src/CrystalQuartz.Build/MainWorkflow.cs index 76b5291..4321284 100644 --- a/src/CrystalQuartz.Build/MainWorkflow.cs +++ b/src/CrystalQuartz.Build/MainWorkflow.cs @@ -200,7 +200,7 @@ from data in initTask "DevBuild", () => { }, - Default(), + /*Default(),*/ DependsOn(buildClient), DependsOn(buildPackages), DependsOn(copyGhPagesAssets), @@ -232,6 +232,7 @@ from data in initTask }, package => "Push" + package.NameWithoutExtension), + Default(), DependsOn(buildPackages)); } } diff --git a/src/version.txt b/src/version.txt index b2aea02..3000b58 100644 --- a/src/version.txt +++ b/src/version.txt @@ -1 +1 @@ -7.0.0.25 \ No newline at end of file +7.2.0.0 \ No newline at end of file