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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Panel initialization error
+
+
+
+
+
+
+
+
+
+
+
+
+
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 ;
+}
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
+
+
+
+
+
+
+
+ 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[]) =>
+
+
+
+ - {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
+
+
+
+
+
+ {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 @@
+
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 @@
+
+
+
+
+
+
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
+ }
+
+ // 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 @@
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 ;
+ }
+}
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
+
+
+
+
+
+
+
\ 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 ;
+ }
+
+ 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...
+
+
+
+
+
+
+
+
+
+
+
\ 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 ;
+ }
+
+ // 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 ;
+ }
+}
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
+
+
+
+
;
+ }
+
+ // 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 @@
+
+
+
+
+
\ 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
+ }}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+}
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
+ }
+}
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 ;
+ }
+
+ /** @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 ;
+ }
+
+ 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 = () => ;
+
+ // 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