Commit bad4d15e authored by Tobinsk's avatar Tobinsk
Browse files

Merge branch '36-data-insights' into 'master'

Resolve "Data insights"

Closes #36

See merge request !54
parents b6e271ef 6459c643
Pipeline #8883 passed with stage
in 4 minutes and 40 seconds
<template>
<div>
<b-alert variant="primary" :show="list.length === 0">
No data for your {{ $keycloak.tokenParsed.provider }}. Please check back later.
</b-alert>
<b-table
id="my-table"
:busy.sync="loading"
:items="list"
:fields="fields"
v-if="list.length > 0">
<template v-slot:cell(id)="id">
<!-- `data.value` is the value after formatted by the Formatter -->
<RouterLink :to="`/conflict/${id.value}`">{{id.value}}</RouterLink>
</template>
<template v-slot:cell(created_at)="created_at">
<!-- `data.value` is the value after formatted by the Formatter -->
{{created_at.value | date}}
</template>
</b-table>
</div>
</template>
<script>
import {mapGetters} from 'vuex'
export default {
name: "ConflictList",
data () {
return {
fields: [
{
key: 'id',
sortable: false
},
{
key: 'created_at',
sortable: true
},
]
}
},
computed: {
...mapGetters('conflict', ['list', 'loading'])
},
filters: {
date: function(d) {
return d.slice(0, 10)
}
}
}
</script>
<style scoped>
</style>
<template>
<div>
<cytoscape :config="config" :preConfig="preConfig" :afterCreated="afterCreated">
<cy-element
v-for="def in elements"
:key="`${def.data.id}`"
:definition="def"
/>
</cytoscape>
<b-alert v-for="(step, index) in steps" :key="index" variant="primary" show >
<strong v-html="step.message"></strong>
<span v-if="step.links.length > 0"> and found the following links:</span>
<ul>
<li v-for="(link, lindex) in step.links" :key="lindex">
<a :href="link.url">{{link.label}}</a>
</li>
</ul>
<b-button @click="toggle(step)" :pressed.sync="step.selected">Highlight</b-button>
</b-alert>
<b-alert variant="danger" show>
{{single.message}}
</b-alert>
</div>
</template>
<script>
import {mapGetters, mapMutations} from "vuex";
import {Getters, Mutations} from "@/store/conflict/type"
import fcose from 'cytoscape-fcose';
export default {
name: "ConflictVisual",
data() {
return {
button: [{state: false}],
config: {
style: [
{
selector: 'label',
style: {
}
},
{
selector: 'node',
style: {
'background-color': '#666',
'label': 'data(label)',
"font-size": "10px",
}
}, {
selector: function (el) {
if( typeof el.data().class !== "undefined") {
return el.data().class.includes('error')
}
return false;
},
css: {
'background-color': 'purple',
}
},{
selector: function (el) {
if( typeof el.data().class !== "undefined") {
return el.data().class.includes('collected')
}
return false
},
css: {
'background-color': 'yellow',
}
},{
selector: function (el) {
if( typeof el.data().class !== "undefined") {
return el.data().class.includes('highlight')
}
return false
},
css: {
'background-color': 'red',
}
},
{
selector: 'edge',
style: {
'width': 2,
'line-color': 'rgba(0,0,0,0.5)',
'target-arrow-color': '#ccc',
'target-arrow-shape': 'triangle'
}
},
{
selector: ':parent',
style: {
'background-opacity': 0.333,
'border-color': '#2B65EC',
'background-color': '#2B65EC'
}
},
],
layout: {
name: 'fcose',
fit: false,
nodeDimensionsIncludeLabels: true,
tilingPaddingVertical: 1,
// Represents the amount of the horizontal space to put between the zero degree members during the tiling operation(can also be a function)
tilingPaddingHorizontal: 1,
// Node repulsion (non overlapping) multiplier
nodeRepulsion: node => 1,
// Ideal edge (non nested) length
idealEdgeLength: edge => 1,
// Divisor to compute edge forces
edgeElasticity: edge => 0.45,
nodeSeparation: 1
},
}
}
},
computed: {
...mapGetters('conflict', {single: Getters.SINGLE}),
elements() {
// concat nodes and edges for cytoscape
return this.single.nodes.concat(this.single.edges)
},
steps() {
// calculate steps
return this.single.steps
}
},
methods: {
...mapMutations('conflict', {'highlightStore': Mutations.HIGHLIGHT, maskStore: Mutations.MASK}),
preConfig(cytoscape) {
// cytoscape: this is the cytoscape constructor
cytoscape.use(fcose);
},
afterCreated: function(cy) {
this.$nextTick(() => {
cy.layout(this.config.layout).run();
cy.center()
});
},
toggle(step) {
if(!step.selected) {
this.maskStore(step);
} else {
this.highlightStore(step);
}
}
}
}
</script>
<style scoped>
</style>
...@@ -13,6 +13,9 @@ ...@@ -13,6 +13,9 @@
<b-nav-item to="/matching"> <b-nav-item to="/matching">
<b-icon icon="shuffle"></b-icon> Match <b-icon icon="shuffle"></b-icon> Match
</b-nav-item> </b-nav-item>
<b-nav-item to="/conflict">
<b-icon icon="box"></b-icon> Data insights <b-badge pill variant="warning">Alpha</b-badge>
</b-nav-item>
<b-nav-item @click="$keycloak.logoutFn" v-if="$keycloak.authenticated"> <b-nav-item @click="$keycloak.logoutFn" v-if="$keycloak.authenticated">
<b-icon icon="power"></b-icon> Logout <b-icon icon="power"></b-icon> Logout
</b-nav-item> </b-nav-item>
......
...@@ -8,6 +8,10 @@ import VueKeycloakJs from '@dsb-norge/vue-keycloak-js'; ...@@ -8,6 +8,10 @@ import VueKeycloakJs from '@dsb-norge/vue-keycloak-js';
import './plugins/fetch-interceptor'; import './plugins/fetch-interceptor';
import 'whatwg-fetch'; import 'whatwg-fetch';
import Vuelidate from 'vuelidate' import Vuelidate from 'vuelidate'
import VueCytoscape from 'vue-cytoscape';
Vue.use(VueCytoscape)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Vue.use(Vuelidate) Vue.use(Vuelidate)
Vue.config.productionTip = false; Vue.config.productionTip = false;
......
...@@ -20,11 +20,12 @@ import { ...@@ -20,11 +20,12 @@ import {
ButtonPlugin, ButtonPlugin,
ButtonGroupPlugin, ButtonGroupPlugin,
CardPlugin, CardPlugin,
NavbarPlugin NavbarPlugin,
AlertPlugin
} from 'bootstrap-vue' } from 'bootstrap-vue'
// load icons // load icons
import { BIcon, BIconSearch, BIconShuffle, BIconFilter, BIconPower, BIconBoxArrowUpRight, BIconPencil, BIconArrowBarUp,BIconTrash,BIconX, BIconCheck, BIconGear, BIconPlus, BIconCalculator, BIconPlay } from 'bootstrap-vue' import { BIcon,BIconBox, BIconSearch, BIconShuffle, BIconFilter, BIconPower, BIconBoxArrowUpRight, BIconPencil, BIconArrowBarUp,BIconTrash,BIconX, BIconCheck, BIconGear, BIconPlus, BIconCalculator, BIconPlay } from 'bootstrap-vue'
// icons // icons
vue.component('BIcon', BIcon) vue.component('BIcon', BIcon)
vue.component('BIconSearch', BIconSearch) vue.component('BIconSearch', BIconSearch)
...@@ -41,6 +42,7 @@ vue.component('BIconGear', BIconGear) ...@@ -41,6 +42,7 @@ vue.component('BIconGear', BIconGear)
vue.component('BIconPlay', BIconPlay) vue.component('BIconPlay', BIconPlay)
vue.component('BIconCalculator', BIconCalculator) vue.component('BIconCalculator', BIconCalculator)
vue.component('BIconPlus', BIconPlus) vue.component('BIconPlus', BIconPlus)
vue.component('BIconBox', BIconBox)
// components // components
vue.use(FormPlugin) vue.use(FormPlugin)
vue.use(FormGroupPlugin) vue.use(FormGroupPlugin)
...@@ -61,3 +63,4 @@ vue.use(FormSelectPlugin) ...@@ -61,3 +63,4 @@ vue.use(FormSelectPlugin)
vue.use(FormCheckboxPlugin) vue.use(FormCheckboxPlugin)
vue.use(OverlayPlugin) vue.use(OverlayPlugin)
vue.use(LayoutPlugin) vue.use(LayoutPlugin)
vue.use(AlertPlugin)
...@@ -3,6 +3,8 @@ import VueRouter from 'vue-router' ...@@ -3,6 +3,8 @@ import VueRouter from 'vue-router'
import Search from '../views/Search.vue' import Search from '../views/Search.vue'
import Proposal from '../views/Proposal.vue' import Proposal from '../views/Proposal.vue'
import MatchOverview from '../views/MatchOverview.vue' import MatchOverview from '../views/MatchOverview.vue'
import ConflictOverview from '../views/ConflictOverview.vue'
import Conflict from '../views/Conflict.vue'
Vue.use(VueRouter); Vue.use(VueRouter);
...@@ -22,6 +24,16 @@ const routes = [ ...@@ -22,6 +24,16 @@ const routes = [
name: 'Proposal', name: 'Proposal',
component: Proposal component: Proposal
}, },
{
path: '/conflict',
name: 'ConflictOverview',
component: ConflictOverview
},
{
path: '/conflict/:id',
name: 'Conflict',
component: Conflict
},
{ {
path: '/about', path: '/about',
name: 'About', name: 'About',
......
import {Commit, Module} from 'vuex';
import {api, Conflict, MetagridError, Node, Step} from '@/store';
import {Actions, Getters, Mutations} from '@/store/conflict/type';
import {ErrorType} from '@/store/error';
import {TmpPagination} from "@/store/proposal";
interface ConflictState {
loading: boolean;
list: Conflict[];
single: Conflict;
}
const conflict: Module<ConflictState, any> = {
namespaced: true,
state: {
loading: false,
list: [],
single: {
nodes: [],
edges: [],
steps: []
},
},
mutations: {
/**
* Override the conflicts data
* @param state
* @param conflict
*/
[Mutations.LIST](state: ConflictState, conflict: Conflict[]): void {
state.list = conflict;
},
/**
* Set a single conflict
* @param state
* @param conflict
*/
[Mutations.SET](state: ConflictState, conflict: Conflict) : void {
state.single = conflict;
},
[Mutations.HIGHLIGHT](state: ConflictState, step: Step): void {
// select step
state.single.steps.map((s) => {
if(s === step) {
s.selected = true;
}
});
// highlight the nodes
const selectedNodes: string[] = []
step.links.forEach((l) => {
selectedNodes.push(l.label)
})
selectedNodes.push(step.main_person.label)
state.single.nodes.forEach((n: Node) => {
if(selectedNodes.includes(n.data.label)) {
if(!n.data.class.includes('highlight')) {
n.data.class.push('highlight');
}
}
})
},
[Mutations.MASK](state: ConflictState, step: Step): void {
// mask step
state.single.steps.map((s) => {
if(s === step) {
s.selected = false;
}
});
// masks the nodes again
const selectedNodes: string[] = []
step.links.forEach((l) => {
selectedNodes.push(l.label)
})
selectedNodes.push(step.main_person.label)
state.single.nodes.forEach((n: Node) => {
if(selectedNodes.includes(n.data.label)) {
n.data.class.splice(n.data.class.indexOf('highlight'),1)
}
return n
})
},
/**
* init loading
* @param state
*/
[Mutations.START_LOAD]: (state: ConflictState) : void => {
state.loading = true;
},
/**
* finish loading
* @param state
*/
[Mutations.FINISH_LOAD]: (state: ConflictState): void => {
state.loading = false;
},
},
getters: {
/**
* Get a paginated list of conflicts
* @param state
*/
[Getters.LIST]: (state: ConflictState) : Conflict[] => {
return state.list;
},
/**
* Get the selected conflict
* @param state
*/
[Getters.SINGLE]: (state: ConflictState): Conflict => {
return state.single;
},
/**
* Get the state of loading
* @param state
*/
[Getters.LOADING]: (state: ConflictState) : boolean => {
return state.loading;
},
},
actions: {
/**
* Get a list of conflicts form the server
* @param commit
* @param state
* @param pagination
*/
async [Actions.LIST]({commit} : {commit: Commit}, pagination: TmpPagination): Promise<void> {
if(typeof pagination === "undefined") {
pagination = {
size: 20,
from: 0,
};
}
commit(Mutations.START_LOAD);
const m: any = await fetch(`${api}/v3/conflicts?from=${pagination.from}&size=${pagination.size}`)
.then(r => {
if (r.status >= 200 && r.status <= 299) {
return r.json();
} else {
throw Error(r.statusText);
}
}).catch((err) => {
const metagridError: MetagridError = {
title: "Network error",
message: 'Can\'t fetch list of conflicts.',
type: ErrorType.error
}
commit('error/add', metagridError, { root: true });
throw err;
});
commit(Mutations.LIST, m.conflicts);
commit(Mutations.FINISH_LOAD);
return m.meta.total;
},
/**
* Get a single conflicts
* @param commit
* @param state
* @param id
*/
async [Actions.SINGLE]({commit}: {commit: Commit}, id: string): Promise<void> {
commit(Mutations.START_LOAD);
const conflict: Conflict = await fetch(`${api}/v3/conflicts/${id}`)
.then(r => {
if (r.status >= 200 && r.status <= 299) {
return r.json();
} else {
throw Error(r.statusText);
}
}).catch((err) => {
const metagridError: MetagridError = {
title: "Network error",
message: 'Can\'t fetch matching from server.',
type: ErrorType.error
}
commit('error/add', metagridError, { root: true });
throw err;
});
// Map state
conflict.steps = conflict.steps.map((s) => {
s.selected = false
return s
})
commit(Mutations.SET, conflict);
commit(Mutations.FINISH_LOAD);
},
}
};
export default conflict;
export const Actions = {
LIST: 'list',
SINGLE: 'single',
};
export const Getters = {
LIST: 'list',
LOADING: 'loading',
SINGLE: 'single',
};
export const Mutations = {
LIST: 'list',
START_LOAD: 'startLoad',
FINISH_LOAD: 'finishLoad',
SET: 'set',
HIGHLIGHT: 'highlight',
MASK: 'mask',
};
...@@ -5,6 +5,7 @@ import concordance from '@/store/concordance'; ...@@ -5,6 +5,7 @@ import concordance from '@/store/concordance';
import match from '@/store/match'; import match from '@/store/match';
import provider from '@/store/provider'; import provider from '@/store/provider';
import error, {ErrorType} from '@/store/error'; import error, {ErrorType} from '@/store/error';
import conflict from "@/store/conflict";
Vue.use(Vuex); Vue.use(Vuex);
...@@ -85,7 +86,7 @@ export interface Match { ...@@ -85,7 +86,7 @@ export interface Match {
parameters: MatchParameter[]; parameters: MatchParameter[];
} }
// proposals interfaace // proposals interface
export interface MatchProposal { export interface MatchProposal {
base: Concordance; base: Concordance;
proposals: Concordance[]; proposals: Concordance[];
...@@ -98,6 +99,46 @@ export interface MetagridError { ...@@ -98,6 +99,46 @@ export interface MetagridError {
type: ErrorType; type: ErrorType;
} </