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: {};
+}