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",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "parcel src/index.html"
},
"author": "",
"author": "Adam Goldsmith <contact@adamgoldsmith.name>",
"license": "ISC",
"dependencies": {
"vue": "^2.6.10",
"vue-hot-reload-api": "^2.3.4",
"vue-property-decorator": "^8.3.0"
},
"devDependencies": {
"@vue/component-compiler-utils": "^3.0.2",
"sass": "^1.24.0",
"typescript": "^3.7.3",
"vue-template-compiler": "^2.6.10"
"@vitejs/plugin-vue": "^1.9.3",
"sass": "^1.42.1",
"typescript": "^4.4.3",
"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 }}
</span>
<Job
v-for="(jobEvents, jid) in jobs"
v-for="[jid, jobEvents] in jobs"
:key="jid"
:jid="Number(jid)"
:jid="jid"
:events="jobEvents"
:showRawEvents="showRawEvents"
:hideFindJob="hideFindJob"
@ -21,91 +21,88 @@
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop, Ref } from 'vue-property-decorator';
<script setup lang="ts">
import { ref, Ref, onMounted } from 'vue';
import credentials from './credentials.json';
import * as salt from './salt';
import Job from './Job';
import Job from './Job.vue';
const BASE_URL = 'https://salt.sawtooth.claremontmakerspace.org:8123/';
@Component({ components: { Job } })
export default class App extends Vue {
evtSource: EventSource | null = null;
events: salt.SaltEvent[] = [];
jobs: { [key: string]: salt.JobEvent[] } = {};
showRawEvents: boolean = false;
hideFindJob: boolean = true;
evtSourceTimeout: number | null = null;
evtSourceState: 'Disconnected' | 'Connecting' | 'Connected' = 'Disconnected';
let evtSource: EventSource;
const events: salt.SaltEvent[] = [];
const jobs: Ref<Map<salt.JobID, salt.JobEvent[]>> = ref(new Map());
const showRawEvents: Ref<boolean> = ref(false);
const hideFindJob: Ref<boolean> = ref(true);
const evtSourceTimeout: Ref<number | null> = ref(null);
const evtSourceState: Ref<'Disconnected' | 'Connecting' | 'Connected'> =
ref('Disconnected');
mounted(): void {
fetch(BASE_URL + 'login', {
method: 'POST',
mode: 'cors',
body: JSON.stringify(credentials),
})
.then((r) => r.json())
.then((r) => this.connectEventSource(r.return[0].token));
function connectEventSource(token: string): void {
if (evtSourceTimeout.value) {
evtSourceTimeout.value = null;
}
connectEventSource(token: string): void {
if (this.evtSourceTimeout) {
this.evtSourceTimeout = 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);
}
};
console.info('Connecting to eventSource.');
this.evtSource = new EventSource(BASE_URL + 'events?token=' + token);
evtSource.onerror = (e) => {
if (!evtSourceTimeout.value) {
console.info('Lost eventSource, reconnecting');
console.info(e);
updateEvtSourceState();
evtSource?.close();
evtSourceTimeout.value = window.setTimeout(
connectEventSource,
1000,
token
);
}
};
}
this.evtSource.onopen = (e) => {
console.info('Connected to eventSource.');
this.updateEvtSourceState();
};
onMounted(() => {
fetch(BASE_URL + 'login', {
method: 'POST',
mode: 'cors',
body: JSON.stringify(credentials),
})
.then((r) => r.json())
.then((r) => connectEventSource(r.return[0].token));
});
this.evtSource.onmessage = (e) => {
const event = JSON.parse(e.data);
event.splitTag = event.tag.split('/');
this.events.push(event);
function isEventType<T extends salt.SaltEvent['splitTag'][1]>(
event: salt.SaltEvent,
type: T
): event is Extract<salt.SaltEvent, { splitTag: ['salt', T, ...any[]] }> {
return event.splitTag[1] === type;
}
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,
type: T
): event is Extract<salt.SaltEvent, { splitTag: ['salt', T, ...any[]] }> {
return event.splitTag[1] === type;
}
updateEvtSourceState(): void {
this.evtSourceState =
!this.evtSource || this.evtSource.readyState == this.evtSource.CLOSED
? 'Disconnected'
: this.evtSource.readyState == this.evtSource.OPEN
? 'Connected'
: 'Connecting';
}
function updateEvtSourceState(): void {
evtSourceState.value =
!evtSource || evtSource.readyState == evtSource.CLOSED
? 'Disconnected'
: evtSource.readyState == evtSource.OPEN
? 'Connected'
: 'Connecting';
}
</script>

View File

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

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({
render: (h) => h(App),
}).$mount('#app');
const app = Vue.createApp(App)
.mount('#app');

View File

@ -1,8 +1,15 @@
{
"compilerOptions": {
"target": "es6",
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"module": "es2015",
"moduleResolution": "node"
}
"jsx": "preserve",
"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()],
})