diff --git a/src/App.vue b/src/App.vue index 513fc7e..8a52ba7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -15,7 +15,7 @@ v-for="(event, index) in minion_events" :key="index" > - {{ event.tag }} + {{ event_name(event) }}
{{ JSON.stringify(event, null, 2) }}
@@ -29,14 +29,33 @@ import { Vue, Component, Prop, Ref } from 'vue-property-decorator'; import credentials from './credentials.json'; +import * as salt from './salt'; + +function isEventType( + event: salt.SaltEvent, + type: T +): event is Extract { + return event.splitTag[1] === type; +} + +function isJobEventType( + event: salt.SaltEvent, + type: T +): event is Extract< + salt.SaltEvent, + { splitTag: ['salt', 'job', any, T, ...any[]] } +> { + return event.splitTag[3] === type; +} + const BASE_URL = 'https://salt.sawtooth.claremontmakerspace.org:8000/'; @Component export default class App extends Vue { evtSource: EventSource | null = null; - events: any[] = []; + events: salt.SaltEvent[] = []; - mounted() { + mounted(): void { fetch(BASE_URL + 'login', { method: 'POST', mode: 'cors', @@ -44,50 +63,58 @@ export default class App extends Vue { }) .then(r => r.json()) .then(r => { - const token = r.return[0].token; + const token: string = r.return[0].token; this.evtSource = new EventSource(BASE_URL + 'events?token=' + token); this.evtSource.onmessage = e => { - this.events.push(JSON.parse(e.data)); + const evt = JSON.parse(e.data); + evt.splitTag = evt.tag.split('/'); + this.events.push(evt); }; }); } - parse_tag(tag) { - // todo: would probably be easier as string splitting at this point - const match = tag.match(/salt\/job\/([^\/]*)\/(prog|ret)\/([^\/]*)\/?(.*)/); - if (!match) return null; - return { - jid: match[1], - type: match[2], - minion: match[3], - prog_id: match.length >= 5 ? match[4] : null, - }; + event_name(event: salt.JobEvent) { + if (isJobEventType(event, 'prog')) { + return `${this.event_symbol(event)} Progress: ${ + event.data.data.ret.name + }|${event.data.data.ret.duration} `; + } else if (isJobEventType(event, 'ret')) { + return `${this.event_symbol(event)} Return: ${event.data.fun} `; + } } - event_symbol(event) { - const tag = this.parse_tag(event.tag); - if (tag.type === 'prog') { + event_symbol(event: salt.JobEvent) { + if (isJobEventType(event, 'prog')) { const ret = event.data.data.ret; if (!ret.result) return '✗'; - else if ('ret' in ret.changes) return 'Δ'; + else if (Object.keys(ret.changes).length !== 0) return 'Δ'; else return '✓'; - } else if (tag.type === 'ret') { + } else if (isJobEventType(event, 'ret')) { return event.data.success ? '✓' : '✗'; } return '?'; } - get jobs() { - return this.events.reduce((acc, e) => { - const tag = this.parse_tag(e.tag); - if (tag) { - if (!(tag.jid in acc)) acc[tag.jid] = {}; - if (!(tag.minion in acc[tag.jid])) acc[tag.jid][tag.minion] = []; - acc[tag.jid][tag.minion].push(e); - } - return acc; - }, {}); + // {jid: {mid: event[]}} + get jobs(): { [key: string]: { [key: string]: salt.JobEvent[] } } { + return this.events.reduce( + ( + acc: { [key: string]: { [key: string]: salt.JobEvent[] } }, + e: salt.SaltEvent + ) => { + if ( + isEventType(e, 'job') && + (isJobEventType(e, 'prog') || isJobEventType(e, 'ret')) + ) { + if (!(e.data.jid in acc)) acc[e.data.jid] = {}; + if (!(e.data.id in acc[e.data.jid])) acc[e.data.jid][e.data.id] = []; + acc[e.data.jid][e.data.id].push(e); + } + return acc; + }, + {} + ); } } @@ -96,4 +123,8 @@ export default class App extends Vue { .minion .event { margin-left: 2ex; } + +pre { + white-space: pre-wrap; +} diff --git a/src/salt.d.ts b/src/salt.d.ts new file mode 100644 index 0000000..cac1405 --- /dev/null +++ b/src/salt.d.ts @@ -0,0 +1,95 @@ +export type MinionID = string; +export type JobID = string; + +export type SaltEvent = AuthenticationEvent | StartEvent | KeyEvent | JobEvent; +// | RunnerEvent +// | PresenceEvent +// | CloudEvent; + +export interface AuthenticationEvent { + tag: 'salt/auth'; + splitTag: ['salt', 'auth']; + data: { + id: MinionID; + act: 'accept' | 'pend' | 'reject'; + pub: string; + }; +} + +export interface StartEvent { + tag: string; // salt/minion//start + splitTag: ['salt', 'minion', string, 'start']; + data: { + id: MinionID; + }; +} + +export interface KeyEvent { + tag: 'salt/key'; + splitTag: ['salt', 'key']; + data: { + id: MinionID; + act: 'accept' | 'delete'; + }; +} + +export type JobEvent = JobNewEvent | JobRetEvent | JobProgEvent; + +export interface JobNewEvent { + tag: string; // salt/job//new + splitTag: ['salt', 'job', string, 'new']; + data: { + jid: JobID; + tgt: string; + tgt_type: 'glob' | 'grain' | 'compound' | string; + fun: string; + arg: [string]; + minions: [MinionID]; + user: string; + }; +} + +export interface JobRetEvent { + tag: string; // salt/job//ret/ + splitTag: ['salt', 'job', string, 'ret', string]; + data: { + id: MinionID; + jid: JobID; + retcode: number; + fun: string; + fun_args: [string]; + success: boolean; + return: any; + }; +} + +export interface JobProgEvent { + tag: string; // salt/job//prog// + splitTag: ['salt', 'job', string, 'prog', string, string]; + data: { + data: any; + id: MinionID; + jid: JobID; + }; +} + +// TODO +export interface RunnerEvent { + tag: string; + splitTag: []; + data: {}; +} + +// TODO +export interface PresenceEvent { + tag: string; + splitTag: []; + data: {}; +} + +// TODO +export interface CloudEvent { + tag: string; + splitTag: []; + data: {}; +}