Skip to content
Snippets Groups Projects
Verified Commit 713eb140 authored by BARBIER Marc's avatar BARBIER Marc
Browse files

src + ajout pattern au plot + fix eslint

parent 9de9f85f
No related branches found
No related tags found
No related merge requests found
......@@ -3,6 +3,9 @@ import HeaderComponent from '../components/HeaderComponent/HeaderComponent.vue'
export default defineComponent({
name: 'App',
components: {
HeaderComponent
},
data() {
return {
projects: null as Project[] | null,
......@@ -12,12 +15,9 @@ export default defineComponent({
},
computed: {
isReady() {
return this.projects != null;
return this.projects != null
}
},
components: {
HeaderComponent
},
mounted() {
this.loadProjects()
},
......@@ -81,7 +81,7 @@ export default defineComponent({
},
async deleteDataset(id: string) {
const url = 'http://localhost:8080/rest/item/remove?item='+encodeURIComponent(id);
const url = 'http://localhost:8080/rest/item/remove?item='+encodeURIComponent(id)
await fetch(url, { method: 'POST' })
this.loadProjects()
},
......
......
......@@ -8,6 +8,11 @@ import switchToNewestProject from '@/switchToNewestProject'
export default defineComponent({
name: 'TransformView',
components: {
HeaderComponent,
TableComponent,
Multiselect
},
data() {
return {
columns: [] as string[],
......@@ -76,11 +81,6 @@ export default defineComponent({
this.fileItem = await (await fetch(`http://localhost:8080/rest/metadata/fileitem?id=${localStorage.getItem('datasetId')}`)).json()
this.dictionary = await(await fetch(`http://localhost:8080/rest/mining/dictionary?id=${localStorage.getItem('datasetId')}`)).json()
},
components: {
HeaderComponent,
TableComponent,
Multiselect
},
methods: {
async runSetMining(e: Event) {
e.preventDefault()
......@@ -140,19 +140,19 @@ export default defineComponent({
form.append('id', '' + localStorage.getItem('datasetId'))
await fetch('http://localhost:8080/rest/mining/remove-patternset', { method: 'POST', body: form })
this.$router.go(0);
this.$router.go(0)
},
async filterLength(e: Event) {
e.preventDefault()
const currentPattern = this.currentPattern;
const currentPattern = this.currentPattern
if(currentPattern) {
//@ts-expect-error we know target is a form
const form = new FormData(e.target)
form.append('filename', currentPattern)
form.append('id', '' + localStorage.getItem('datasetId'))
await fetch('http://localhost:8080/rest/mining/filter-length', { method: 'POST', body: form })
this.$router.go(0);
this.$router.go(0)
} else {
this.$toast.warning('Please select a pattern')
}
......@@ -160,7 +160,7 @@ export default defineComponent({
async filterSupport(e: Event) {
e.preventDefault()
const currentPattern = this.currentPattern;
const currentPattern = this.currentPattern
if(currentPattern) {
//@ts-expect-error we know target is a form
const domForm: HTMLFormElement = e.target
......@@ -176,7 +176,7 @@ export default defineComponent({
form.append('filename', currentPattern)
form.append('id', '' + localStorage.getItem('datasetId'))
await fetch('http://localhost:8080/rest/mining/filter-support', { method: 'POST', body: form })
this.$router.go(0);
this.$router.go(0)
} else {
this.$toast.warning('Please select a pattern')
}
......@@ -184,7 +184,7 @@ export default defineComponent({
async removeRedundant(e: Event) {
e.preventDefault()
const currentPattern = this.currentPattern;
const currentPattern = this.currentPattern
if(currentPattern) {
//@ts-expect-error we know target is a form
const domForm: HTMLFormElement = e.target
......@@ -201,7 +201,7 @@ export default defineComponent({
form.append('filename', currentPattern)
form.append('id', '' + localStorage.getItem('datasetId'))
await fetch('http://localhost:8080/rest/mining/filter-jaccard', { method: 'POST', body: form })
this.$router.go(0);
this.$router.go(0)
} else {
this.$toast.warning('Please select a pattern')
}
......@@ -209,7 +209,7 @@ export default defineComponent({
async filterGapSpan(e: Event) {
e.preventDefault()
const currentPattern = this.currentPattern;
const currentPattern = this.currentPattern
if(currentPattern) {
//@ts-expect-error we know target is a form
const domForm: HTMLFormElement = e.target
......@@ -225,7 +225,7 @@ export default defineComponent({
form.append('filename', currentPattern)
form.append('id', '' + localStorage.getItem('datasetId'))
await fetch('http://localhost:8080/rest/mining/filter-time-constraint', { method: 'POST', body: form })
this.$router.go(0);
this.$router.go(0)
} else {
this.$toast.warning('Please select a pattern')
}
......@@ -275,7 +275,7 @@ export default defineComponent({
async filterMinMaxSupport(e: Event) {
e.preventDefault()
const currentPattern = this.currentPattern;
const currentPattern = this.currentPattern
if(!currentPattern) {
this.$toast.error('Please select a pattern')
......@@ -300,7 +300,7 @@ export default defineComponent({
const idsToIgnore: string[] = []
for(const pairs of this.requiredItems.split(',')) {
const [column, val] = pairs.split('=');
const [column, val] = pairs.split('=')
if(column !== this.currentColumn) continue
for(const item of this.newRequiredItems) {
......
......
......@@ -8,6 +8,13 @@ import makeTheSliderDragOnLock from './makeTheSliderDragOnLock'
export default defineComponent({
name: 'PlotView',
components: {
HeaderComponent,
TableComponent,
Multiselect,
ChartComponent,
Slider
},
data() {
return {
dataset: null as Table | null,
......@@ -16,7 +23,11 @@ export default defineComponent({
selectedY: [] as string[],
selectedEvents: [] as string[],
selectedColumns: [] as string[],
selectedAnomalyPatterns: [] as string[],
selectedPatterns: [] as string[],
selectedPatternSet: "",
patterns: null as Table | null,
patternsOcc: null as Table | null,
scores: null as Record<string, Table> | null,
showedColumns: [] as number[],
chartScale: [] as Array<string | number>,
......@@ -33,19 +44,51 @@ export default defineComponent({
},
computed: {
isReady() {
return true;
return true
},
hasWindows() {
return localStorage.getItem('hasWindows') === 'true'
},
patternCount(): number {
return new Set(this.patternsOcc?.rowsStartingFrom1.map(e => e[1])).size
}
},
components: {
HeaderComponent,
TableComponent,
Multiselect,
ChartComponent,
Slider
watch: {
selectedPatterns() {
this.loadAnomalyScores()
},
scores() {
this.generateChartData()
},
selectedColumns() {
this.generateChartData()
},
async slider() {
let xrangeMax= this.slider[1]
const xrangeMin= this.slider[0]
if(xrangeMin > xrangeMax ||
xrangeMax < 0 || xrangeMin < 0) {
return
}
//the server has a hard limit of 1K
if(xrangeMax - xrangeMin > 1000) {
xrangeMax = xrangeMin + 1000
}
await this.loadDataSet(xrangeMin, xrangeMax)
this.generateChartData()
},
selectedX() {
this.generateChartData()
}
},
async mounted() {
if(!localStorage.getItem('datasetId')) {
......@@ -60,6 +103,10 @@ export default defineComponent({
this.sliderMax = this.dataset?.totalSize || 1000
makeTheSliderDragOnLock(this.sliderMax)
this.generateChartData()
this.fetchPatternData()
},
updated() {
makeTheSliderDragOnLock(this.sliderMax)
},
methods: {
async loadDataSet(xmin?: number, xmax?: number) {
......@@ -76,7 +123,7 @@ export default defineComponent({
},
async loadAnomalyScores() {
const scores = this.fileItem?.scores.filter(e => this.selectedPatterns.includes(e.label))
const scores = this.fileItem?.scores.filter(e => this.selectedAnomalyPatterns.includes(e.label))
if(!scores || !scores.length) {
this.scores = {}
return
......@@ -91,56 +138,49 @@ export default defineComponent({
this.scores = item
},
async generateChartData() {
this.chartData = []
this.chartScale = []
const dataset = this.dataset
if(!dataset) return
const scaleColumnId = this.columnId(this.selectedX)
this.chartScale = this.extractColumn(dataset, scaleColumnId).map(e => {
if(!isNaN(parseFloat(e))) {
return parseFloat(e)
}
return e
})
async generateChartBackground(chartData: ChartElement[], dataset: Table) {
const labelColumnId = this.columnId('label')
if(labelColumnId !== -1 && this.showAnomalies) {
const labels = this.extractColumn(dataset, labelColumnId)
this.chartData.push({
chartData.push({
type: 'Background',
label: 'anomalies',
color: ["#FF5555", "#f4fff4"],
data: labels.map(e => e !== '1.0')
} as BackgroundElement)
}
},
if(this.hasWindows && this.showWindows)this.chartData.push({
async generateChartBlocks(chartData: ChartElement[], dataset: Table) {
if(this.hasWindows && this.showWindows)chartData.push({
type: 'Block',
data: this.windowToStepIntervales(dataset),
data: this.windowToStepIntervals(dataset),
coloringMode: 'stroke',
color: [ '#DDD'],
label: 'windows'
})
},
async generateChartLine(chartData: ChartElement[], dataset: Table) {
const colors = [ '#4e93c3', '#ff8a24', '#aec7e8', ...[...Array(dataset.rowsStartingFrom1.length)].map(this.generateRandomColor)]
for(const [ index, columnName ] of this.selectedColumns.entries()) {
const values = this.extractColumn(dataset, this.columnId(columnName))
this.chartData.push({
chartData.push({
type: 'Line',
data: values.map(e => parseFloat(e)),
label: this.dataset?.rows[0][this.columnId(columnName)] || 'no name',
color: colors[index]
})
}
},
async generateChartSelfScaledLine(chartData: ChartElement[], dataset: Table) {
const scores = this.scores
const windows = this.extractColumn(dataset, this.columnId('Window')).map(e => parseFloat(e))
if(scores) {
for(const patternScore of this.selectedPatterns) {
for(const patternScore of this.selectedAnomalyPatterns) {
const visibleScore = scores[patternScore].rowsStartingFrom1.filter(e => windows.includes(parseFloat(e[0])))
this.chartData.push({
chartData.push({
type: 'SelfScaledLine',
data: visibleScore.map(e => parseFloat(e[1])),
max: 1,
......@@ -152,11 +192,76 @@ export default defineComponent({
}
},
occurrencesToMap(occurences: string[][]): Record<string, string[]> {
const map: Record<string, string[]> = {}
for(const row of occurences) {
const [window, patternId] = row
if(map[patternId] === undefined) {
map[patternId] = [ window ]
} else {
map[patternId].push(window)
}
}
return map
},
async generateChartLabels(chartData: ChartElement[], dataset: Table) {
const patternOcc = this.patternsOcc
if(patternOcc) {
const windowIntervals = this.windowToStepIntervals(dataset)
//todo search for the window columns
const firstRow = dataset.rowsStartingFrom1[0]
const firstWindow = firstRow[firstRow.length - 1]
const occurrences = this.occurrencesToMap(patternOcc.rowsStartingFrom1)
for(const patternId of this.selectedPatterns) {
const patternWindows = occurrences[(parseInt(patternId) + 1).toString()]
//undefined values are values that is outside the current graph
// so we remove them with a filter
//TODO include instead of parse
const windowsPlacements = patternWindows.map(w => windowIntervals[parseInt(w) - parseInt(firstWindow)]).filter(e => !!e)
chartData.push({
type: 'Label',
label: patternId.toString(),
data: windowsPlacements.map(e => e.begin),
color: '#FF000055',
size: 10,
y: parseInt(patternId) / this.patternCount
})
}
}
},
async generateChartData() {
this.chartData = []
this.chartScale = []
const chartData = [] as ChartElement[]
const dataset = this.dataset
if(!dataset) return
const scaleColumnId = this.columnId(this.selectedX)
this.chartScale = this.extractColumn(dataset, scaleColumnId).map(e => {
if(!isNaN(parseFloat(e))) {
return parseFloat(e)
}
return e
})
await this.generateChartBackground(chartData, dataset)
await this.generateChartBlocks(chartData, dataset)
await this.generateChartLine(chartData, dataset)
await this.generateChartSelfScaledLine(chartData, dataset)
await this.generateChartLabels(chartData, dataset)
this.chartData = chartData
},
extractColumn(dataset: Table, col: number) {
return dataset.rowsStartingFrom1.map(e => e.filter((_val, index) => col === index)).flat()
},
windowToStepIntervales(dataset: Table) {
windowToStepIntervals(dataset: Table) {
const windowSets = this.extractColumn(dataset, this.columnId('Window'))
const windowIntervals = [] as { begin: number, end: number }[]
......@@ -178,43 +283,27 @@ export default defineComponent({
generateRandomColor() {
return `rgb(${Math.round(Math.random()* 255)},${Math.round(Math.random()* 255)},${Math.round(Math.random()* 255)})`
},
},
watch: {
selectedPatterns() {
this.loadAnomalyScores()
},
scores() {
this.generateChartData()
},
selectedColumns() {
this.generateChartData()
},
async slider() {
let xrangeMax= this.slider[1]
const xrangeMin= this.slider[0]
if(xrangeMin > xrangeMax ||
xrangeMax < 0 || xrangeMin < 0) {
return;
}
//the server has a hard limit of 1K
if(xrangeMax - xrangeMin > 1000) {
xrangeMax = xrangeMin + 1000
async fetchPatternData() {
const patternList = this.fileItem?.patterns
const selectedPattern = this.selectedPatternSet
if(!patternList) return
//simultaneous requests
let filename = ""
if(!selectedPattern && patternList[0]) {
filename = patternList[0].filename
this.selectedPatternSet = patternList[0].label
} else if(patternList.length) {
const patternId = patternList.findIndex(e => e.label === selectedPattern)
filename = patternList[patternId].filename
}
await this.loadDataSet(xrangeMin, xrangeMax)
this.generateChartData()
this.patterns = await (await fetch(`http://localhost:8080/rest/load-patterns?filename=${encodeURIComponent(filename)}&id=${localStorage.getItem('datasetId')}`)).json()
this.patternsOcc = await (await fetch(`http://localhost:8080/rest/load-pattern-occ?filename=${encodeURIComponent(filename)}&id=${localStorage.getItem('datasetId')}`)).json()
this.selectedPatterns = []
},
selectedX() {
this.generateChartData()
selectAll() {
this.selectedPatterns = this.patterns?.rowsStartingFrom1.map((val, index) => index.toString()) || []
}
},
updated() {
makeTheSliderDragOnLock(this.sliderMax)
},
})
......@@ -36,17 +36,34 @@
</label>
</div>
</div>
<div class="col-md-6">
<label>Anomaly score</label>
<Multiselect
mode="tags"
:options="fileItem?.scores.map(e => e.label)"
v-model="selectedAnomalyPatterns"
/>
</div>
<div class="row">
<label>Patterns</label>
<div div class="col-md-12">
<div class="col-md-4">
<Multiselect
:options="fileItem?.patterns.map(e => e.label)"
v-model="selectedPatternSet"
@change="fetchPatternData"
/>
</div>
<div div class="col-md-6">
<Multiselect
mode="tags"
:options="fileItem?.scores.map(e => e.label)"
:options="patterns?.rowsStartingFrom1.map((e, index) => ({ value: index, label: index + ': ' + e[0]}))"
v-model="selectedPatterns"
/>
</div>
<div div class="col-md-2">
<button class="btn btn-secondary">Select All</button>
<button class="btn btn-secondary" @click="selectAll">Select All</button>
</div>
</div>
</main>
......
......
......@@ -4,30 +4,30 @@
* @param {number} max
*/
export default function makeTheSliderDragOnLock(max) {
const limit = 1000;
const margin = 100;
const limit = 1000
const margin = 100
const slider = document.querySelector(".Slider")
slider.noUiSlider.on('slide', function( values, handle ) {
if (values[1] - values[0] > limit) {
if (handle) {
slider.noUiSlider.set([parseInt(values[1]) - limit, null]);
slider.noUiSlider.set([parseInt(values[1]) - limit, null])
} else {
slider.noUiSlider.set([null, parseInt(values[0]) + limit]);
slider.noUiSlider.set([null, parseInt(values[0]) + limit])
}
} else if(values[1] - values[0] < margin) {
if (handle) {
if(values[0] <= 0) {
slider.noUiSlider.set([null, margin]);
slider.noUiSlider.set([null, margin])
}else {
slider.noUiSlider.set([parseInt(values[1]) - margin, null]);
slider.noUiSlider.set([parseInt(values[1]) - margin, null])
}
} else {
if(values[1] >= max) {
slider.noUiSlider.set([max - margin, max]);
slider.noUiSlider.set([max - margin, max])
} else {
slider.noUiSlider.set([null, parseInt(values[0]) + margin]);
slider.noUiSlider.set([null, parseInt(values[0]) + margin])
}
}
}
});
})
}
......@@ -4,6 +4,10 @@ import TableComponent from '@/components/TableComponent/TableComponent.vue'
export default defineComponent({
name: 'TableView',
components: {
HeaderComponent,
TableComponent,
},
data() {
return {
dataset: null as Table | null,
......@@ -16,7 +20,7 @@ export default defineComponent({
},
computed: {
isReady() {
return this.dataset != null;
return this.dataset != null
},
hasWindows() {
return localStorage.getItem('hasWindows') === 'true'
......@@ -29,9 +33,8 @@ export default defineComponent({
return totalSize
}
},
components: {
HeaderComponent,
TableComponent,
watch: {
},
async mounted() {
if(!localStorage.getItem('datasetId')) {
......@@ -70,8 +73,5 @@ export default defineComponent({
const dataset = await this.fetchDataset(from, to)
return dataset.rows
}
},
watch: {
}
})
......@@ -10,6 +10,11 @@ ChartJS.register(Tooltip, BarElement, CategoryScale, LinearScale)
export default defineComponent({
name: 'TransformView',
components: {
HeaderComponent,
Multiselect,
Bar
},
data() {
return {
PAAValues: null as string[] | null,
......@@ -33,11 +38,6 @@ export default defineComponent({
return null
}
},
components: {
HeaderComponent,
Multiselect,
Bar
},
async mounted() {
if(!localStorage.getItem('datasetId')) {
this.$router.push('/')
......@@ -54,7 +54,7 @@ export default defineComponent({
async paa(e: Event) {
e.preventDefault()
//TODO: warning
if(!this.PAAValues || !this.PAAwindow)return;
if(!this.PAAValues || !this.PAAwindow)return
const form = new FormData()
form.append('columns', this.PAAValues?.join(','))
form.append('window', this.PAAwindow.toString())
......@@ -66,8 +66,8 @@ export default defineComponent({
async discretize(e: Event) {
e.preventDefault()
//TODO: warning
if(!this.discretizedColumns || !this.bins) return;
const form = new FormData();
if(!this.discretizedColumns || !this.bins) return
const form = new FormData()
form.append('columns', this.discretizedColumns.join(','))
form.append('density', this.useDensity.toString())
form.append('bins', this.bins.toString())
......
......
import { defineComponent, type PropType } from 'vue'
type BlockElement = {
interface BlockElement {
type: 'Block',
label: string,
data: { begin: number, end: number }[]
......@@ -8,21 +8,21 @@ type BlockElement = {
color: string[],
}
type BackgroundElement = {
interface BackgroundElement {
type: 'Background',
data?: boolean[],
label?: string,
color: string[]
}
type LineElement = {
interface LineElement {
type: 'Line',
label: string,
data: number[],
color: string
}
type SelfScaledLineElement = {
interface SelfScaledLineElement {
type: 'SelfScaledLine',
label: string,
max?: number,
......@@ -31,27 +31,43 @@ type SelfScaledLineElement = {
color: string
}
type ChartElement = BlockElement | LineElement | BackgroundElement | SelfScaledLineElement
interface LabelElement {
type: 'Label'
label: string
data: number[] //list of points
y?: number // scale between 0 and 1
size: number // px
color: string
}
type ChartElement = BlockElement | LineElement | BackgroundElement | SelfScaledLineElement | LabelElement
export type { ChartElement, BlockElement, LineElement, BackgroundElement }
export default defineComponent({
name: 'ChartComponent',
props: {
data: { type: Array as PropType<ChartElement[]>, required: true },
scale: { type: Array as PropType<Array<number | string>>, required: true },
width: { type: Number, default: null },
height: { type: Number, default: null},
scaleHeight: { type: Number, default: null}
},
data() {
return {
canvasId: this.$.uid
}
},
props: {
data: { type: Array as PropType<ChartElement[]>, required: true },
scale: { type: Array as PropType<Array<number | string>>, required: true },
width: { type: Number},
height: { type: Number},
scaleHeight: { type: Number }
watch: {
data() {
this.render()
},
computed: {
scale() {
this.render()
}
},
components: {
mounted() {
this.render()
},
methods: {
render() {
......@@ -164,6 +180,23 @@ export default defineComponent({
context.restore()
if(data.end === this.scale.length) break
}
break
}
case 'Label': {
for(const data of element.data) {
const x = data * dataGap + scaleHeight
const y = (element.y !== undefined ? element.y : 0.5 ) * (height - scaleHeight - element.size * 2) + element.size
context.save()
context.font = `${element.size * 1.5}px Arial`
context.beginPath()
context.ellipse(x, y, element.size, element.size, 0, 0, 2 * Math.PI)
context.fillStyle = element.color
context.fill()
context.fillStyle = "#000"
context.fillText(element.label, x - element.size/4, y + element.size/4, element.size)
context.restore()
}
break
}
}
}
......@@ -211,7 +244,7 @@ export default defineComponent({
printAngledText(context: CanvasRenderingContext2D, rotation: number, text: string, x: number, y: number, fontSize: number) {
context.save()
context.font = `${fontSize}px Arial`;
context.font = `${fontSize}px Arial`
context.translate(x - fontSize / 2, y - fontSize / 2)
context.rotate(rotation)
context.fillText(text, fontSize / 2, fontSize / 2)
......@@ -254,29 +287,18 @@ export default defineComponent({
context.save()
context.fillStyle = color
context.strokeStyle = color
context.beginPath();
context.arc(x1, y1, radius, 0, 2 * Math.PI);
context.beginPath()
context.arc(x1, y1, radius, 0, 2 * Math.PI)
context.fill()
context.lineWidth = 1;
context.lineWidth = 1
context.fillStyle = color
context.strokeStyle = color
context.beginPath();
context.moveTo(x1, y1);
context.beginPath()
context.moveTo(x1, y1)
context.lineTo(x2, y2)
context.stroke();
context.stroke()
context.restore()
}
},
mounted() {
this.render()
},
watch: {
data() {
this.render()
},
scale() {
this.render()
}
}
})
......@@ -2,6 +2,8 @@ import { defineComponent } from 'vue'
export default defineComponent({
name: 'HeaderComponent',
components: {
},
data() {
return {
project: localStorage.getItem('datasetId') || 'No current project'
......@@ -12,6 +14,4 @@ export default defineComponent({
return localStorage.getItem('datasetId') !== null
}
},
components: {
},
})
......@@ -2,6 +2,12 @@ import { defineComponent, type PropType } from 'vue'
export default defineComponent({
name: 'TableComponent',
props: {
data: { type: Array as PropType<Array<string | number>[]>, required: false, default: null },
dataCallback: { type: Function as PropType<(from: number, to: number) => ((number | string)[][] | Promise<(number | string)[][]>)>, required: false, default: null },
pageLength: { type: Number as PropType<number>, required: true },
nbElements: { type: Number as PropType<number>, required: false, default: null }
},
data() {
return {
currentPage: 0,
......@@ -9,12 +15,6 @@ export default defineComponent({
body: [] as (string | number)[][]
}
},
props: {
data: { type: Array as PropType<Array<string | number>[]>, required: false },
dataCallback: { type: Function as PropType<(from: number, to: number) => ((number | string)[][] | Promise<(number | string)[][]>)>, required: false},
pageLength: { type: Number as PropType<number>, required: true },
nbElements: { type: Number as PropType<number>, required: false }
},
computed: {
from() {
return this.currentPage * this.pageLength
......@@ -27,7 +27,7 @@ export default defineComponent({
nbPages(): number {
if(this.data)
return Math.ceil((this.data.length) / this.pageLength)
else if(this.dataCallback) {
else if(this.dataCallback !== null) {
if(this.nbElements)
return Math.ceil(this.nbElements / this.pageLength)
else throw new Error('nbElement need to be define with dataCallback')
......@@ -35,7 +35,13 @@ export default defineComponent({
throw new Error('No data was provided to the table component')
},
},
components: {
watch: {
currentPage() {
this.update()
}
},
mounted() {
this.update()
},
methods: {
changePage(id: number) {
......@@ -98,14 +104,5 @@ export default defineComponent({
this.header = await this.head()
this.body = await this.dataHeadless()
}
},
mounted() {
this.update()
},
watch: {
currentPage() {
this.update()
}
}
})
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import routes from './router';
import Toaster from "@incuca/vue3-toaster";
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import routes from './router'
import Toaster from "@incuca/vue3-toaster"
const router = createRouter({
history: createWebHistory(),
routes
});
})
const app = createApp({});
app.use(router);
const app = createApp({})
app.use(router)
app.use(Toaster)
app.mount("#app");
app.mount("#app")
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment