Migrate to Vue 3.2 and Vite

This commit is contained in:
Adam Goldsmith 2021-10-18 02:17:29 -04:00
parent 2bfd4318d5
commit 4706ea6534
8 changed files with 178 additions and 165 deletions

12
index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Salt Status</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/index.ts"></script>
</body>
</html>

View File

@ -2,21 +2,22 @@
"name": "saltstatus", "name": "saltstatus",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "index.js", "author": "Adam Goldsmith <contact@adamgoldsmith.name>",
"scripts": {
"start": "parcel src/index.html"
},
"author": "",
"license": "ISC", "license": "ISC",
"dependencies": {
"vue": "^2.6.10",
"vue-hot-reload-api": "^2.3.4",
"vue-property-decorator": "^8.3.0"
},
"devDependencies": { "devDependencies": {
"@vue/component-compiler-utils": "^3.0.2", "@vitejs/plugin-vue": "^1.9.3",
"sass": "^1.24.0", "sass": "^1.42.1",
"typescript": "^3.7.3", "typescript": "^4.4.3",
"vue-template-compiler": "^2.6.10" "vite": "^2.6.4",
"vue-tsc": "^0.3.0"
},
"dependencies": {
"vue": "^3.2.0"
},
"scripts": {
"start": "npm run dev",
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"serve": "vite preview"
} }
} }

View File

@ -11,9 +11,9 @@
{{ evtSourceState }} {{ evtSourceState }}
</span> </span>
<Job <Job
v-for="(jobEvents, jid) in jobs" v-for="[jid, jobEvents] in jobs"
:key="jid" :key="jid"
:jid="Number(jid)" :jid="jid"
:events="jobEvents" :events="jobEvents"
:showRawEvents="showRawEvents" :showRawEvents="showRawEvents"
:hideFindJob="hideFindJob" :hideFindJob="hideFindJob"
@ -21,91 +21,88 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { Vue, Component, Prop, Ref } from 'vue-property-decorator'; import { ref, Ref, onMounted } from 'vue';
import credentials from './credentials.json'; import credentials from './credentials.json';
import * as salt from './salt'; import * as salt from './salt';
import Job from './Job'; import Job from './Job.vue';
const BASE_URL = 'https://salt.sawtooth.claremontmakerspace.org:8123/'; const BASE_URL = 'https://salt.sawtooth.claremontmakerspace.org:8123/';
@Component({ components: { Job } }) let evtSource: EventSource;
export default class App extends Vue { const events: salt.SaltEvent[] = [];
evtSource: EventSource | null = null; const jobs: Ref<Map<salt.JobID, salt.JobEvent[]>> = ref(new Map());
events: salt.SaltEvent[] = []; const showRawEvents: Ref<boolean> = ref(false);
jobs: { [key: string]: salt.JobEvent[] } = {}; const hideFindJob: Ref<boolean> = ref(true);
showRawEvents: boolean = false; const evtSourceTimeout: Ref<number | null> = ref(null);
hideFindJob: boolean = true; const evtSourceState: Ref<'Disconnected' | 'Connecting' | 'Connected'> =
evtSourceTimeout: number | null = null; ref('Disconnected');
evtSourceState: 'Disconnected' | 'Connecting' | 'Connected' = 'Disconnected';
mounted(): void { function connectEventSource(token: string): void {
if (evtSourceTimeout.value) {
evtSourceTimeout.value = null;
}
console.info('Connecting to eventSource.');
evtSource = new EventSource(BASE_URL + 'events?token=' + token);
evtSource.onopen = (e) => {
console.info('Connected to eventSource.');
updateEvtSourceState();
};
evtSource.onmessage = (e) => {
const event = JSON.parse(e.data);
event.splitTag = event.tag.split('/');
events.push(event);
if (isEventType(event, 'job')) {
if (!jobs.value.has(event.data.jid)) jobs.value.set(event.data.jid, []);
jobs.value.get(event.data.jid)!.push(event);
}
};
evtSource.onerror = (e) => {
if (!evtSourceTimeout.value) {
console.info('Lost eventSource, reconnecting');
console.info(e);
updateEvtSourceState();
evtSource?.close();
evtSourceTimeout.value = window.setTimeout(
connectEventSource,
1000,
token
);
}
};
}
onMounted(() => {
fetch(BASE_URL + 'login', { fetch(BASE_URL + 'login', {
method: 'POST', method: 'POST',
mode: 'cors', mode: 'cors',
body: JSON.stringify(credentials), body: JSON.stringify(credentials),
}) })
.then((r) => r.json()) .then((r) => r.json())
.then((r) => this.connectEventSource(r.return[0].token)); .then((r) => connectEventSource(r.return[0].token));
} });
connectEventSource(token: string): void { function isEventType<T extends salt.SaltEvent['splitTag'][1]>(
if (this.evtSourceTimeout) {
this.evtSourceTimeout = null;
}
console.info('Connecting to eventSource.');
this.evtSource = new EventSource(BASE_URL + 'events?token=' + token);
this.evtSource.onopen = (e) => {
console.info('Connected to eventSource.');
this.updateEvtSourceState();
};
this.evtSource.onmessage = (e) => {
const event = JSON.parse(e.data);
event.splitTag = event.tag.split('/');
this.events.push(event);
if (this.isEventType(event, 'job')) {
if (!(event.data.jid in this.jobs))
this.$set(this.jobs, event.data.jid, []);
this.jobs[event.data.jid].push(event);
}
};
this.evtSource.onerror = (e) => {
if (!this.evtSourceTimeout) {
console.info('Lost eventSource, reconnecting');
console.info(e);
this.updateEvtSourceState();
this.evtSource?.close();
this.evtSourceTimeout = window.setTimeout(
this.connectEventSource,
1000,
token
);
}
};
}
isEventType<T extends salt.SaltEvent['splitTag'][1]>(
event: salt.SaltEvent, event: salt.SaltEvent,
type: T type: T
): event is Extract<salt.SaltEvent, { splitTag: ['salt', T, ...any[]] }> { ): event is Extract<salt.SaltEvent, { splitTag: ['salt', T, ...any[]] }> {
return event.splitTag[1] === type; return event.splitTag[1] === type;
} }
updateEvtSourceState(): void { function updateEvtSourceState(): void {
this.evtSourceState = evtSourceState.value =
!this.evtSource || this.evtSource.readyState == this.evtSource.CLOSED !evtSource || evtSource.readyState == evtSource.CLOSED
? 'Disconnected' ? 'Disconnected'
: this.evtSource.readyState == this.evtSource.OPEN : evtSource.readyState == evtSource.OPEN
? 'Connected' ? 'Connected'
: 'Connecting'; : 'Connecting';
}
} }
</script> </script>

View File

@ -7,7 +7,7 @@
</span> </span>
<span class="jid"> {{ jid }}</span> <span class="jid"> {{ jid }}</span>
</summary> </summary>
<div v-for="(events, minion) in minionEvents" :key="minion"> <div v-for="[minion, events] in minionEvents" :key="minion">
<details class="minion"> <details class="minion">
<summary> <summary>
{{ minion }} {{ minion }}
@ -50,82 +50,76 @@
</details> </details>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { Vue, Component, Prop, Ref } from 'vue-property-decorator'; import { computed, ComputedRef } from '@vue/reactivity';
import * as salt from './salt'; import * as salt from './salt';
type MinionJobEvent = salt.JobProgEvent | salt.JobRetEvent; type MinionJobEvent = salt.JobProgEvent | salt.JobRetEvent;
@Component const props = defineProps<{
export default class Job extends Vue { jid: salt.JobID;
@Prop(Number) readonly jid: salt.JobID; showRawEvents: boolean;
@Prop(Boolean) readonly showRawEvents: boolean; events: salt.JobEvent[];
@Prop() readonly events: salt.JobEvent[]; hideFindJob: boolean;
@Prop(Boolean) readonly hideFindJob: boolean; }>();
isJobEventType<T extends salt.JobEvent['splitTag'][3]>( function isJobEventType<T extends salt.JobEvent['splitTag'][3]>(
event: salt.JobEvent, event: salt.JobEvent,
type: T type: T
): event is Extract< ): event is Extract<
salt.JobEvent, salt.JobEvent,
{ splitTag: ['salt', 'job', any, T, ...any[]] } { splitTag: ['salt', 'job', any, T, ...any[]] }
> { > {
return event.splitTag[3] === type; return event.splitTag[3] === type;
} }
get startEvent(): salt.JobNewEvent | undefined { function isMinionJobEvent(event: salt.JobEvent): event is MinionJobEvent {
return this.events.find((event): event is salt.JobNewEvent => return isJobEventType(event, 'prog') || isJobEventType(event, 'ret');
this.isJobEventType(event, 'new') }
);
}
get minionEvents(): { [key: string]: MinionJobEvent[] } { const startEvent: ComputedRef<salt.JobNewEvent | undefined> = computed(() =>
return this.events props.events.find((event): event is salt.JobNewEvent =>
.filter( isJobEventType(event, 'new')
(event) =>
this.isJobEventType(event, 'prog') ||
this.isJobEventType(event, 'ret')
) )
.reduce((acc, event: MinionJobEvent) => { );
if (!(event.data.id in acc)) acc[event.data.id] = [];
acc[event.data.id].push(event); const minionEvents: ComputedRef<Map<string, MinionJobEvent[]>> = computed(() =>
props.events.filter(isMinionJobEvent).reduce((acc, event: MinionJobEvent) => {
if (!acc.has(event.data.id)) acc.set(event.data.id, []);
acc.get(event.data.id).push(event);
return acc; return acc;
}, {}); }, new Map())
} );
get hidden(): boolean { const hidden: ComputedRef<boolean> = computed(
return ( () =>
this.hideFindJob && props.hideFindJob &&
this.events.find( props.events.find(
(event) => (event) =>
!this.isJobEventType(event, 'prog') && !isJobEventType(event, 'prog') && event.data.fun === 'saltutil.find_job'
event.data.fun === 'saltutil.find_job'
) !== undefined ) !== undefined
); );
}
event_name(event: salt.JobEvent) { function event_name(event: salt.JobEvent) {
if (this.isJobEventType(event, 'prog')) { if (isJobEventType(event, 'prog')) {
return `${this.event_symbol(event)} Progress: ${ return `${event_symbol(event)} Progress: ${event.data.data.ret.name}|${
event.data.data.ret.name event.data.data.ret.duration
}|${event.data.data.ret.duration} `; } `;
} else if (this.isJobEventType(event, 'ret')) { } else if (isJobEventType(event, 'ret')) {
return `${this.event_symbol(event)} Return: ${event.data.fun} `; return `${event_symbol(event)} Return: ${event.data.fun} `;
}
} }
}
event_symbol(event: salt.JobEvent) { function event_symbol(event: salt.JobEvent) {
if (this.isJobEventType(event, 'prog')) { if (isJobEventType(event, 'prog')) {
const ret = event.data.data.ret; const ret = event.data.data.ret;
if (!ret.result) return '✗'; if (!ret.result) return '✗';
else if (Object.keys(ret.changes).length !== 0) return 'Δ'; else if (Object.keys(ret.changes).length !== 0) return 'Δ';
else return '✓'; else return '✓';
} else if (this.isJobEventType(event, 'ret')) { } else if (isJobEventType(event, 'ret')) {
return event.data.success ? '→✔' : '→✘'; return event.data.success ? '→✔' : '→✘';
} }
return '?'; return '?';
}
} }
</script> </script>

View File

@ -1,4 +0,0 @@
<body>
<div id="app"></div>
<script type="text/javascript" src="index.ts"></script>
</body>

View File

@ -1,7 +1,6 @@
import Vue from 'vue'; import * as Vue from 'vue';
import App from './App'; import App from './App.vue';
let app = new Vue({ const app = Vue.createApp(App)
render: (h) => h(App), .mount('#app');
}).$mount('#app');

View File

@ -1,8 +1,15 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es6", "target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true, "strict": true,
"module": "es2015", "jsx": "preserve",
"moduleResolution": "node" "sourceMap": true,
} "resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
} }

7
vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
})