feat: dashboard feature ktx (#242)

* add dashboard functionality

* fix: regression issue linting errors

* fix: UI changes

* fix: temporary

* fix: reverted the file name change

---------

Co-authored-by: ktx-keshavi <keshavi.bhalani@kiara.tech>
This commit is contained in:
Bhargav
2023-03-10 10:02:28 +05:30
committed by GitHub
co-authored by ktx-keshavi
parent 0dbe81dab8
commit a7dfb2ee05
41 changed files with 4518 additions and 1078 deletions
+855 -42
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15Z" stroke="#ec1414" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M5.19995 8H10.8" stroke="#ec1414" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 418 B

@@ -1,18 +1,3 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<q-card class="column full-height">
<q-card-section class="q-px-md q-py-md text-black">
@@ -37,7 +22,7 @@
</q-card-section>
<q-separator />
<q-card-section class="q-w-md q-mx-lg">
<q-form ref="adddashboardForm" @submit="onSubmit">
<q-form ref="addDashboardForm" @submit="onSubmit">
<q-input
v-if="beingUpdated"
v-model="dashboardData.id"
@@ -45,11 +30,10 @@
:disabled="beingUpdated"
:label="t('dashboard.id')"
/>
<q-input
v-model="dashboardData.name"
:placeholder="t('dashboard.nameHolder')"
:label="t('dashboard.name') + '*'"
:placeholder="t('dashboard.nameHolder')"
color="input-border"
bg-color="input-bg"
class="q-py-md showLabelOnTop"
@@ -57,11 +41,13 @@
outlined
filled
dense
:rules="[(val) => !!val || t('dashboard.nameRequired')]"
/>
<span>&nbsp;</span>
<q-input
v-model="dashboardData.type"
v-model="dashboardData.description"
:placeholder="t('dashboard.typeHolder')"
:label="t('dashboard.typeDesc') + '*'"
:label="t('dashboard.typeDesc')"
color="input-border"
bg-color="input-bg"
class="q-py-md showLabelOnTop"
@@ -82,6 +68,7 @@
no-caps
/>
<q-btn
:disable="dashboardData.name === ''"
:label="t('dashboard.save')"
class="q-mb-md text-bold no-border q-ml-md"
color="secondary"
@@ -107,13 +94,14 @@ const defaultValue = () => {
return {
id: "",
name: "",
description: "",
};
};
let calldashboard: Promise<{ data: any }>;
let callDashboard: Promise<{ data: any }>;
export default defineComponent({
name: "ComponentAddUpdateUser",
name: "ComponentAddDashboard",
props: {
modelValue: {
type: Object,
@@ -124,7 +112,7 @@ export default defineComponent({
setup() {
const store: any = useStore();
const beingUpdated: any = ref(false);
const adddashboardForm: any = ref(null);
const addDashboardForm: any = ref(null);
const disableColor: any = ref("");
const dashboardData: any = ref(defaultValue());
const isValidIdentifier: any = ref(true);
@@ -141,7 +129,7 @@ export default defineComponent({
beingUpdated,
status,
dashboardData,
adddashboardForm,
addDashboardForm,
store,
getRandInteger,
isValidIdentifier,
@@ -155,6 +143,7 @@ export default defineComponent({
this.dashboardData = {
id: this.modelValue.id,
name: this.modelValue.name,
description: this.modelValue.description,
};
}
},
@@ -171,7 +160,7 @@ export default defineComponent({
message: "Please wait...",
timeout: 2000,
});
this.adddashboardForm.validate().then((valid: any) => {
this.addDashboardForm.validate().then((valid: any) => {
if (!valid) {
return false;
}
@@ -191,7 +180,7 @@ export default defineComponent({
};
const obj = toRaw(this.dashboardData);
baseObj.title = obj.name;
baseObj.description = obj.type;
baseObj.description = obj.description;
baseObj.dashboardId = "DashID_" + this.getRandInteger();
baseObj.created = new Date().toISOString();
baseObj.owner =
@@ -199,15 +188,37 @@ export default defineComponent({
" " +
toRaw(this.store.state.currentuser.last_name);
calldashboard = dashboardService.create(
callDashboard = dashboardService.create(
this.store.state.selectedOrganization.identifier,
baseObj.dashboardId,
JSON.stringify(JSON.stringify(baseObj))
);
}
calldashboard.then((res: { data: any }) => {
this.$emit("updated");
});
callDashboard
.then((res: { data: any }) => {
const data = res.data;
this.dashboardData = {
id: "",
name: "",
description: "",
};
this.$emit("update:modelValue", data);
this.$emit("updated");
console.log("Done saving");
this.addDashboardForm.resetValidation();
dismiss();
})
.catch((err: any) => {
this.$q.notify({
type: "negative",
message: JSON.stringify(
err.response.data["error"] || "Dashboard creation failed."
),
});
console.log(err);
dismiss();
});
});
},
},
@@ -0,0 +1,55 @@
<template>
<div class="plotlycontainer">
<div class="drag-allow">
<PanelHeader
:panelDataElement="props.data"
:dashboardId="$route.query.dashboard"
@clicked="onClickChild"
/>
</div>
<ChartRender
:data="props.data"
:selectedTimeDate="props.selectedTimeDate"
></ChartRender>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import ChartRender from "./addPanel/ChartRender.vue";
import PanelHeader from "./PanelHeader.vue";
export default defineComponent({
name: "PanelContainer",
emits: ["updated:chart"],
props: ["data", "selectedTimeDate"],
components: {
ChartRender,
PanelHeader,
},
setup(props) {
//get props data
const getPanelDataElement = () => {
return props.data;
};
return {
props,
getPanelDataElement,
};
},
methods: {
getDashboardId() {
return this.$route.query.dashboard;
},
onClickChild(panelDataElementValue: any) {
this.$emit("updated:chart", panelDataElementValue);
},
},
});
</script>
<style lang="scss" scoped>
.plotlycontainer {
height: 100%;
}
</style>
@@ -0,0 +1,116 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<div>
<q-bar class="bg-white">
<!-- <q-separator dark vertical inset /> -->
<div class="">
{{ renderTitle }}
</div>
<q-space />
<!-- <q-toolbar-title>{{ panelDataElementObject.title }}</q-toolbar-title>
<q-separator dark vertical inset /> -->
<q-btn-dropdown dense flat label="" no-caps >
<q-list dense>
<q-item clickable v-close-popup @click="onPanelModifyClick('EditPanel')">
<q-item-section>
<q-item-label class="q-pa-sm">Edit Panel</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="onPanelModifyClick('DeletePanel')">
<q-item-section>
<q-item-label class="q-pa-sm">Delete Panel</q-item-label>
</q-item-section>
</q-item>
<!-- <q-item clickable v-close-popup @click="onPanelModifyClick('PrintPanel')">
<q-item-section>
<q-item-label class="q-pa-sm">Print Panel</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="onPanelModifyClick('SharePanel')">
<q-item-section>
<q-item-label class="q-pa-sm">Share Panel</q-item-label>
</q-item-section>
</q-item> -->
</q-list>
</q-btn-dropdown>
</q-bar>
</div>
</template>
<script lang="ts">
import { log } from "console";
import { computed, defineComponent } from "vue";
import { toRaw } from 'vue';
import { useRouter } from "vue-router";
export default defineComponent({
name: "PanelHeader",
props: ["panelDataElement", "dashboardId"],
panelData: [],
setup(props, { emit }) {
const panelDataElementObject = toRaw(props.panelDataElement)
const dashboardId = props.dashboardId
const router = useRouter();
const renderTitle = computed(() => {
return props.panelDataElement.config?.title
})
const addNewPanel = () => {
// return router.push({
// path: "/addPanel",
// query: { dashboard: String(dashboardId), panelId:panelDataElementObject.id },
// });
return router.push({
path: "/addPanel",
query: { dashboard: String(dashboardId), panelId:panelDataElementObject.id },
});
}
const deletePanel = () => {
// return router.push({
// path: "/viewDashboard",
// query: { dashboard: String(dashboardId), panelId:panelDataElementObject.id },
// });
return router.push({
path: "/viewDashboard",
query: { dashboard: String(dashboardId), panelId:panelDataElementObject.id },
});
}
return {
panelDataElementObject,
dashboardId,
addNewPanel,
deletePanel,
renderTitle
};
}, methods: {
onPanelModifyClick(evt: any) {
if(evt == 'EditPanel'){this.addNewPanel()}
else if(evt == 'DeletePanel') {//this.deletePanel()
this.$emit('clicked', this.panelDataElementObject)
}
else {console.log(evt)}
},
},
});
</script>
@@ -0,0 +1,329 @@
<template>
<div ref="editorRef" id="editor"></div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted, onUpdated, watch } from "vue";
import * as monaco from "monaco-editor";
export default defineComponent({
props: {
query: {
type: String,
default: "",
},
fields: {
type: Array,
default: [],
},
functions: {
type: Array,
default: [],
},
readOnly: {
type: Boolean,
default: false
}
},
emits: ["update-query", "run-query", "update:query"],
setup(props, { emit }) {
const editorRef: any = ref();
let editorObj: any = null;
const createDependencyProposals = (range: any) => {
const keywords = [
{
label: "and",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "and ",
range: range,
},
{
label: "or",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "or ",
range: range,
},
{
label: "like",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "like '%${1:params}%' ",
range: range,
},
{
label: "in",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "in ('${1:params}') ",
range: range,
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
},
{
label: "not in",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "not in ('${1:params}') ",
range: range,
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
},
{
label: "between",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "between('${1:params}','${1:params}') ",
range: range,
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
},
{
label: "not between",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "not between('${1:params}','${1:params}') ",
range: range,
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
},
{
label: "is null",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "is null ",
range: range,
},
{
label: "is not null",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "is not null ",
range: range,
},
{
label: ">",
kind: monaco.languages.CompletionItemKind.Operator,
insertText: "> ",
range: range,
},
{
label: "<",
kind: monaco.languages.CompletionItemKind.Operator,
insertText: "< ",
range: range,
},
{
label: ">=",
kind: monaco.languages.CompletionItemKind.Operator,
insertText: ">= ",
range: range,
},
{
label: "<=",
kind: monaco.languages.CompletionItemKind.Operator,
insertText: "<= ",
range: range,
},
{
label: "<>",
kind: monaco.languages.CompletionItemKind.Operator,
insertText: "<> ",
range: range,
},
{
label: "=",
kind: monaco.languages.CompletionItemKind.Operator,
insertText: "= ",
range: range,
},
{
label: "!=",
kind: monaco.languages.CompletionItemKind.Operator,
insertText: "!= ",
range: range,
},
{
label: "()",
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: "(${1:condition}) ",
range: range,
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
},
];
props.fields.forEach((field: any) => {
if (field.name == "_timestamp") {
return;
}
let itemObj = {
label: field.name,
kind: monaco.languages.CompletionItemKind.Text,
insertText: field.name,
range: range,
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
};
keywords.push(itemObj);
});
props.functions.forEach((field: any) => {
let itemObj = {
label: field.name,
kind: monaco.languages.CompletionItemKind.Text,
insertText: field.name + field.args,
range: range,
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
};
keywords.push(itemObj);
});
return keywords;
};
onMounted(async () => {
// editorRef.value.addEventListener("keyup", onKeyUp);
monaco.editor.defineTheme("myCustomTheme", {
base: "vs", // can also be vs-dark or hc-black
inherit: true, // can also be false to completely replace the builtin rules
rules: [{ token: "comment", background: "FF0000" }],
colors: {
"editor.foreground": "#000000",
"editor.background": "#F7F7F7",
"editorCursor.foreground": "#000000",
"editor.lineHighlightBackground": "#000000",
"editorLineNumber.foreground": "#000000",
"editor.border": "#000000",
},
});
monaco.languages.registerCompletionItemProvider("sql", {
provideCompletionItems: function (model, position) {
// find out if we are completing a property in the 'dependencies' object.
var textUntilPosition = model.getValueInRange({
startLineNumber: 1,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
});
var word = model.getWordUntilPosition(position);
var range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
};
let arr = textUntilPosition.trim().split(" ");
let filteredSuggestions = [];
filteredSuggestions = createDependencyProposals(range);
filteredSuggestions = filteredSuggestions.filter((item) => {
return item.label.toLowerCase().includes(word.word.toLowerCase());
});
// if (filteredSuggestions.length == 0) {
const lastElement = arr.pop();
filteredSuggestions.push({
label: `match_all('${lastElement}')`,
kind: monaco.languages.CompletionItemKind.Text,
insertText: `match_all('${lastElement}')`,
range: range,
});
filteredSuggestions.push({
label: `match_all_no_case('${lastElement}')`,
kind: monaco.languages.CompletionItemKind.Text,
insertText: `match_all_no_case('${lastElement}')`,
range: range,
});
return {
suggestions: filteredSuggestions,
};
// } else {
// return {
// suggestions: filteredSuggestions,
// };
// }
},
});
editorObj = monaco.editor.create(editorRef.value, {
value: props.query,
language: "sql",
theme: "myCustomTheme",
showFoldingControls: "never",
wordWrap: "on",
lineNumbers: "on",
lineNumbersMinChars: 0,
overviewRulerLanes: 0,
fixedOverflowWidgets: false,
overviewRulerBorder: false,
lineDecorationsWidth: 15,
hideCursorInOverviewRuler: true,
renderLineHighlight: "none",
glyphMargin: false,
folding: false,
scrollBeyondLastColumn: 0,
scrollBeyondLastLine: true,
scrollbar: { horizontal: "auto", vertical: "visible" },
find: {
addExtraSpaceOnTop: false,
autoFindInSelection: "never",
seedSearchStringFromSelection: "never",
},
minimap: { enabled: false },
readOnly: props.readOnly
});
editorObj.onDidChangeModelContent((e: any) => {
emit("update-query", editorObj.getValue());
emit("update:query", editorObj.getValue());
});
editorObj.createContextKey("ctrlenter", true);
editorObj.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
function () {
emit("run-query");
},
"ctrlenter"
);
});
const setValue = (value: string) => {
editorObj.setValue(value);
};
// update readonly when prop value changes
watch(() => props.readOnly, () => {
editorObj.updateOptions({ readOnly: props.readOnly })
})
// update readonly when prop value changes
watch(() => props.query, () => {
if(props.readOnly) {
editorObj.getModel().setValue(props.query)
}
})
return {
editorRef,
editorObj,
setValue,
};
},
});
</script>
<style>
#editor {
min-height: 4rem;
border-radius: 5px;
border: 0px solid #dbdbdb;
}
.monaco-editor,
.monaco-editor .monaco-editor {
padding: 0px 0px 0px 0px !important;
}
</style>
+301
View File
@@ -0,0 +1,301 @@
<template>
<div>
<q-bar class="q-pa-sm bg-white">
<span class="text-subtitle2 text-weight-bold">SQL</span>
<q-space />
<q-toggle
v-model="dashboardPanelData.layout.showCustomQuery"
:label="t('Custom SQL')"
/>
<q-btn-dropdown
color="white"
text-color="black"
@click="onDropDownClick"
/>
</q-bar>
</div>
<div class="row" v-if="showQuery">
<!-- <div class="col">
<q-input
v-model="dashboardPanelData.data.query"
filled
clearable
autogrow
color="green-8"
label="Please provide SQL Query for Visualization"
hint="Press Apply Button to complete query preview"
:disable="!dashboardPanelData.meta.stream.showCustomQuery"
/>
</div> -->
<div class="col">
<query-editor
ref="queryEditorRef"
class="monaco-editor"
v-model:query="dashboardPanelData.data.query"
v-model:fields="dashboardPanelData.meta.stream.selectedStreamFields"
v-model:functions="dashboardPanelData.meta.stream.functions"
@run-query="searchData"
:readOnly="!dashboardPanelData.layout.showCustomQuery"
></query-editor>
<!-- @update-query="updateQueryValue" -->
<div style="color: red;" class="q-mx-sm">{{ data.queryErrors.join(', ') }}&nbsp;</div>
</div>
</div>
</template>
<script lang="ts">
// @ts-nocheck
import { defineComponent, ref, watch, reactive, toRaw } from "vue";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import { useQuasar } from "quasar";
import { Parser } from "node-sql-parser";
import QueryEditor from "./QueryEditor.vue";
import useDashboardPanelData from "../../composables/useDashboardPanel";
export default defineComponent({
name: "ComponentSearchSearchBar",
components: {
QueryEditor,
},
emits: ["searchdata"],
methods: {
searchData() {
if (this.searchdashboardPanelData.loading == false) {
// this.searchObj.runQuery = true;
this.$emit("searchdata");
}
},
},
setup() {
// show the query box
const showQuery = ref(true)
const data = reactive({
queryErrors: [] // custom query errors
})
const router = useRouter();
const { t } = useI18n();
const $q = useQuasar();
const { dashboardPanelData } = useDashboardPanelData()
const parser = new Parser();
let streamName = "";
// toggle show query view
const onDropDownClick= () =>{
showQuery.value = !showQuery.value
}
// Generate the query when the fields are updated
watch(() => [
dashboardPanelData.data.fields.stream,
dashboardPanelData.data.fields.x,
dashboardPanelData.data.fields.y,
dashboardPanelData.data.fields.filter,
dashboardPanelData.layout.showCustomQuery
], () => {
// only continue if current mode is auto query generation
if(!dashboardPanelData.layout.showCustomQuery){
console.log("Updating query");
// STEP 1: first check if there is at least 1 field selected
if(dashboardPanelData.data.fields.x.length == 0 && dashboardPanelData.data.fields.y.length == 0) {
dashboardPanelData.data.query = ""
return;
}
// STEP 2: Now, continue if we have at least 1 field selected
// merge the fields list
let query = "SELECT "
const fields = [...dashboardPanelData.data.fields.x, ...dashboardPanelData.data.fields.y]
const filter = [...dashboardPanelData.data.fields?.filter]
const array = fields.map((field, i) => {
let selector = ""
// TODO: add aggregator
if(field.aggregationFunction) {
selector += `${field.aggregationFunction}(${field.column})`
} else {
selector += `${field.column}`
}
selector += ` as ${field.label}${i==fields.length-1 ? ' ' : ', '}`
return selector
})
query += array.join("")
// now add from stream name
query += ` FROM '${dashboardPanelData.data.fields.stream}' `
query += filter.length ? "WHERE " : ""
const filterData = filter?.map((field, i)=>{
let selectFilter = ""
if(field.type == "list" && field.values?.length > 0){
selectFilter += `${field.column} IN (${field.values.map(it => `'${it}'`).join(', ')})`
}else if (field.type == "condition" && field.operator != null && field.value != null && field.value != ''){
selectFilter += `${field.column} `
switch(field.operator) {
case "=":
case "<>":
case "<":
case ">":
case "<=":
case ">=":
selectFilter += `${field.operator} ${field.value}`
break;
case "Contains":
selectFilter += `LIKE '%${field.value}%'`
break;
case "Not Contains":
selectFilter += `NOT LIKE '%${field.value}%'`
break;
default:
selectFilter += `${field.operator} ${field.value}`
break;
}
}
return selectFilter
})
console.log("query: filterData",filterData);
query += filterData.filter((it: any)=> it).join(" AND ")
// add group by statement
query += ` GROUP BY ${dashboardPanelData.data.fields.x[0]?.label}`
console.log('generated query: ', query)
dashboardPanelData.data.query = query
}
}, {deep: true})
watch(() => dashboardPanelData.data.query, ()=>{
console.log("query changes in search bar",dashboardPanelData.layout.showCustomQuery);
// only continue if current mode is show custom query
if(dashboardPanelData.layout.showCustomQuery){
updateQueryValue()
}
})
//TODO: verification to perform when the query is updated
const updateQueryValue = () => {
// store the query in the dashboard panel data
// dashboardPanelData.meta.editorValue = value;
// dashboardPanelData.data.query = value;
if (dashboardPanelData.layout.showCustomQuery) {
console.log("query: value", dashboardPanelData.data.query);
// empty the errors
data.queryErrors = []
console.log("----------", dashboardPanelData.data.query);
console.log("---sql mode-------", dashboardPanelData.layout.showCustomQuery);
// Get the parsed query
try {
dashboardPanelData.meta.parsedQuery = parser.astify(dashboardPanelData.data.query);
console.log(dashboardPanelData.meta.parsedQuery)
} catch(e) {
console.log("error")
console.log(e)
// exit as there is an invalid query
data.queryErrors.push("Invalid SQL Syntax")
return null;
}
console.log("--parsed query---", dashboardPanelData.meta.parsedQuery);
if(!dashboardPanelData.meta.parsedQuery) {
return;
}
// We have the parsed query, now get the columns and tables
// get the columns first
if(Array.isArray(dashboardPanelData.meta.parsedQuery?.columns)
&& dashboardPanelData.meta.parsedQuery?.columns?.length > 0) {
dashboardPanelData.meta.stream.customQueryFields = []
dashboardPanelData.meta.parsedQuery.columns.forEach((item: any, index: any) => {
let val;
// if there is a lable, use that, else leave it
if (item["as"] === undefined || item["as"] === null) {
val = item["expr"]["column"];
} else {
val = item["as"];
}
if (!dashboardPanelData.meta.stream.customQueryFields.find(it => it.name == val)) {
dashboardPanelData.meta.stream.customQueryFields.push({name: val, type: ''});
}
});
} else {
data.queryErrors.push("Invalid Columns")
}
// now check if the correct stream is selected
if (dashboardPanelData.meta.parsedQuery.from?.length > 0) {
console.log("---parsedQuery.from--------",dashboardPanelData.meta.parsedQuery.from);
const streamFound = dashboardPanelData.meta.stream.streamResults.find(it => it.name == dashboardPanelData.meta.parsedQuery.from[0].table)
if(streamFound) {
if(dashboardPanelData.data.fields.stream != streamFound.name) {
dashboardPanelData.data.fields.stream = streamFound.name
}
} else {
data.queryErrors.push("Invalid stream")
}
// if (dashboardPanelData.meta.parsedQuery.from[0].table !== dashboardPanelData.data.fields.stream) {
// let streamFound = false;
// const streamNameFromQuery = dashboardPanelData.meta.parsedQuery.from[0].table;
// dashboardPanelData.meta.stream.streamResults.forEach((stream) => {
// console.log("Query: get stream data from streamResults", stream);
// if (stream.name == streamNameFromQuery) {
// streamFound = true;
// let itemObj = {
// label: stream.name,
// value: stream.name,
// };
// dashboardPanelData.data.stream.selectedStream = itemObj;
// stream.schema.forEach((field) => {
// dashboardPanelData.meta.stream.selectedStreamFields.push({
// name: field.name,
// });
// });
// }
// });
// if (streamFound == false) {
// dashboardPanelData.data.stream.selectedStream = {};
// dashboardPanelData.meta.stream.selectedStreamFields = [];
// $q.notify({
// message: "Stream not found",
// color: "negative",
// position: "top",
// timeout: 2000,
// });
// }
// }
} else {
data.queryErrors.push("Stream name required")
}
}
};
return {
t,
router,
updateQueryValue,
onDropDownClick,
showQuery,
dashboardPanelData,
data
};
},
});
</script>
@@ -0,0 +1,369 @@
<template>
<div v-if="props.data.type == 'table'" class="q-pa-md">
<q-table
class="my-sticky-virtscroll-table"
virtual-scroll
v-model:pagination="pagination"
:rows-per-page-options="[0]"
:virtual-scroll-sticky-size-start="48"
dense
:rows="searchQueryData?.data || []"
row-key="id"
>
</q-table>
</div>
<div v-else>
<div ref="plotRef" :id="chartID" class="plotlycontainer"></div>
</div>
</template>
<script lang="ts">
import {
defineComponent,
onMounted,
onUpdated,
ref,
reactive,
nextTick,
watch,
computed,
onActivated,
} from "vue";
import { useStore } from "vuex";
import { useQuasar, date } from "quasar";
import queryService from "../../../services/nativequery";
import Plotly from "plotly.js";
export default defineComponent({
name: "ChartRender",
props: ["data", "selectedTimeDate"],
setup(props) {
const $q = useQuasar();
const store = useStore();
const searchQueryData = reactive({
data: [],
});
onUpdated(() => {
console.log("updated");
if (props.data.type != "table") {
renderChart();
}
});
const plotRef: any = ref(null);
const chartID = ref("chart1");
let selectedTimeObj = computed(function () {
return props.selectedTimeDate;
});
// If query changes, we need to get the data again and rerender the chart
watch(
() => [props.data, props.selectedTimeDate],
() => {
if (props.data.query) {
fetchQueryData();
} else {
Plotly.react(
plotRef.value,
[],
{},
{
responsive: true,
displaylogo: false,
displayModeBar: false,
}
);
}
},
{ deep: true }
);
// just wait till the component is mounted and then create a plotly instance
onMounted(async () => {
await nextTick();
if (props.data.type != "table") {
await Plotly.newPlot(
plotRef.value,
[{}],
{},
{
responsive: true,
displaylogo: false,
displayModeBar: false,
}
);
}
if (props.data.query) {
fetchQueryData();
}
});
// wrap the text for long x axis names
const addBreaksAtLength = 12;
const textwrapper = function (traces: any) {
traces = traces.map((text: any) => {
let rxp = new RegExp(".{1," + addBreaksAtLength + "}", "g");
if (text) {
return text?.match(rxp)?.join("<br>");
} else {
return " ";
}
});
return traces;
};
// Chart Related Functions
const fetchQueryData = async () => {
const queryData = props.data.query;
const chartParams = {
title: "Found " + "2" + " hits in " + "10" + " ms",
};
const sqlQueryModified = queryData;
// get query object
// TODO: update queryData to sqlQueryModified
const timestamps = selectedTimeObj.value;
let startISOTimestamp: any;
let endISOTimestamp: any;
if (
timestamps.start_time != "Invalid Date" &&
timestamps.end_time != "Invalid Date"
) {
startISOTimestamp =
new Date(timestamps.start_time.toISOString()).getTime() * 1000;
endISOTimestamp =
new Date(timestamps.end_time.toISOString()).getTime() * 1000;
}
const query = {
query: {
sql: queryData,
sql_mode: "full",
start_time: startISOTimestamp,
end_time: endISOTimestamp,
},
};
await queryService
.runquery(query, store.state.selectedOrganization.identifier)
.then((res) => {
searchQueryData.data = res.data.hits;
$q.notify({
type: "positive",
message: "Query applied successfully.",
timeout: 5000,
});
})
.catch((error) => {
$q.notify({
type: "negative",
message: "Something went wrong!",
timeout: 5000,
});
});
};
// If data or chart type is updated, rerender the chart
watch(
() => [searchQueryData.data, props.data.type],
() => {
console.log("Query: new data received");
if (props.data.type != "table") {
renderChart();
}
},
{ deep: true }
);
const renderChart = async () => {
console.log("Query: rendering chart");
console.log("Query: chart type", props.data.type);
// Step 1: Get the Y-Axis Count
const xAxisKey = getXAxisKey();
const yAxisKeys = getYAxisKeys();
console.log("xaxis=", textwrapper(getAxisDataFromKey(xAxisKey)));
let traces;
traces = yAxisKeys.map((key: any) => {
const trace = {
name: props.data.fields?.y.find((it: any) => it.label == key).column,
...getTraceValuesByChartType(xAxisKey, key),
showlegend: props.data.config?.show_legends,
marker: {
color:
props.data.fields?.y.find((it: any) => it.label == key).color ||
"#5960b2",
opacity: 0.8,
},
};
return trace;
});
console.log("Query: populating traces: ", traces);
const layout: any = {
title: false,
showlegend: props.data.config?.show_legends,
font: { size: 12 },
autosize: true,
legend: {
bgcolor: "#f7f7f7",
},
xaxis: {
tickangle: -20,
automargin: true,
},
yaxis: {
automargin: true,
},
margin: {
l: 32,
r: 16,
t: 38,
b: 32,
},
...getPropsByChartTypeForLayout(),
};
Plotly.react(plotRef.value, traces, layout, {
responsive: true,
displaylogo: false,
displayModeBar: false,
});
};
const getTraceValuesByChartType = (xAxisKey: string, yAxisKey: string) => {
const trace: any = {
...getPropsByChartTypeForTraces(),
};
if (props.data.type == "pie") {
trace["labels"] = textwrapper(getAxisDataFromKey(xAxisKey));
trace["values"] = getAxisDataFromKey(yAxisKey);
} else if (props.data.type == "h-bar") {
trace["y"] = textwrapper(getAxisDataFromKey(xAxisKey));
trace["x"] = getAxisDataFromKey(yAxisKey);
} else {
trace["x"] = textwrapper(getAxisDataFromKey(xAxisKey));
trace["y"] = getAxisDataFromKey(yAxisKey);
}
return trace;
};
const getXAxisKey = () => {
return props.data.fields.x.map((it: any) => it.label)[0];
};
const getYAxisKeys = () => {
return props.data.fields.y.map((it: any) => it.label);
};
const getAxisDataFromKey = (key: string) => {
return searchQueryData.data.map((item) => item[key]);
};
const getPropsByChartTypeForTraces = () => {
switch (props.data.type) {
case "bar":
return {
type: "bar",
};
case "line":
return {
mode: "lines",
};
case "scatter":
return {
mode: "markers",
};
case "pie":
return {
type: "pie",
};
case "h-bar":
return {
type: "bar",
orientation: "h",
};
case "area":
return {
fill: "tozeroy", //TODO: hoe to change the color of plot chart
type: "scatter",
};
default:
return {
type: "bar",
};
}
};
const getPropsByChartTypeForLayout = async () => {
switch (props.data.type) {
case "bar":
return {
barmode: "group",
};
case "line":
return {};
case "scatter":
return {
scattermode: "group",
};
case "pie":
return {};
case "h-bar":
return {
barmode: "group",
};
case "area":
return {};
default:
return {};
}
};
return {
plotRef,
props,
searchQueryData,
pagination: ref({
rowsPerPage: 0,
}),
chartID,
};
},
});
</script>
<style lang="scss" scoped>
.my-sticky-virtscroll-table {
/* height or max-height is important */
height: 410px;
.q-table__top,
.q-table__bottom,
thead tr:first-child th {
/* bg color is important for th; just specify one */
background-color: #fff;
}
thead tr th {
position: sticky;
z-index: 1;
}
/* this will be the loading indicator */
thead tr:last-child th {
/* height of all previous header rows */
top: 48px;
}
thead tr:first-child th {
top: 0;
}
}
</style>
@@ -0,0 +1,90 @@
<template>
<div style="height: 100%">
<div class="q-pa-none" style="max-width: 75px">
<q-list separator>
<q-item
:class="
selectedChartType === item.id ? 'q-pa-none bg-grey-3' : 'q-pa-none'
"
v-for="(item, index) in ChartsArray"
:key="index"
clickable
v-ripple
@click="$emit('update:selectedChartType', item.id)"
>
<q-item-section class="q-pa-sm">
<q-icon
size="sm"
color="primary"
:name="item.image"
class="q-mx-auto q-mb-sm"
/>
<q-item-label class="q-pa-none q-mx-auto" caption>{{
item.title
}}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import { getImageURL } from "../../../utils/zincutils";
export default defineComponent({
name: "ChartSelection",
props: ["selectedChartType"],
emits: ["update:selectedChartType"],
setup() {
const chartsArray = ref([
{
image: "img:" + getImageURL("images/dashboard/charts/area-graph.png"),
title: "Area",
id: "area",
},
{
image: "img:" + getImageURL("images/dashboard/charts/bar-chart.png"),
title: "Bar",
id: "bar",
},
{
image: "img:" + getImageURL("images/dashboard/charts/bar-graph.png"),
title: "Horizontal",
id: "h-bar",
},
{
image: "img:" + getImageURL("images/dashboard/charts/line-chart.png"),
title: "Line",
id: "line",
},
{
image: "img:" + getImageURL("images/dashboard/charts/pie-chart.png"),
title: "Pie",
id: "pie",
},
{
image:
"img:" + getImageURL("images/dashboard/charts/scatter-graph.png"),
title: "Scatter",
id: "scatter",
},
{
image: "img:" + getImageURL("images/dashboard/charts/table.png"),
title: "Table",
id: "table",
},
]);
return {
ChartsArray: chartsArray,
};
},
components: {},
});
</script>
<style lang="sass" scoped>
</style>
@@ -0,0 +1,59 @@
<template>
<div class="col" style="height: 100%">
<div class="q-pa-sm">
<span class="text-weight-bold">Fields</span>
</div>
<div
class="scroll"
style="width: 100%;"
v-if="true"
>
<index-list/>
</div>
<!-- <div v-else-if="dashboardPanelData.meta.stream.loading == true">
<q-spinner-dots
color="primary"
size="40px"
style="margin: 0 auto; display: block"
/>
</div> -->
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from "vue"
import { useQuasar, date } from "quasar";
import { useStore } from "vuex";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import IndexList from "./IndexList.vue"
import useDashboardPanelData from "../../../composables/useDashboardPanel";
export default defineComponent({
name: "GetFields",
props:[],
components: {
IndexList,
},
setup(){
const store = useStore();
const router = useRouter();
const $q = useQuasar();
const { t } = useI18n();
const {dashboardPanelData} = useDashboardPanelData()
return {
store,
router,
dashboardPanelData
}
}
})
</script>
<style lang="sass" scoped>
</style>
@@ -0,0 +1,515 @@
<template>
<div class="column index-menu">
<div>
<q-select
v-model="dashboardPanelData.data.fields.stream"
:label="
dashboardPanelData.data.fields.stream ? '' : t('search.selectIndex')
"
:options="data.indexOptions"
data-cy="index-dropdown"
input-debounce="0"
behavior="menu"
filled
borderless
dense
>
<template #no-option>
<q-item>
<q-item-section> {{ t("search.noResult") }}</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="index-table q-mt-xs">
<q-table
:columns="[
{
name: 'name',
field: 'name',
align: 'left',
label: 'Field',
sortable: true,
},
]"
:rows="data.currentFieldsList"
row-key="name"
:filter="dashboardPanelData.meta.stream.filterField"
:filter-method="filterFieldFn"
:pagination="{ rowsPerPage: 10000 }"
hide-header
hide-bottom
id="fieldList"
>
<template #body-cell-name="props">
<q-tr :props="props">
<q-td
class="field_list"
:class="
dashboardPanelData.data.fields.x.find((it: any)=>it.column === props.row.name) ||
dashboardPanelData.data.fields.y.find((it: any)=>it.column === props.row.name) ||
dashboardPanelData.data.fields.filter.find((it: any)=>it.column === props.row.name)
? 'selected'
: ''
"
:props="props"
>
<div class="field_overlay">
<div class="field_label">
{{ props.row.name }}
</div>
<div class="field_icons">
<q-btn
color="white"
padding="sm"
text-color="black"
@click="addXAxisItem(props.row.name)"
>
<div>+X</div>
</q-btn>
<q-btn
color="white"
padding="sm"
text-color="black"
@click="addYAxisItem(props.row.name)"
>
<div>+Y</div>
</q-btn>
<q-btn
color="white"
padding="sm"
text-color="black"
@click="addFilteredItem(props.row.name)"
>
<div>+F</div>
</q-btn>
</div>
</div>
</q-td>
</q-tr>
</template>
<template #top-right>
<q-input
v-model="dashboardPanelData.meta.stream.filterField"
data-cy="index-field-search-input"
filled
borderless
dense
clearable
debounce="1"
:placeholder="t('search.searchField')"
>
<template #prepend>
<q-icon name="search" />
</template>
</q-input>
</template>
</q-table>
</div>
</div>
</template>
<script lang="ts">
import {
defineComponent,
reactive,
ref,
onMounted,
computed,
watch,
onActivated,
} from "vue";
import { useI18n } from "vue-i18n";
import { useStore } from "vuex";
import { useQuasar } from "quasar";
import { useRouter } from "vue-router";
import useDashboardPanelData from "../../../composables/useDashboardPanel";
import IndexService from "../../../services/index";
import queryService from "../../../services/nativequery";
export default defineComponent({
name: "ComponentSearchIndexSelect",
props: ["selectedXAxisValue", "selectedYAxisValue"],
emits: ["update:selectedXAxisValue", "update:selectedYAxisValue"],
setup() {
const store = useStore();
const router = useRouter();
const { t } = useI18n();
const data = reactive({
schemaList: [],
indexOptions: [],
currentFieldsList: [],
});
const $q = useQuasar();
const { dashboardPanelData, addXAxisItem, addYAxisItem } =
useDashboardPanelData();
const x = ref();
const y = ref();
console.log("schemaList:", data.schemaList);
// console.log("schemaList name:", schemaList.data );
console.log("indexOptions:", data.indexOptions);
onActivated(() => {
console.log("inside mounted");
getStreamList();
});
// update the selected stream fields list
watch(
() => [data.schemaList, dashboardPanelData.data.fields.stream],
() => {
const fields: any = data.schemaList.find(
(it: any) => it.name == dashboardPanelData.data.fields.stream
);
dashboardPanelData.meta.stream.selectedStreamFields =
fields?.schema || [];
}
);
// update the current list fields if any of the lists changes
watch(
() => [
dashboardPanelData.meta.stream.selectedStreamFields,
dashboardPanelData.meta.stream.customQueryFields,
],
() => {
console.log("updated custom query fields or selected stream fields");
data.currentFieldsList = [];
data.currentFieldsList = [
...dashboardPanelData.meta.stream.customQueryFields,
...dashboardPanelData.meta.stream.selectedStreamFields,
];
}
);
// get the stream list by making an API call
const getStreamList = () => {
IndexService.nameList(
store.state.selectedOrganization.identifier,
"",
true
).then((res) => {
data.schemaList = res.data.list;
dashboardPanelData.meta.stream.streamResults = res.data.list;
data.indexOptions = res.data.list.map((data: any) => {
return data.name;
});
// set the first stream as the selected stream when the api loads the data
if (
!dashboardPanelData.data.fields.stream &&
data.indexOptions.length > 0
) {
dashboardPanelData.data.fields.stream = data.indexOptions[0];
}
});
};
const filterFieldFn = (rows: any, terms: any) => {
var filtered = [];
if (terms != "") {
terms = terms.toLowerCase();
for (var i = 0; i < rows.length; i++) {
if (rows[i]["name"].toLowerCase().includes(terms)) {
filtered.push(rows[i]);
}
}
}
return filtered;
};
const addFilteredItem = (name: string) => {
console.log("name=", name);
if (!dashboardPanelData.data.fields.filter) {
dashboardPanelData.data.fields.filter = [];
}
if (
!dashboardPanelData.data.fields.filter.find(
(it: any) => it.column == name
)
) {
console.log("data");
dashboardPanelData.data.fields.filter.push({
type: "list",
values: [],
column: name,
operator: null,
value: null,
});
}
if (!dashboardPanelData.meta.filterValue) {
dashboardPanelData.meta.filterValue = [];
}
// dashboardPanelData.meta.filterValue.push({
// column: name,
// value: ["abc", "def"]
// })
console.log("filterValue array= ", dashboardPanelData.meta.filterValue);
if (
!dashboardPanelData.meta.filterValue.find(
(it: any) => it.column == name
)
) {
let queryData = "SELECT ";
// get unique value of the selected fields
queryData += `${name} as value`;
//now add the selected stream
queryData += ` FROM '${dashboardPanelData.data.fields.stream}'`;
console.log("queryData= ", queryData);
// add group by statement
queryData += ` GROUP BY value`;
const query = {
query: { sql: queryData, sql_mode: "full" },
};
queryService
.runquery(query, store.state.selectedOrganization.identifier)
.then((res) => {
console.log(
"-distinct vals--",
res.data.hits.map((it: any) => it.value).filter((it: any) => it)
);
dashboardPanelData.meta.filterValue.push({
column: name,
value: res.data.hits
.map((it: any) => it.value)
.filter((it: any) => it),
});
// dashboardPanelData.data.fields.filter.find((it:any) => it.column == name).values = res.data.hits.map((it:any)=>it.value)
// $q.notify({
// type: "positive",
// message: "Query applied successfully.",
// timeout: 5000,
// });
})
.catch((error) => {
$q.notify({
type: "negative",
message: "Something went wrong!",
timeout: 5000,
});
});
}
};
return {
t,
store,
router,
filterFieldFn,
addXAxisItem,
addYAxisItem,
addFilteredItem,
data,
getStreamList,
dashboardPanelData,
};
},
});
</script>
<style lang="scss" scoped>
.q-menu {
box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.1);
transform: translateY(0.5rem);
border-radius: 0px;
.q-virtual-scroll__content {
padding: 0.5rem;
}
}
.index-menu {
width: 100%;
height: 100%;
.q-field {
&__control {
height: 35px;
padding: 0px 5px;
min-height: auto !important;
&-container {
padding-top: 0px !important;
}
}
&__native :first-of-type {
padding-top: 0.25rem;
}
}
.q-select {
text-transform: capitalize;
}
.index-table {
width: 100%;
// border: 1px solid rgba(0, 0, 0, 0.02);
.q-table {
display: block;
}
tr {
margin-bottom: 1px;
}
tbody,
tr,
td {
width: 100%;
display: block;
height: 25px;
}
.q-table__top {
padding: 0px;
}
.q-table__control,
label.q-field {
width: 100%;
}
.q-table thead tr,
.q-table tbody td {
height: auto;
}
.q-table__top {
border-bottom: unset;
}
}
.field-table {
width: 100%;
}
.field_list {
padding: 0px;
margin-bottom: 0.125rem;
position: relative;
overflow: visible;
cursor: default;
.field_overlay {
justify-content: space-between;
background-color: transparent;
transition: all 0.3s ease;
padding: 0px 10px;
align-items: center;
position: absolute;
line-height: 2rem;
overflow: hidden;
inset: 0;
display: flex;
z-index: 1;
width: 100%;
border-radius: 0px;
height: 25px;
.field_icons {
padding: 0 0 0 0.25rem;
transition: all 0.3s ease;
background-color: white;
position: absolute;
z-index: 3;
opacity: 0;
right: 0;
.q-icon {
cursor: pointer;
}
}
.field_label {
pointer-events: none;
font-size: 0.825rem;
position: relative;
display: inline;
z-index: 2;
left: 0;
// text-transform: capitalize;
}
}
&.selected {
.field_overlay {
background-color: rgba(89, 96, 178, 0.3);
.field_icons {
opacity: 0;
}
}
&:hover {
.field_overlay {
box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.17);
background-color: white;
.field_icons {
background-color: white;
}
}
}
}
&:hover {
.field_overlay {
box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.17);
.field_icons {
background-color: white;
opacity: 1;
}
}
}
}
}
.q-item {
color: $dark-page;
min-height: 1.3rem;
padding: 5px 10px;
&__label {
font-size: 0.75rem;
}
&.q-manual-focusable--focused > .q-focus-helper {
background: none !important;
opacity: 0.3 !important;
}
&.q-manual-focusable--focused > .q-focus-helper,
&--active {
background-color: $selected-list-bg !important;
}
&.q-manual-focusable--focused > .q-focus-helper,
&:hover,
&--active {
color: $primary;
}
}
.q-field--dense .q-field__before,
.q-field--dense .q-field__prepend {
padding: 0px 0px 0px 0px;
height: auto;
line-height: auto;
}
.q-field__native,
.q-field__input {
padding: 0px 0px 0px 0px;
}
.q-field--dense .q-field__label {
top: 5px;
}
.q-field--dense .q-field__control,
.q-field--dense .q-field__marginal {
height: 34px;
}
</style>
@@ -0,0 +1,701 @@
<template>
<div>
<div class="q-pa-sm">
<span class="text-weight-bold">Layout</span>
</div>
<q-separator />
<div>
<q-expansion-item
default-opened
dense
:label="
dashboardPanelData.data.type == 'table'
? t(`First Column`)
: dashboardPanelData.data.type == 'h-bar'
? t(`Y-Axis (Columns)`)
: t(`X-Axis (Row)`)
"
>
<div class="column index-menu q-mb-md">
<div class="index-table q-mt-xs">
<q-table
v-model:selected="dashboardPanelData.data.fields.x"
:columns="[
{
name: 'column',
field: 'column',
align: 'left',
label: 'Field',
},
]"
:rows="dashboardPanelData.data.fields.x"
row-key="name"
class="field-table"
id="fieldList"
:hide-pagination="true"
hide-header
hide-bottom
>
<template #body-cell-column="props">
<q-tr :props="props">
<q-td :props="props" class="field_list">
<div class="field_overlay">
<div class="field_label">
{{ props.row.column }}
</div>
<div class="field_icons">
<q-icon
:name="
'img:' +
getImageURL('images/layout/remove_icon.svg')
"
size="1rem"
@click="removeXAxisItem(props.row.column)"
/>
</div>
</div>
</q-td>
</q-tr>
</template>
</q-table>
</div>
<div
class="text-caption text-weight-bold text-center q-ma-sm"
v-if="dashboardPanelData.data.fields.x.length < 1"
>
Please add a field from the list
</div>
</div>
</q-expansion-item>
<q-separator />
<q-expansion-item
dense
default-opened
:label="
dashboardPanelData.data.type == 'table'
? t(`Other Columns`)
: dashboardPanelData.data.type == 'h-bar'
? t(`X-Axis (Row)`)
: t(`Y-Axis (Columns)`)
"
>
<div class="column index-menu q-mb-lg">
<div class="index-table q-mt-xs">
<q-table
v-model:selected="dashboardPanelData.data.fields.y"
:columns="[
{
name: 'column',
field: 'column',
align: 'left',
label: 'Field',
},
]"
:rows="dashboardPanelData.data.fields.y"
row-key="column"
class="field-table"
id="fieldList"
virtual-scroll
v-model:pagination="pagination"
:rows-per-page-options="[0]"
:virtual-scroll-sticky-size-start="48"
:hide-pagination="true"
hide-header
hide-bottom
>
<template #body-cell-column="props">
<q-tr :props="props">
<q-td :props="props" class="field_list">
<div class="field_overlay" :title="props.row.column">
<div class="field_label">
{{ props.row.column }}
<!-- <div>
<q-expansion-item
expand-separator
icon="perm_identity"
label=""
caption=""
>
<div class="color-input-wrapper">
<input type="color" v-model="dashboardPanelData.data.fields.y.color">
</div>
<div>
<q-select
v-model="dashboardPanelData.data.fields.y.aggregationFunction"
:options="triggerOperators"
dense
filled
:label="t('Aggregation')"
></q-select>
</div>
</q-expansion-item>
</div> -->
</div>
<div class="field_icons">
<!-- TODO: aggregation and color picker and label fields -->
<div>
<q-btn
size="xs"
color="red"
round
dense
@click="props.expand = !props.expand"
:icon="props.expand ? 'remove' : 'add'"
/>
<q-icon
:name="
'img:' +
getImageURL('images/layout/remove_icon.svg')
"
size="1rem"
@click="removeYAxisItem(props.row.column)"
/>
</div>
</div>
</div>
</q-td>
</q-tr>
<q-tr
v-show="props.expand"
:props="props"
style="height: min-content"
>
<q-td colspan="100%">
<div>
<div class="flex items-center">
<div class="q-pa-xs" style="width: 180px">
<q-select
v-model="
dashboardPanelData.data.fields.y[props.pageIndex]
.aggregationFunction
"
:options="triggerOperators"
dense
filled
label="Aggregation"
></q-select>
</div>
<div class="color-input-wrapper">
<input
type="color"
v-model="
dashboardPanelData.data.fields.y[props.pageIndex]
.color
"
/>
</div>
</div>
<q-input
dense
filled
label="Label"
v-model="
dashboardPanelData.data.fields.y[props.pageIndex]
.label
"
/>
</div>
</q-td>
</q-tr>
</template>
</q-table>
</div>
<div
class="text-caption text-weight-bold text-center q-ma-sm"
v-if="dashboardPanelData.data.fields.y.length < 1"
>
Please add a field from the list
</div>
</div>
</q-expansion-item>
<q-separator />
<q-expansion-item default-opened dense :label="t(`Config`)">
<div>
<q-toggle
v-model="dashboardPanelData.data.config.show_legends"
label="Show Legends"
/>
<q-form ref="" class="q-pa-sm">
<q-input
v-model="dashboardPanelData.data.config.title"
:label="t('panel.name')"
class="q-py-md showLabelOnTop"
stack-label
filled
dense
/>
<q-input
v-model="dashboardPanelData.data.config.description"
type="textarea"
:label="t('panel.typeDesc')"
class="q-py-md showLabelOnTop"
stack-label
filled
dense
/>
</q-form>
</div>
</q-expansion-item>
</div>
<q-separator />
<q-expansion-item default-opened dense :label="t(`Filters`)">
<div class="column index-menu q-mb-lg">
<div class="index-table q-mt-xs">
<q-table
v-model:selected="dashboardPanelData.data.fields.filter"
:columns="[
{
name: 'column',
field: 'column',
align: 'left',
label: 'Filter',
},
]"
:rows="dashboardPanelData.data.fields.filter"
row-key="column"
class="field-table"
id="fieldList"
virtual-scroll
v-model:pagination="pagination"
:rows-per-page-options="[0]"
:virtual-scroll-sticky-size-start="48"
:hide-pagination="true"
hide-header
hide-bottom
>
<template #body-cell-column="props">
<q-tr :props="props">
<q-td :props="props" class="field_list">
<div class="field_overlay" :title="props.row.column">
<div class="field_label">
{{ props.row.column }}
</div>
<div class="field_icons">
<div>
<q-btn
size="xs"
color="red"
round
dense
@click="props.expand = !props.expand"
:icon="props.expand ? 'remove' : 'add'"
/>
<q-icon
:name="
'img:' +
getImageURL('images/layout/remove_icon.svg')
"
size="1rem"
@click="removeFilterItem(props.row.column)"
/>
</div>
</div>
</div>
</q-td>
</q-tr>
<q-tr
v-show="props.expand"
:props="props"
style="height: min-content"
>
<q-td colspan="100%">
<div class="q-pa-xs">
<div class="q-gutter-xs">
<q-tabs
v-model="
dashboardPanelData.data.fields.filter[props.pageIndex]
.type
"
>
<q-tab
name="list"
label="List"
style="width: auto"
></q-tab>
<q-tab
name="condition"
label="Condition"
style="width: auto"
></q-tab>
</q-tabs>
<q-separator></q-separator>
<q-tab-panels
dense
v-model="
dashboardPanelData.data.fields.filter[props.pageIndex]
.type
"
animated
style="background-color: #f5f5f5"
>
<q-tab-panel dense name="condition">
<div class="flex justify-between">
<q-select
dense
filled
v-model="
dashboardPanelData.data.fields.filter[
props.pageIndex
].operator
"
:options="options"
label="Operator"
style="width: 100%"
/>
<q-input
dense
filled
v-model="
dashboardPanelData.data.fields.filter[
props.pageIndex
].value
"
label="Value"
style="width: 100%; margin-top: 5px"
/>
</div>
</q-tab-panel>
<q-tab-panel dense name="list">
<q-select
dense
filled
v-model="
dashboardPanelData.data.fields.filter[
props.pageIndex
].values
"
:options="dashboardPanelData.meta.filterValue.find((it: any)=>it.column == props.row.column)?.value"
label="Select Filter"
multiple
emit-value
map-options
>
<template v-slot:selected>
{{
dashboardPanelData.data.fields.filter[
props.pageIndex
].values[0]?.length > 15
? dashboardPanelData.data.fields.filter[
props.pageIndex
].values[0]?.substring(0, 15) + "..."
: dashboardPanelData.data.fields.filter[
props.pageIndex
].values[0]
}}
{{
dashboardPanelData.data.fields.filter[
props.pageIndex
].values?.length > 1
? " +" +
(dashboardPanelData.data.fields.filter[
props.pageIndex
].values?.length -
1)
: ""
}}
</template>
<template
v-slot:option="{
itemProps,
opt,
selected,
toggleOption,
}"
>
<q-item v-bind="itemProps">
<q-item-section side>
<q-checkbox
dense
:model-value="selected"
@update:model-value="toggleOption(opt)"
></q-checkbox>
</q-item-section>
<q-item-section>
<div v-html="opt"></div>
</q-item-section>
</q-item>
</template>
</q-select>
</q-tab-panel>
</q-tab-panels>
</div>
</div>
</q-td>
</q-tr>
</template>
</q-table>
</div>
<div
class="text-caption text-weight-bold text-center q-ma-sm"
v-if="dashboardPanelData.meta.filterValue.length < 1"
>
Please add a field from the list
</div>
</div>
<div></div>
</q-expansion-item>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useQuasar } from "quasar";
import useDashboardPanelData from "../../../composables/useDashboardPanel";
import { getImageURL } from "../../../utils/zincutils";
export default defineComponent({
name: "dashboard-layout",
components: {},
setup() {
const showXAxis = ref(true);
const panelName = ref("");
const panelDesc = ref("");
const { t } = useI18n();
const $q = useQuasar();
const {
dashboardPanelData,
removeXAxisItem,
removeYAxisItem,
removeFilterItem,
} = useDashboardPanelData();
const triggerOperators: any = ref(["count", "sum", "avg", "min", "max"]);
return {
showXAxis,
t,
panelName,
panelDesc,
dashboardPanelData,
removeXAxisItem,
removeYAxisItem,
triggerOperators,
removeFilterItem,
pagination: ref({
rowsPerPage: 0,
}),
model: ref([]),
tab: ref("General"),
options: ["=", "<>", ">=", "<=", ">", "<", "Contains", "Not Contains"],
getImageURL,
};
},
});
</script>
<style lang="scss" scoped>
.color-input-wrapper {
height: 1.5em;
width: 1.5em;
overflow: hidden;
border-radius: 50%;
display: inline-flex;
align-items: center;
position: relative;
}
.color-input-wrapper input[type="color"] {
position: absolute;
height: 4em;
width: 4em;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
overflow: hidden;
border: none;
margin: 0;
padding: 0;
}
.q-menu {
box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.1);
transform: translateY(0.5rem);
border-radius: 0px;
.q-virtual-scroll__content {
padding: 0.5rem;
}
}
.index-menu {
width: 100%;
.q-field {
&__control {
height: 35px;
padding: 0px 5px;
min-height: auto !important;
&-container {
padding-top: 0px !important;
}
}
&__native :first-of-type {
padding-top: 0.25rem;
}
}
.q-select {
text-transform: capitalize;
}
.index-table {
width: 100%;
// border: 1px solid rgba(0, 0, 0, 0.02);
.q-table {
display: block;
}
tr {
margin-bottom: 1px;
}
tbody,
tr,
td {
width: 100%;
display: block;
height: 25px;
}
.q-table__top {
padding: 0px;
}
.q-table__control,
label.q-field {
width: 100%;
}
.q-table thead tr,
.q-table tbody td {
height: auto;
}
.q-table__top {
border-bottom: unset;
}
}
.field-table {
width: 100%;
}
.field_list {
padding: 0px;
margin-bottom: 0.125rem;
position: relative;
overflow: visible;
cursor: default;
.field_overlay {
justify-content: space-between;
background-color: transparent;
transition: all 0.3s ease;
padding: 0px 10px;
align-items: center;
position: absolute;
line-height: 2rem;
overflow: hidden;
inset: 0;
display: flex;
z-index: 1;
width: 100%;
border-radius: 0px;
height: 25px;
.field_icons {
padding: 0 0.625rem 0 0.25rem;
transition: all 0.3s ease;
background-color: white;
position: absolute;
z-index: 3;
opacity: 0;
right: 0;
.q-icon {
cursor: pointer;
}
}
.field_label {
pointer-events: none;
font-size: 0.825rem;
position: relative;
display: inline;
z-index: 2;
left: 0;
// text-transform: capitalize;
}
}
&.selected {
.field_overlay {
background-color: rgba(89, 96, 178, 0.3);
.field_icons {
opacity: 0;
}
}
&:hover {
.field_overlay {
box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.17);
background-color: white;
.field_icons {
background-color: white;
}
}
}
}
&:hover {
.field_overlay {
box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.17);
.field_icons {
background-color: white;
opacity: 1;
}
}
}
}
}
.q-item {
color: $dark-page;
min-height: 1.3rem;
padding: 5px 10px;
&__label {
font-size: 0.75rem;
}
&.q-manual-focusable--focused > .q-focus-helper {
background: none !important;
opacity: 0.3 !important;
}
&.q-manual-focusable--focused > .q-focus-helper,
&--active {
background-color: $selected-list-bg !important;
}
&.q-manual-focusable--focused > .q-focus-helper,
&:hover,
&--active {
color: $primary;
}
}
.q-field--dense .q-field__before,
.q-field--dense .q-field__prepend {
padding: 0px 0px 0px 0px;
height: auto;
line-height: auto;
}
.q-field__native,
.q-field__input {
padding: 0px 0px 0px 0px;
}
.q-field--dense .q-field__label {
top: 5px;
}
.q-field--dense .q-field__control,
.q-field--dense .q-field__marginal {
height: 34px;
}
</style>
+23 -41
View File
@@ -20,7 +20,7 @@
<script lang="ts">
import Plotly from "plotly.js";
import { defineComponent, onMounted, ref, onUpdated } from "vue";
import { defineComponent, onMounted, ref, onUpdated, nextTick } from "vue";
export default defineComponent({
name: "ReactiveChart",
@@ -34,6 +34,7 @@ export default defineComponent({
y: [],
unparsed_x: [],
name: "barchart",
// showlegend: true,
type: "bar",
marker: {
color: "#5960b2",
@@ -51,25 +52,23 @@ export default defineComponent({
font: { size: 12 },
autosize: true,
legend: {
bgcolor: "red",
},
margin: {
l: 32,
r: 16,
t: 38,
b: 32,
bgcolor: "#f7f7f7",
},
xaxis: {
ticklen: 5,
nticks: 15,
tickangle: -20,
automargin: true,
},
yaxis: {
automargin: true,
},
};
onMounted(async () => {
await nextTick();
await Plotly.newPlot(plotref.value, [trace], layout, {
responsive: true,
displaylogo: false,
displayModeBar: false,
displayModeBar: true,
});
});
@@ -80,6 +79,16 @@ export default defineComponent({
}
});
// wrap the text for long x axis names
const addBreaksAtLength = 12;
const textwrapper = function (traces: any) {
traces = traces.map((text: any) => {
let rxp = new RegExp(".{1," + addBreaksAtLength + "}", "g");
return text.match(rxp).join("<br>");
});
return traces;
};
onUpdated(async () => {
if (props.data) {
chartID.value = "chart_" + props.data.id;
@@ -91,7 +100,7 @@ export default defineComponent({
y: any,
params: { title: any; unparsed_x_data: any }
) => {
trace.x = x;
trace.x = textwrapper(x);
trace.y = y;
// layout.title.text = params.title;
trace.unparsed_x = params.unparsed_x_data;
@@ -99,43 +108,16 @@ export default defineComponent({
forceReLayout();
};
// created force relayout function to avoid infinite loop
const forceReLayout = (flag = true) => {
const forceReLayout = async (flag = true) => {
zoomFlag.value = flag;
const update: any = {
"xaxis.autorange": true,
"yaxis.autorange": true,
};
await nextTick();
Plotly.relayout(plotref.value, update);
};
const onPlotZoom = () => {
if (
plotref.value.layout.xaxis.range.length == 2 &&
zoomFlag.value == false
) {
const start = Math.round(plotref.value.layout.xaxis.range[0]);
const end = Math.round(plotref.value.layout.xaxis.range[1]);
if (
start >= 0 &&
end >= 0 &&
trace.unparsed_x[start] != undefined &&
trace.unparsed_x[end] != undefined
) {
zoomFlag.value = true;
let start_d = new Date(Date.parse(trace.unparsed_x[start] + "Z"));
let end_d = new Date(Date.parse(trace.unparsed_x[end] + "Z"));
emit("updated:chart", {
start: start_d.toLocaleString("sv-SE"),
end: end_d.toLocaleString("sv-SE"),
});
}
} else {
zoomFlag.value = false;
}
};
return {
plotref,
reDraw,
+24 -5
View File
@@ -20,7 +20,7 @@
<script lang="ts">
import Plotly from "plotly.js";
import { defineComponent, onMounted, ref, onUpdated } from "vue";
import { defineComponent, onMounted, ref, onUpdated, nextTick } from "vue";
export default defineComponent({
name: "ReactiveLineChart",
@@ -34,6 +34,7 @@ export default defineComponent({
y: [],
unparsed_x: [],
mode: "lines",
showlegend: true,
name: "Markers",
};
const chartID = ref("");
@@ -47,15 +48,22 @@ export default defineComponent({
},
font: { size: 12 },
autosize: true,
xaxis: {
automargin: true,
tickangle: -20,
},
yaxis: {
tickangle: 0,
},
legend: {
bgcolor: "red",
bgcolor: "#f7f7f7",
},
};
onMounted(async () => {
await Plotly.newPlot(plotref.value, [trace], layout, {
responsive: true,
displayModeBar: false,
displayModeBar: true,
});
});
@@ -66,12 +74,22 @@ export default defineComponent({
}
});
// wrap the text for long x axis names
const addBreaksAtLength = 12;
const textwrapper = function (traces: any) {
traces = traces.map((text: any) => {
let rxp = new RegExp(".{1," + addBreaksAtLength + "}", "g");
return text.match(rxp).join("<br>");
});
return traces;
};
const reDraw: any = (
x: any,
y: any,
params: { title: any; unparsed_x_data: any }
) => {
trace.x = x;
trace.x = textwrapper(x);
trace.y = y;
// layout.title.text = params.title;
trace.unparsed_x = params.unparsed_x_data;
@@ -80,13 +98,14 @@ export default defineComponent({
};
// created force relayout function to avoid infinite loop
const forceReLayout = (flag = true) => {
const forceReLayout = async (flag = true) => {
zoomFlag.value = flag;
const update: any = {
"xaxis.autorange": true,
"yaxis.autorange": true,
};
await nextTick();
Plotly.relayout(plotref.value, update);
};
@@ -16,7 +16,6 @@
<template>
<div class="plotlycontainer">
<div
class="q-pa-sm q-gutter-sm innerPlotlycontainer"
v-if="getVizualizationSelector() === 'BarChart'"
>
<!-- inside barchart-- {{ plotData }}-- -->
@@ -28,7 +27,6 @@
<ReactiveBarChart :data="plotData"></ReactiveBarChart>
</div>
<div
class="q-pa-sm q-gutter-sm innerPlotlycontainer"
v-else-if="getVizualizationSelector() === 'TimeSeries'"
>
<PanelHeader
@@ -39,7 +37,6 @@
<ReactiveLineChart :data="plotData"></ReactiveLineChart>
</div>
<div
class="q-pa-sm col-4 q-gutter-sm innerPlotlycontainer"
:id="panelDataElement.id"
v-else-if="getVizualizationSelector() === 'Table'"
>
@@ -51,7 +48,6 @@
<ReactiveTableChart ref="plotChart" :data="plotData"></ReactiveTableChart>
</div>
<div
class="q-pa-sm q-gutter-sm innerPlotlycontainer"
v-else-if="getVizualizationSelector() === 'Stat'"
>
<PanelHeader
@@ -59,7 +55,7 @@
:dashboardId="getDashboardId()"
@clicked="onClickChild"
/>
<ReactiveTableChart ref="plotChart" :data="plotData" />
<ReactiveTableChart ref="plotChart" :data="plotData"/>
</div>
<div v-else>
<h2>{{ getVizualizationSelector() }}</h2>
@@ -81,7 +77,7 @@ import * as Plotly from "plotly.js";
import { computed, defineComponent, onMounted, onUpdated, ref } from "vue";
import { toRaw, unref, watch } from "vue";
import { useStore } from "vuex";
import { modifySQLQuery } from "../../../utils/commons";
import {modifySQLQuery} from '../../../utils/commons'
export default defineComponent({
name: "PanelContainer",
@@ -108,14 +104,14 @@ export default defineComponent({
watch(selectedTimeObj, () => {
if (toRaw(unref(selectedTimeObj))) {
renderPanel(props.panelDataElement, false);
renderPanel(props.panelDataElement, false)
}
});
const renderPanel = async (panelDataElement: any, onMounted: boolean) => {
const chartParams = {
title: "Found " + "2" + " hits in " + "10" + " ms",
};
};
const sqlQueryModified = modifySQLQuery(
toRaw(unref(selectedTimeObj)),
panelDataElement.query[0]
@@ -124,13 +120,19 @@ export default defineComponent({
query: { sql: sqlQueryModified, sql_mode: "full" },
};
console.log("query===",query);
const panelData = toRaw(store.state.currentPanelsData);
console.log("panelData====",panelData);
const existingPanel = panelData.find(
(panel: any) => panel.id == panelDataElement.id
(panel:any) => panel.id == panelDataElement.id
);
console.log("existingPanel---",existingPanel);
if (existingPanel && onMounted) {
plotData.value = existingPanel;
plotData.value = existingPanel;
} else {
await queryService
.runquery(query, store.state.selectedOrganization.identifier)
@@ -155,14 +157,12 @@ export default defineComponent({
data_vals.map((row) => row[colIndex])
);
const styledHeader = header_vals.map(
(header) => "<b>" + header + "</b>"
);
const styledHeader = header_vals.map((header) => "<b>" + header + "</b>");
const columnColor = output[0].map((col: any, index: any) => {
if (index % 2 == 0) {
return "#FFF";
} else {
return "#C9CCE6";
return "#f7f7f7";
}
});
@@ -172,18 +172,18 @@ export default defineComponent({
header: {
values: styledHeader,
align: "center",
height: 30,
line: { width: 1, color: "black" },
fill: { color: "#7A80C2" },
font: { family: "Arial", size: 14, color: "white" },
height: 28,
line: { width: 1, color: "#91909091" },
fill: { color: "#B1B5E5" },
font: { family: "Nunito Sans", size: 12, color: "black" },
},
cells: {
values: output,
align: "center",
height: 25,
line: { color: "black", width: 1 },
height: 28,
line: { color: "#D4D4D4", width: 1 },
fill: { color: [columnColor] },
font: { family: "Arial", size: 11, color: ["black"] },
font: { family: "Nunito Sans", size: 12, color: ["black"] },
},
};
@@ -263,7 +263,4 @@ export default defineComponent({
.plotlycontainer {
height: 100%;
}
.innerPlotlycontainer {
height: 90%;
}
</style>
@@ -15,32 +15,22 @@
<template>
<div>
<q-toolbar class="col-5 bg-indigo-1">
<q-bar class="bg-white">
<!-- <q-separator dark vertical inset /> -->
<q-toolbar-title>
<div class="text-subtitle1">
{{ renderTitle }}
</div>
</q-toolbar-title>
<q-separator vertical inset />
<div class="">
{{ renderTitle }}
</div>
<q-space />
<!-- <q-toolbar-title>{{ panelDataElementObject.title }}</q-toolbar-title>
<q-separator dark vertical inset /> -->
<q-btn-dropdown flat label="" no-caps>
<q-list>
<q-item
clickable
v-close-popup
@click="onPanelModifyClick('EditPanel')"
>
<q-btn-dropdown dense flat label="" no-caps >
<q-list dense>
<q-item clickable v-close-popup @click="onPanelModifyClick('EditPanel')">
<q-item-section>
<q-item-label class="q-pa-sm">Edit Panel</q-item-label>
</q-item-section>
</q-item>
<q-item
clickable
v-close-popup
@click="onPanelModifyClick('DeletePanel')"
>
<q-item clickable v-close-popup @click="onPanelModifyClick('DeletePanel')">
<q-item-section>
<q-item-label class="q-pa-sm">Delete Panel</q-item-label>
</q-item-section>
@@ -57,64 +47,69 @@
</q-item> -->
</q-list>
</q-btn-dropdown>
</q-toolbar>
</q-bar>
</div>
</template>
<script lang="ts">
import { log } from "console";
import { computed, defineComponent } from "vue";
import { toRaw } from "vue";
import { toRaw } from 'vue';
import { useRouter } from "vue-router";
export default defineComponent({
name: "PanelHeader",
props: ["panelDataElement", "dashboardId"],
panelData: [],
setup(props, { emit }) {
const panelDataElementObject = toRaw(props.panelDataElement);
const dashboardId = props.dashboardId;
const panelDataElementObject = toRaw(props.panelDataElement)
const dashboardId = props.dashboardId
const router = useRouter();
const renderTitle = computed(() => {
return props.panelDataElement.title;
});
return props.panelDataElement.title
})
const addNewPanel = () => {
// return router.push({
// path: "/addPanel",
// query: { dashboard: String(dashboardId), panelId:panelDataElementObject.id },
// });
return router.push({
path: "/editPanel",
query: {
dashboard: String(dashboardId),
panelId: panelDataElementObject.id,
},
query: { dashboard: String(dashboardId), panelId:panelDataElementObject.id },
});
};
}
const deletePanel = () => {
// return router.push({
// path: "/viewDashboard",
// query: { dashboard: String(dashboardId), panelId:panelDataElementObject.id },
// });
};
return router.push({
path: "/viewDashboard",
query: { dashboard: String(dashboardId), panelId:panelDataElementObject.id },
});
}
return {
panelDataElementObject,
dashboardId,
addNewPanel,
deletePanel,
renderTitle,
renderTitle
};
},
methods: {
}, methods: {
onPanelModifyClick(evt: any) {
if (evt == "EditPanel") {
this.addNewPanel();
} else if (evt == "DeletePanel") {
//this.deletePanel()
this.$emit("clicked", this.panelDataElementObject);
} else {
console.log(evt);
if(evt == 'EditPanel'){this.addNewPanel()}
else if(evt == 'DeletePanel') {//this.deletePanel()
this.$emit('clicked', this.panelDataElementObject)
}
else {console.log(evt)}
},
},
});
@@ -45,7 +45,7 @@
<script lang="ts">
import Plotly from "plotly.js";
import { defineComponent, onMounted, ref, onUpdated } from "vue";
import { defineComponent, onMounted, ref, onUpdated, nextTick } from "vue";
export default defineComponent({
name: "ReactiveTableChart",
@@ -55,7 +55,7 @@ export default defineComponent({
const plotref: any = ref(props.data ? props.data.id : 1);
const zoomFlag: any = ref(false);
const chartID = ref("");
const tableId = ref(props?.data?.id);
const tableId = ref(props?.data?.id)
const trace: any = [
{
type: "table",
@@ -122,13 +122,14 @@ export default defineComponent({
};
// created force relayout function to avoid infinite loop
const forceReLayout = (flag = true) => {
const forceReLayout = async (flag = true) => {
zoomFlag.value = flag;
const update: any = {
"xaxis.autorange": true,
"yaxis.autorange": true,
};
await nextTick()
Plotly.relayout(plotref.value, update);
};
@@ -165,7 +166,7 @@ export default defineComponent({
reDraw,
chartID,
forceReLayout,
tableId,
tableId
};
},
});
@@ -173,6 +174,6 @@ export default defineComponent({
<style lang="scss" scoped>
.plotlycontainer {
height: 100%;
height: 90%;
}
</style>
+116
View File
@@ -0,0 +1,116 @@
import { reactive } from "vue";
const getDefaultDashboardPanelData = () => (
{
data: {
id: "",
type: "bar",
fields: {
stream: '',
x: <any>[],
y: <any>[],
filter: <any>[]
},
config: {
title: "Chart",
description: "",
show_legends: false,
},
query: "",
},
layout: {
showCustomQuery: false,
loading:false,
},
meta: {
parsedQuery: "",
editorValue: "",
dateTime: {},
filterValue: <any>[],
stream: {
selectedStreamFields: [],
customQueryFields: [],
functions: [],
streamResults: <any>[],
filterField: "",
},
}
}
)
let dashboardPanelData = reactive({ ...getDefaultDashboardPanelData()});
const useDashboardPanelData = () => {
const resetDashboardPanelData = () => {
Object.assign(dashboardPanelData, getDefaultDashboardPanelData());
console.log("updated...",dashboardPanelData);
};
const addXAxisItem = (name: string) => {
if(!dashboardPanelData.data.fields.x) {
dashboardPanelData.data.fields.x = []
}
// TODO: condition for all chart type
if(dashboardPanelData.data.fields.x.length >= 1){
return;
}
// check for existing field
if(!dashboardPanelData.data.fields.x.find((it:any) => it.column == name)) {
dashboardPanelData.data.fields.x.push({
label: 'x_axis_' + dashboardPanelData.data.fields.x.length,
column: name,
color: null,
aggregationFunction: null
})
}
}
const addYAxisItem = (name: string) => {
if(!dashboardPanelData.data.fields.y) {
dashboardPanelData.data.fields.y = []
}
if(!dashboardPanelData.data.fields.y.find((it:any) => it.column == name)) {
dashboardPanelData.data.fields.y.push({
label: 'y_axis_' + dashboardPanelData.data.fields.y.length,
column: name,
color: '#5960b2',
aggregationFunction: 'count'
})
}
}
const removeXAxisItem = (name: string) => {
const index = dashboardPanelData.data.fields.x.findIndex((it:any) => it.column == name)
if(index >= 0) {
dashboardPanelData.data.fields.x.splice(index, 1)
}
}
const removeYAxisItem = (name: string) => {
const index = dashboardPanelData.data.fields.y.findIndex((it:any) => it.column == name)
if(index >= 0) {
dashboardPanelData.data.fields.y.splice(index, 1)
}
}
const removeFilterItem = (name: string) => {
const index = dashboardPanelData.data.fields.filter.findIndex((it:any) => it.column == name)
if(index >= 0) {
dashboardPanelData.data.fields.filter.splice(index, 1)
}
}
return {
dashboardPanelData,
resetDashboardPanelData,
addXAxisItem,
addYAxisItem,
removeXAxisItem,
removeYAxisItem,
removeFilterItem
};
};
export default useDashboardPanelData;
+5
View File
@@ -285,6 +285,11 @@ export default defineComponent({
target: "_blank",
external: true,
},
{
title: t("menu.dashboard"),
icon: "img:" + getImageURL("images/left_nav/dashb2.svg"),
link: "/dashboardList",
},
]);
const getConfig = async () => {
+11 -2
View File
@@ -232,7 +232,15 @@
"typeDesc": "Type Description of Dashboard",
"createdashboard": "Create a New Dashboard",
"nameHolder": "Enter Name of New Dashboard",
"typeHolder": "Enter Type Description of New Dashboard"
"typeHolder": "Enter Type Description of New Dashboard",
"identifier": "Identifier",
"role": "Role",
"description": "Description",
"type": "Type",
"owner": "Owner",
"created": "Created",
"actions": "Actions",
"nameRequired": "Name is required"
},
"panel": {
"header": "Panel",
@@ -246,7 +254,8 @@
"createdashboard": "Create a New Panel",
"nameHolder": "Enter Name of New Panel",
"typeHolder": "Enter Type Description of New Panel",
"editPanel": "Edit Panel"
"editPanel": "Edit Panel",
"addPanel" : "Add Panel"
},
"alerts": {
"header": "Alerts",
+9 -1
View File
@@ -229,7 +229,15 @@
"typeDesc": "Gösterge Tablosunun Tip Açıklaması",
"createdashboard": "Yeni Bir Gösterge Tablosu Oluşturun",
"nameHolder": "Yeni Gösterge Tablosunun Adını Girin",
"typeHolder": "Yeni Gösterge Tablosunun Tip Açıklamasını Girin"
"typeHolder": "Yeni Gösterge Tablosunun Tip Açıklamasını Girin",
"invite": "Invite Teammate",
"identifier": "Identifier",
"role": "Role",
"description": "Description",
"owner": "Owner",
"created": "Created",
"actions": "Actions",
"nameRequired": "Name is required"
},
"panel": {
"header": "Panel",
+9 -1
View File
@@ -229,7 +229,15 @@
"typeDesc": "仪表板的类型描述",
"createdashboard": "创建新仪表板",
"nameHolder": "输入新仪表板的名称",
"typeHolder": "输入新仪表板的类型描述"
"typeHolder": "输入新仪表板的类型描述",
"invite": "Invite Teammate",
"identifier": "Identifier",
"role": "Role",
"description": "Description",
"owner": "Owner",
"created": "Created",
"actions": "Actions",
"nameRequired": "Name is required"
},
"panel": {
"header": "小组",
+1 -1
View File
@@ -184,7 +184,7 @@ export default defineComponent({
});
</script>
<style lang="scss">
<style lang="scss" scoped>
.q-menu {
box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.1);
transform: translateY(0.5rem);
+9 -10
View File
@@ -18,10 +18,8 @@ import Home from "../views/HomeView.vue";
import Tickets from "../views/TicketsView.vue";
import Users from "../views/User.vue";
import About from "../views/About.vue";
import Dashboard from "../views/Dashboard.vue";
import ViewDashboard from "../views/ViewDashboard.vue";
import EditPanel from "../views/EditPanel.vue";
import ViewDashboard from "../views/Dashboards/ViewDashboard.vue";
import AddPanel from "../views/Dashboards/addPanel/AddPanel.vue";
import LoginCallback from "../views/LoginCallback.vue";
import MemberSubscription from "../views/MemberSubscription.vue";
import Search from "../views/Search.vue";
@@ -30,6 +28,7 @@ import Functions from "../views/Functions.vue";
import Alerts from "../views/Alerts.vue";
import Ingestion from "../views/Ingestion.vue";
import Error404 from "../views/Error404.vue";
import DashboardList from "../views/Dashboards/DashboardList.vue";
import {
useLocalUserInfo,
getDecodedUserInfo,
@@ -109,9 +108,9 @@ export default function (store: any) {
},
},
{
path: "dashboard",
name: "dashboard",
component: Dashboard,
path: "dashboardList",
name: "dashboardList",
component: DashboardList,
meta: {
keepAlive: true,
},
@@ -126,9 +125,9 @@ export default function (store: any) {
},
},
{
path: "editPanel",
name: "editPanel",
component: EditPanel,
path: "addPanel",
name: "addPanel",
component: AddPanel,
props: true,
meta: {
keepAlive: true,
+11 -3
View File
@@ -10,20 +10,28 @@
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// limitations under the License.
import http from "./http";
const dashboards = {
list: (organization: string) => {
list: (
page_num: number,
page_size: number,
sort_by: string,
desc: boolean,
name: string,
organization: string
) => {
return http().get(
`/api/${organization}/dashboards`
`/api/${organization}/dashboards?page_num=${page_num}&page_size=${page_size}&sort_by=${sort_by}&desc=${desc}&name=${name}`
);
},
create: (organization: string, dashboardID: String, data: any) => {
return http().post(`/api/${organization}/dashboards/${dashboardID}`, data, { headers: { 'Content-Type': 'application/json; charset=UTF-8' } });
},
delete: (organization: string, dashboardID: String) => {
console.log(dashboardID)
return http().delete(`/api/${organization}/dashboards/${dashboardID}`);
},
get_Dashboard: (org_identifier: string) => {
+50
View File
@@ -0,0 +1,50 @@
// Copyright 2022 Zinc Labs Inc. and Contributors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http:www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import http from "./http";
var index = {
nameList: (org_identifier: string, type: string, schema: boolean) => {
let url = `/api/${org_identifier}/streams`;
if (type != "") {
url += "?type=" + type;
}
if (schema) {
url += url.indexOf("?") > 0 ? "&fetchSchema=" + schema : "?fetchSchema=" + schema;
}
return http().get(url);
},
schema: (org_identifier: string, stream_name: string, type: string) => {
let url = `/api/${org_identifier}/${stream_name}/schema`;
if (type != "") {
url += "?type=" + type;
}
return http().get(url);
},
updateSettings: (org_identifier: string, stream_name: string, type: string, data: any) => {
let url = `/api/${org_identifier}/${stream_name}/settings`;
if (type != "") {
url += "?type=" + type;
}
return http().post(url, data);
},
};
export default index;
+26 -19
View File
@@ -28,9 +28,10 @@ export default createStore({
currentuser: (useLocalCurrentUser()) ? useLocalCurrentUser() : {},
searchCollapsibleSection: 20,
organizationPasscode: "",
allCurrentDashboards: {},
currentSelectedDashboard: {},
currentPanelsData: [],
// allCurrentDashboards: {},
// currentSelectedDashboard: {},
// currentPanelsData: [],
allDashboardList: [],
search: {
query: {
dateVal: {
@@ -84,11 +85,14 @@ export default createStore({
setOrganizationPasscode(state, payload) {
state.organizationPasscode = payload;
},
setAllCurrentDashboards(state, payload) {
state.allCurrentDashboards = payload;
},
setCurrentSelectedDashboard(state, payload) {
state.currentSelectedDashboard = payload;
// setAllCurrentDashboards(state, payload) {
// state.allCurrentDashboards = payload;
// },
// setCurrentSelectedDashboard(state, payload) {
// state.currentSelectedDashboard = payload;
// },
setAllDashboardList(state, payload) {
state.allDashboardList = payload
},
setSearch(state, payload) {
state.search = payload;
@@ -96,9 +100,9 @@ export default createStore({
setStreamFields(state, payload) {
state.streamFields = payload;
},
setCurrentPanelsData(state, payload) {
state.currentPanelsData = payload;
},
// setCurrentPanelsData(state, payload) {
// state.currentPanelsData = payload;
// },
setQuotaThresholdMsg(state, payload) {
state.quotaThresholdMsg = payload;
},
@@ -134,11 +138,14 @@ export default createStore({
setOrganizationPasscode(context, payload) {
context.commit("setOrganizationPasscode", payload);
},
setAllCurrentDashboards(context, payload) {
context.commit("setAllCurrentDashboards", payload);
},
setCurrentSelectedDashboard(context, payload) {
context.commit("setCurrentSelectedDashboard", payload);
// setAllCurrentDashboards(context, payload) {
// context.commit('setAllCurrentDashboards', payload);
// },
// setCurrentSelectedDashboard(context, payload) {
// context.commit('setCurrentSelectedDashboard', payload);
// },
setAllDashboardList(context, payload) {
context.commit('setAllDashboardList', payload)
},
setSearch(context, payload) {
context.commit("setSearch", payload);
@@ -146,9 +153,9 @@ export default createStore({
setStreamFields(context, payload) {
context.commit("setStreamFields", payload);
},
setCurrentPanelsData(context, payload) {
context.commit("setCurrentPanelsData", payload);
},
// setCurrentPanelsData(context, payload) {
// context.commit('setCurrentPanelsData', payload);
// },
setQuotaThresholdMsg(context, payload) {
context.commit("setQuotaThresholdMsg", payload);
},
+5
View File
@@ -38,4 +38,9 @@
.border-right{
border-right: 1px solid $border-color;
}
// For Dashboard draggable color change
.displayDiv .vue-grid-item.vue-grid-placeholder {
background: #B1B5E5 !important;
}
+222 -1
View File
@@ -84,7 +84,7 @@ export const deletePanelFromDashboard = async (
export const updateAllDashboardsFromBackend = async (store: any) => {
await dashboardService
.list(store.state.selectedOrganization.identifier)
.list(0, 1000, "name", false, "",store.state.selectedOrganization.identifier)
.then((res) => {
store.dispatch("setAllCurrentDashboards", res.data.list);
});
@@ -139,7 +139,10 @@ export const setCurrentPanelToDashboardList = (
let found = 0;
for (const dashboard of dashboardList) {
if (dashboardId === dashboard.name) {
console.log("Inside " + counter);
let modDashboardObject = JSON.parse(toRaw(dashboard.details));
console.log("modDashboardObject---",modDashboardObject);
if (modDashboardObject.layouts && modDashboardObject.layouts.length > 0) {
modDashboardObject.layouts.push(newLayoutObj);
} else {
@@ -147,6 +150,8 @@ export const setCurrentPanelToDashboardList = (
}
for (const panel of modDashboardObject.panels) {
if (panelId === panel.id) {
console.log("Inside Panel match -- " + panelId);
console.log("Panel Replace --- " + JSON.stringify(panel));
modDashboardObject.panels[counter] = panelObject;
found++;
}
@@ -163,6 +168,7 @@ export const setCurrentPanelToDashboardList = (
}
dashboardPos++;
}
console.log("query dashboardList ====",dashboardList);
store.dispatch("setAllCurrentDashboards", dashboardList);
};
@@ -175,7 +181,11 @@ export const getDateConsumableDateTime = function (dateVal: {
endDate: string;
endTime: string;
}) {
console.log("==dateVal===",dateVal);
if (dateVal.tab == "relative") {
console.log('r');
let period = "";
let periodValue = 0;
// quasar does not support arithmetic on weeks. convert to days.
@@ -199,6 +209,7 @@ export const getDateConsumableDateTime = function (dateVal: {
end_time: endTimeStamp,
};
} else {
console.log('a');
let start, end;
if (dateVal.startDate == "" && dateVal.startTime == "") {
@@ -217,11 +228,81 @@ export const getDateConsumableDateTime = function (dateVal: {
start_time: start,
end_time: end,
};
console.log(rVal);
return rVal;
}
};
export function getConsumableDateTime(dateObj: any) {
if (dateObj.tab == "relative") {
let period = "";
let periodValue = 0;
// quasar does not support arithmetic on weeks. convert to days.
if (
dateObj.relative.period.label.toLowerCase() ==
"weeks"
) {
period = "days";
periodValue = dateObj.relative.value * 7;
} else {
period =
dateObj.relative.period.label.toLowerCase();
periodValue = dateObj.relative.value;
}
const subtractObject = '{"' + period + '":' + periodValue + "}";
let endTimeStamp = new Date();
// if (searchObj.data.resultGrid.currentPage > 0) {
// endTimeStamp = searchObj.data.resultGrid.currentDateTime;
// } else {
// searchObj.data.resultGrid.currentDateTime = endTimeStamp;
// }
const startTimeStamp = date.subtractFromDate(
endTimeStamp,
JSON.parse(subtractObject)
);
return {
start_time: startTimeStamp,
end_time: endTimeStamp,
};
} else {
let start, end;
if (
dateObj.absolute.date.from == "" &&
dateObj.absolute.startTime == ""
) {
start = new Date();
} else {
start = new Date(
dateObj.absolute.date.from +
" " +
dateObj.absolute.startTime
);
}
if (
dateObj.absolute.date.to == "" &&
dateObj.absolute.endTime == ""
) {
end = new Date();
} else {
end = new Date(
dateObj.absolute.date.to +
" " +
dateObj.absolute.endTime
);
}
const rVal = {
start_time: start,
end_time: end,
};
return rVal;
}
}
export const modifySQLQuery = (currentTimeObj: any, querySQL: String) => {
const startTime = moment(String(currentTimeObj.start_time)).format(
"YYYY-MM-DDThh:mm:ssZ"
@@ -242,3 +323,143 @@ export const modifySQLQuery = (currentTimeObj: any, querySQL: String) => {
}
return modString;
};
// ----------- NEW Methods to retrieve dashboard data
export const getAllDashboards = async (store: any) => {
// api call
return await dashboardService
.list(0, 1000, "name", false, "",store.state.selectedOrganization.identifier)
.then((res) => {
console.log('dashboard list retriveid');
console.log(res);
// save to store
store.dispatch("setAllDashboardList", res.data.list);
})
.catch((error) => {
console.log(error);
});
}
export const addPanel = async (store: any, dashboardId: any, panelData: any) => {
// get the object of panel data
// find the dashboard and add the panel data to dashboard object
// call the update dashboard function
if(!store.state.allDashboardList || store.state.allDashboardList.length == 0) {
await getAllDashboards(store)
}
const dashboardList = store.state.allDashboardList
let currentDashboard = dashboardList.find((it:any)=>it.name === dashboardId )
currentDashboard = JSON.parse(currentDashboard.details)
if(!currentDashboard.panels){
currentDashboard.panels = []
}
currentDashboard.panels.push(panelData)
const maxI =
currentDashboard.layouts?.length > 0
? Math.max(...currentDashboard.layouts?.map((obj:any) => obj.i))
: 0;
const maxY =
currentDashboard.layouts?.length > 0
? Math.max(...currentDashboard.layouts?.map((obj:any) => obj.y))
: 0;
const newLayoutObj = {
x: 0,
y: currentDashboard.layouts?.length > 0 ? maxY + 10 : 0,
w: 12,
h: 12,
i: maxI + 1,
panelId: panelData.id,
static: false,
};
if(!currentDashboard.layouts){
currentDashboard.layouts = []
}
currentDashboard.layouts.push(newLayoutObj)
console.log(currentDashboard);
return await updateDashboard(store, store.state.selectedOrganization.identifier, dashboardId, currentDashboard )
}
export const deletePanel = async (store:any, dashboardId:any, panelId:any) => {
// get the object of panel id
// find the dashboard and remove the panel data to dashboard object
// call the update dashboard function
const dashboardList = store.state.allDashboardList
let currentDashboard = dashboardList.find((it:any)=>it.name === dashboardId )
currentDashboard = JSON.parse(currentDashboard.details)
//remove panel from current dashboard
const panelIndex = currentDashboard.panels.findIndex(
(panel: any) => panel.id == panelId
);
currentDashboard.panels.splice(panelIndex, 1);
currentDashboard.panels = currentDashboard.panels;
//remove layout from current dashboard
const layoutIndex = currentDashboard.layouts.findIndex(
(layout: any) => layout.panelId == panelId
);
currentDashboard.layouts.splice(layoutIndex, 1);
currentDashboard.layouts = currentDashboard.layouts;
await updateDashboard(store, store.state.selectedOrganization.identifier , dashboardId, currentDashboard )
}
export const updatePanel = async (store:any, dashboardId:any, panelData:any) => {
// get the object of panel id
// find the dashboard and remove the panel data to dashboard object
// call the update dashboard function
const dashboardList = store.state.allDashboardList
let currentDashboard = dashboardList.find((it:any)=>it.name === dashboardId )
currentDashboard = JSON.parse(currentDashboard.details)
//remove panel from current dashboard
const panelIndex = currentDashboard.panels.findIndex(
(panel: any) => panel.id == panelData.id
);
currentDashboard.panels[panelIndex] = panelData;
currentDashboard.panels = currentDashboard.panels;
await updateDashboard(store, store.state.selectedOrganization.identifier , dashboardId, currentDashboard )
}
export const updateDashboard = async (store: any, org:any, dashboardId:any, currentDashboardData: any) => {
// make an api call to update the dashboard
return await dashboardService
.save(
org,
dashboardId,
JSON.stringify(JSON.stringify(currentDashboardData))
)
.then(async (res) => {
// update dashboardList
await getAllDashboards(store)
});
}
export const getDashboard = async (store: any, dashboardId: any) => {
if(!store.state.allDashboardList || store.state.allDashboardList.length == 0) {
await getAllDashboards(store)
}
const dashboardList = store.state.allDashboardList
let currentDashboard = dashboardList.find((it:any)=>it.name === dashboardId )
currentDashboard = JSON.parse(currentDashboard.details)
return currentDashboard
}
export const getPanel = async (store: any, dashboardId: any, panelId: any) => {
if(!store.state.allDashboardList || store.state.allDashboardList.length == 0) {
await getAllDashboards(store)
}
const dashboardList = store.state.allDashboardList
let currentDashboard = dashboardList.find((it:any)=>it.name === dashboardId )
currentDashboard = JSON.parse(currentDashboard.details)
return currentDashboard.panels?.find((it: any) => it.id == panelId)
}
+3 -1
View File
@@ -17,7 +17,7 @@
<q-page class="aboutPage">
<div class="about_head">
<img
src="~@/assets/images/common/app_logo_zo.png"
:src="getImageURL('images/common/app_logo_zo.png')"
class="logo"
width="177"
/>
@@ -48,6 +48,7 @@
<script lang="ts">
import { defineComponent, ref } from "vue";
import { useStore } from "vuex";
import { getImageURL } from "../utils/zincutils";
export default defineComponent({
name: "PageAbout",
@@ -58,6 +59,7 @@ export default defineComponent({
return {
store,
pageData,
getImageURL,
};
},
});
@@ -1,22 +1,8 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- eslint-disable vue/v-on-event-hyphenation -->
<!-- eslint-disable vue/attribute-hyphenation -->
<template>
<q-page class="q-pa-none">
<!-- add dashboard table -->
<q-table
ref="qTable"
:rows="dashboards"
@@ -28,15 +14,17 @@
:loading="loading"
@row-click="onRowClick"
>
<!-- if data not available show nodata component -->
<template #no-data>
<NoData />
</template>
<!-- add delete icon in actions column -->
<template #body-cell-actions="props">
<q-td :props="props">
<q-btn
v-if="props.row.actions == 'true'"
:icon="'img:' + getImageURL('images/common/remove_icon.svg')"
:title="t('organization.invite')"
:title="t('dashboard.delete')"
class="iconHoverBtn"
padding="sm"
unelevated
@@ -47,7 +35,7 @@
></q-btn>
</q-td>
</template>
<!-- searchBar at top -->
<template #top="scope">
<div class="q-table__title">{{ t("dashboard.header") }}</div>
<q-input
@@ -61,6 +49,7 @@
<q-icon name="search" />
</template>
</q-input>
<!-- add dashboard button -->
<q-btn
class="q-ml-md q-mb-xs text-bold no-border"
padding="sm lg"
@@ -70,6 +59,7 @@
@click="addDashboard"
/>
<!-- table pagination -->
<QTablePagination
:scope="scope"
:pageTitle="t('dashboard.header')"
@@ -98,41 +88,11 @@
full-height
maximized
>
<add-update-organization @updated="updateDashboardList" />
</q-dialog>
<q-dialog v-model="showOrgAPIKeyDialog">
<q-card>
<q-card-section>
<div class="text-h6">Organization API Key</div>
</q-card-section>
<q-card-section class="q-pt-none" wrap>
<q-item>
<q-item-section>
<q-item-label lines="3" style="word-wrap: break-word">{{
organizationAPIKey
}}</q-item-label>
</q-item-section>
<q-item-section side>
<q-btn
unelevated
round
flat
padding="sm"
size="sm"
icon="content_copy"
@click="copyAPIKey"
:title="t('organization.copyapikey')"
/>
</q-item-section>
</q-item>
</q-card-section>
</q-card>
<AddDashboard @updated="updateDashboardList" />
</q-dialog>
</q-page>
</template>
<script lang="ts">
// @ts-nocheck
import { computed, defineComponent, onMounted, ref } from "vue";
@@ -140,20 +100,18 @@ import { useStore } from "vuex";
import { useQuasar, date, copyToClipboard } from "quasar";
import { useI18n } from "vue-i18n";
import organizationsService from "../services/organizations";
import dashboardService from "../services/dashboards";
import AddUpdateOrganization from "../components/dashboards/AddUpdateDashboards.vue";
import QTablePagination from "../components/shared/grid/Pagination.vue";
import NoData from "../components/shared/grid/NoData.vue";
import dashboardService from "../../services/dashboards";
import AddDashboard from "../../components/dashboards/AddDashboard.vue";
import QTablePagination from "../../components/shared/grid/Pagination.vue";
import NoData from "../../components/shared/grid/NoData.vue";
import { useRouter } from "vue-router";
import { isProxy, toRaw } from "vue";
import { getImageURL } from "../utils/zincutils";
import { getImageURL } from "../../utils/zincutils";
export default defineComponent({
name: "PageOrganization",
name: "DashboardList",
components: {
AddUpdateOrganization,
AddDashboard,
QTablePagination,
NoData,
},
@@ -161,12 +119,12 @@ export default defineComponent({
const store = useStore();
const { t } = useI18n();
const $q = useQuasar();
const organization = ref({});
const dashboard = ref({});
const showAddDashboardDialog = ref(false);
const showOrgAPIKeyDialog = ref(false);
const organizationAPIKey = ref("");
const qTable: any = ref(null);
const router = useRouter();
const orgData: any = ref(store.state.selectedOrganization);
const columns = ref<QTableProps["columns"]>([
{
name: "#",
@@ -177,49 +135,49 @@ export default defineComponent({
{
name: "name",
field: "name",
label: t("organization.name"),
label: t("dashboard.name"),
align: "left",
sortable: true,
},
{
name: "identifier",
field: "identifier",
label: t("organization.identifier"),
label: t("dashboard.identifier"),
align: "left",
sortable: true,
},
{
name: "role",
field: "role",
label: t("organization.role"),
label: t("dashboard.role"),
align: "left",
sortable: true,
},
{
name: "type",
field: "type",
label: t("organization.type"),
name: "description",
field: "description",
label: t("dashboard.description"),
align: "left",
sortable: true,
},
{
name: "owner",
field: "owner",
label: t("organization.owner"),
label: t("dashboard.owner"),
align: "left",
sortable: true,
},
{
name: "created",
field: "created",
label: t("organization.created"),
label: t("dashboard.created"),
align: "left",
sortable: true,
},
{
name: "actions",
field: "actions",
label: t("organization.actions"),
label: t("dashboard.actions"),
align: "center",
},
]);
@@ -255,7 +213,7 @@ export default defineComponent({
showAddDashboardDialog.value = true;
};
const routeToViewD = (row) => {
//return router.push( '/viewDashboard' )
console.log("row");
return router.push({
path: "/viewDashboard",
query: { dashboard: row.identifier },
@@ -266,29 +224,36 @@ export default defineComponent({
spinner: true,
message: "Please wait while loading dashboards...",
});
await dashboardService
.list(store.state.selectedOrganization.identifier)
.list(
0,
1000,
"name",
false,
"",
store.state.selectedOrganization.identifier
)
.then((res) => {
store.dispatch("setAllCurrentDashboards", res.data.list);
resultTotal.value = res.data.list.length;
console.log(res);
store.dispatch("setAllDashboardList", res.data.list);
dismiss();
})
.catch((error) => {
console.log(error);
});
};
const dashboards = computed(function () {
const dashboardList = toRaw(store.state.allCurrentDashboards);
return dashboardList.map((data: any) => {
const dashboardList = toRaw(store.state.allDashboardList);
return dashboardList.map((data: any, index) => {
const jsonDataOBj = JSON.parse(data.details);
return {
"#":
data.id <= 9
? `0${jsonDataOBj.dashboardID}`
: jsonDataOBj.dashboardID,
"#": index < 9 ? `0${index + 1}` : index + 1,
id: jsonDataOBj.dashboardId,
name: jsonDataOBj.title,
identifier: jsonDataOBj.dashboardId,
type: jsonDataOBj.description,
description: jsonDataOBj.description,
owner: jsonDataOBj.owner,
created: date.formatDate(jsonDataOBj.created, "YYYY-MM-DDTHH:mm:ssZ"),
role: jsonDataOBj.role,
@@ -296,29 +261,19 @@ export default defineComponent({
};
});
});
const onAddTeam = (props: any) => {
console.log(props);
};
const inviteTeam = (props: any) => {
organization.value = {
id: props.row.id,
name: props.row.name,
role: props.row.role,
member_lists: [],
};
};
const removeDashboard = async (props: any) => {
const dashboardId = props.key;
await dashboardService
.delete(store.state.selectedOrganization.identifier, dashboardId)
.then((res) => {
const dashboardList = JSON.parse(
JSON.stringify(toRaw(store.state.allCurrentDashboards))
JSON.stringify(toRaw(store.state.allDashboardList))
);
const newDashboardList = dashboardList.filter(
(dashboard) => dashboard.name != dashboardId
);
store.dispatch("setAllCurrentDashboards", newDashboardList);
store.dispatch("setAllDashboardList", newDashboardList);
$q.notify({
type: "positive",
message: "Dashboard deleted successfully.",
@@ -333,16 +288,15 @@ export default defineComponent({
return {
t,
qTable,
store,
orgData,
router,
loading: ref(false),
dashboards,
organization,
dashboard,
columns,
showAddDashboardDialog,
showOrgAPIKeyDialog,
organizationAPIKey,
addDashboard,
inviteTeam,
onAddTeam,
pagination,
resultTotal,
perPageOptions,
@@ -368,6 +322,7 @@ export default defineComponent({
};
},
methods: {
//after adding dashboard need to update the dashboard list
updateDashboardList() {
this.showAddDashboardDialog = false;
this.getDashboards();
@@ -380,48 +335,28 @@ export default defineComponent({
onRowClick(evt, row) {
this.routeToViewD(row);
},
getAPIKey(org_identifier: string) {
const dismiss: any = this.$q.notify({
message: "Wait while processing your request...",
});
this.organizationAPIKey = "";
organizationsService
.get_organization_passcode(org_identifier)
.then((res) => {
dismiss();
if (res.data.data.token == "") {
this.$q.notify({
type: "negative",
message: "API Key not found.",
timeout: 5000,
});
} else {
this.showOrgAPIKeyDialog = true;
this.organizationAPIKey = res.data.data.token;
}
});
},
computed: {
selectedOrg() {
return this.store.state.selectedOrganization.identifier;
},
copyAPIKey() {
copyToClipboard(this.organizationAPIKey)
.then(() => {
this.$q.notify({
type: "positive",
message: "API Key Copied Successfully!",
timeout: 5000,
});
})
.catch(() => {
this.$q.notify({
type: "negative",
message: "Error while copy API Key.",
timeout: 5000,
});
});
},
watch: {
selectedOrg(newVal: any, oldVal: any) {
this.orgData.identifier = newVal;
if (
(newVal != oldVal || this.dashboards.value == undefined) &&
this.router.currentRoute.value.name == "dashboardList"
) {
console.log("inside if");
this.getDashboards(this.store.state.selectedOrganization.id);
}
},
},
});
</script>
<style lang="scss" scoped>
.q-table {
&__top {
@@ -430,4 +365,3 @@ export default defineComponent({
}
}
</style>
@@ -1,24 +1,9 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- eslint-disable vue/v-on-event-hyphenation -->
<!-- eslint-disable vue/attribute-hyphenation -->
<template>
<q-page class="q-pa-md">
<div class="flex justify-between items-center q-pa-sm">
<div class="q-table__title q-mr-md">{{ t("dashboard.header") }}</div>
<div class="q-table__title q-mr-md">{{ list[0].title }}</div>
<div class="flex items-baseline q-gutter-sm">
<q-btn
class="q-ml-md q-mb-xs text-bold"
@@ -28,13 +13,35 @@
text-color="black"
no-caps
:label="t(`Add Panel`)"
@click="addNewPanelOnClick"
@click="addPanelData"
/>
<q-btn
class="q-ml-md q-mb-xs text-bold"
outline
padding="sm lg"
color="white"
text-color="black"
no-caps
:label="draggable ? t(`Cancel`) : t(`Edit`)"
@click="isDraggableClick"
/>
<!-- <q-btn
class="q-ml-md q-mb-xs text-bold"
outline
padding="sm lg"
color="white"
text-color="black"
no-caps
:label="t(`Edit Panel`)"
@click="addNewPanelOnClick"
/> -->
<q-btn
class="q-ml-md q-mb-xs text-bold no-border"
padding="sm lg"
color="secondary"
no-caps
:disable="!draggable"
:label="t(`Save`)"
@click="saveDashboardOnClick"
/>
@@ -55,26 +62,33 @@
:label="t(`Go back to Dashboard`)"
outline
text-color="black"
@click="goBacToDashboardList"
/>
<date-time
ref="refDateTime"
v-model="dateVal"
@update:date="dateChange"
@click="goBackToDashboardList"
/>
<date-time ref="refDateTime" @date-change="dateChange" />
<!-- <q-btn
class="q-ml-md q-mb-xs text-bold"
padding="sm lg"
color="white"
no-caps
:label="t(`show New Panel`)"
outline
text-color="black"
@click="showNewPanel= !showNewPanel"
/> -->
</div>
</div>
<q-separator></q-separator>
<div class="displayDiv">
<grid-layout
v-if="list[0].panels.length > 0"
v-if="list[0].panels?.length > 0"
v-model:layout.sync="list[0].layouts"
:col-num="12"
:row-height="30"
:is-draggable="draggable"
:isResizable="true"
:vertical-compact="false"
:is-resizable="draggable"
:vertical-compact="true"
:autoSize="true"
:restore-on-drag="true"
:use-css-transforms="true"
@layout-created="layoutCreatedEvent"
@layout-before-mount="layoutBeforeMountEvent"
@@ -82,10 +96,6 @@
@layout-ready="layoutReadyEvent"
@layout-updated="layoutUpdatedEvent"
>
<!-- <li v-for="item in list[0]" :key="item">
<PanelContainer :key="item.id" ref="{{item}}" :panelDataElement="item" :dashbordId="getDashboard"
@updated:chart="onUpdatePanel">
</PanelContainer> -->
<grid-item
class="plotlyBackground"
v-for="item in list[0].panels"
@@ -95,26 +105,22 @@
:w="getPanelLayout(list[0].layouts, item.id, 'w')"
:h="getPanelLayout(list[0].layouts, item.id, 'h')"
:i="getPanelLayout(list[0].layouts, item.id, 'i')"
:minH="getMinimumHeight(item.type)"
@resize="resizeEvent"
@move="moveEvent"
@resized="resizedEvent"
@container-resized="containerResizedEvent"
@moved="movedEvent"
drag-allow-from=".drag-allow"
>
<PanelContainer
:key="
getPanelLayout(list[0].layouts, item.id, 'w') +
getPanelLayout(list[0].layouts, item.id, 'h') +
getPanelLayout(list[0].layouts, item.id, 'y') +
getPanelLayout(list[0].layouts, item.id, 'x')
"
ref="{{item}}"
:panelDataElement="item"
:dashboardId="getDashboard"
@updated:chart="onUpdatePanel"
:selectedTimeDate="currentTimeObj"
>
</PanelContainer>
<div>
<PanelContainer
@updated:chart="onUpdatePanel"
:data="item"
:selectedTimeDate="currentTimeObj"
>
</PanelContainer>
</div>
</grid-item>
</grid-layout>
</div>
@@ -126,55 +132,54 @@
<script lang="ts">
// @ts-nocheck
import { defineComponent, ref, computed, watch, onMounted } from "vue";
import {
defineComponent,
ref,
computed,
watch,
onMounted,
onActivated,
} from "vue";
import { useStore } from "vuex";
import { useQuasar, date, copyToClipboard } from "quasar";
import { useI18n } from "vue-i18n";
import QTablePagination from "../components/shared/grid/Pagination.vue";
import NoData from "../components/shared/grid/NoData.vue";
import ReactiveBarChart from "../components/shared/plotly/barchart.vue";
import PanelContainer from "../components/shared/plotly/panelcontainer.vue";
import dashboardService from "../services/dashboards";
import DateTime from "../plugins/logs/DateTime.vue";
import dashboardService from "../../services/dashboards";
import DateTime from "../../plugins/logs/DateTime.vue";
import VueGridLayout from "vue3-grid-layout";
import { useRouter } from "vue-router";
import dashboards from "@/services/dashboards";
import {
getCurrentDashboard,
getCurrentPanel,
deletePanelFromDashboard,
setCurrentPanelToDashboardList,
getDateConsumableDateTime,
} from "../utils/commons.ts";
import { isProxy, toRaw, unref } from "vue";
import { tsImportEqualsDeclaration } from "@babel/types";
getConsumableDateTime,
getDashboard,
} from "../../utils/commons.ts";
import { toRaw, unref, reactive } from "vue";
import PanelContainer from "../../components/dashboards/PanelContainer.vue";
import { useRoute } from "vue-router";
import { deletePanel, updateDashboard } from "../../utils/commons";
export default defineComponent({
name: "PageOrganization",
name: "ViewDashboard",
components: {
QTablePagination,
ReactiveBarChart,
PanelContainer,
NoData,
GridLayout: VueGridLayout.GridLayout,
GridItem: VueGridLayout.GridItem,
DateTime,
PanelContainer,
},
setup() {
const { t } = useI18n();
const selectedPerPage = ref<number>(20);
const route = useRoute();
const router = useRouter();
const store = useStore();
// let list = ref([])
let valueList = [];
const currentDashboardData = reactive({
data: {},
});
const refDateTime: any = ref(null);
const $q = useQuasar();
let currentTimeObj: any = ref({});
// let showNewPanel = ref(true)
const dateVal = ref({
const initialDateValue = {
tab: "relative",
startDate: "",
startTime: "",
@@ -183,42 +188,37 @@ export default defineComponent({
selectedRelativePeriod: "Minutes",
selectedRelativeValue: 15,
selectedFullTime: false,
});
onMounted(() => {
dateChange(dateVal);
});
watch(dateVal.value, () => {
if (dateVal.value) {
dateChange(dateVal);
}
});
const dateChange = (dateValue: any) => {
const c = toRaw(unref(dateValue));
currentTimeObj.value = getDateConsumableDateTime(c);
};
const pagination: any = ref({
rowsPerPage: 20,
});
// if the date value change, get the Date and time
const dateChange = (dateValue: any) => {
const c = toRaw(unref(dateValue));
currentTimeObj.value = getConsumableDateTime(c);
};
const initialize = () => {};
const changePagination = (val: { label: string; value: any }) => {
selectedPerPage.value = val.value;
pagination.value.rowsPerPage = val.value;
};
// back button to render dashboard List page
const goBack = () => {
return router.push("/dashboard");
return router.push("/dashboardList");
};
const goBackToDashboardList = () => {
goBack();
};
// delete dashboard remove the data from database and update store variable and redirect to dashboardList page
const deleteDashboard = async (dashboardId: String) => {
const baseObj = {};
await dashboardService
.delete(store.state.selectedOrganization.identifier, dashboardId)
.then((res) => {
const dashboardList = JSON.parse(
JSON.stringify(toRaw(store.state.allDashboardList))
);
const newDashboardList = dashboardList.filter(
(dashboard) => dashboard.name != dashboardId
);
store.dispatch("setAllDashboardList", newDashboardList);
$q.notify({
type: "positive",
message: "Dashboard deleted successfully.",
@@ -228,76 +228,93 @@ export default defineComponent({
goBack();
};
const saveDashboardOnClick = async (dashboardId: String) => {
const currentDashboard = toRaw(store.state.currentSelectedDashboard);
await dashboardService
.save(
store.state.selectedOrganization.identifier,
currentDashboard.dashboardId,
JSON.stringify(JSON.stringify(currentDashboard))
)
.then((res) => {
$q.notify({
type: "positive",
message: "Dashboard saved successfully.",
timeout: 5000,
});
});
const deleteDashboardOnClick = async () => {
await deleteDashboard(route.query.dashboard);
};
// save the dashboard value
const saveDashboard = async (dashboardId: String) => {
await updateDashboard(
store,
store.state.selectedOrganization.identifier,
route.query.dashboard,
currentDashboardData.data
);
// const currentDashboard = currentDashboardData.data
// await dashboardService
// .save(
// store.state.selectedOrganization.identifier,
// currentDashboard.dashboardId,
// JSON.stringify(JSON.stringify(currentDashboard))
// )
// .then((res) => {
// $q.notify({
// type: "positive",
// message: "Dashboard saved successfully.",
// timeout: 5000,
// });
// });
currentDashboardData.data = await getDashboard(
this.store,
this.$route.query.dashboard
);
//goBack()
};
const getDashboard = () => {
return currentDashboard.dashboardId;
};
const refreshAllDashboardsAndGetCurrentDashboard = async (
dashId: String
) => {
const a = await getCurrentDashboard(store, dashId);
return a;
const saveDashboardOnClick = async () => {
await saveDashboard(route.query.dashboard);
};
//get current dashboard Id
const getDashboard = () => {
return currentDashboardData.data.dashboardId;
};
//add dashboardId
const addNewPanel = (dashboardId: String) => {
const panelId =
"Panel_ID" + Math.floor(Math.random() * (99999 - 10 + 1)) + 10;
return router.push({
path: "/editPanel",
query: { dashboard: dashboardId, panelId: panelId },
path: "/addPanel",
query: { dashboard: dashboardId },
});
};
const addPanelData = () => {
addNewPanel(route.query.dashboard);
};
const deleteExistingPanel = async (
panelDataElement: any,
dashboardId: String,
dashboardList: any
) => {
//const panelId = 'Panel_ID' + Math.floor(Math.random() * (99999 - 10 + 1)) + 10
deletePanelFromDashboard(
store,
dashboardId,
panelDataElement.id,
dashboardList
);
// await saveDashboardOnClick(dashboardId)
//router.go(0);
};
let list = computed(function () {
return [toRaw(store.state.currentSelectedDashboard)];
return [toRaw(currentDashboardData.data)];
});
initialize();
return {
currentDashboardData,
goBackToDashboardList,
deleteDashboardOnClick,
addPanelData,
t,
list,
goBack,
getDashboard,
valueList,
dateVal,
dateVal: initialDateValue,
deleteDashboard,
addNewPanel,
saveDashboardOnClick,
deleteExistingPanel,
refreshAllDashboardsAndGetCurrentDashboard,
store,
refDateTime,
filterQuery: ref(""),
@@ -313,80 +330,43 @@ export default defineComponent({
},
dateChange,
currentTimeObj,
// showNewPanel,
};
},
data() {
return {
computedTimeObj: Date.now(),
// layouts: [
// { x: 0, y: 0, w: 12, h: 12, i: 0, panelId: "Panel_ID607710", static: false },
// { x: 4, y: 4, w: 12, h: 12, i: 1, panelId: "Panel_ID7545110", static: false },
// ],
// layoutCopy:{
// Panel_ID607710 :{ x: 0, y: 0, w: 2, h: 2, i: "Panel_ID607710", static: false },
// Panel_ID7545110: { x: 20, y: 40, w: 2, h: 4, i: "Panel_ID7545110", static: false },
// },
// mockedData: {
// title: "Time Range Tester Dash",
// dashboardId: "DashID_1262352719",
// layout: [
// { x: 0, y: 0, w: 12, h: 12, i: 0, panelId: "Panel_ID607710", static: false },
// { x: 4, y: 4, w: 12, h: 12, i: 1, panelId: "Panel_ID7545110", static: false },
// ],
// dashboardLayout: {
// Panel_ID607710: { x: 0, y: 0, w: 2, h: 2, i: "0", static: false },
// Panel_ID7545110: { x: 0, y: 0, w: 2, h: 2, i: "0", static: false },
// },
// description: "Time Tester D",
// role: "User Dashboard",
// owner: "Ayon Bhattacharya",
// created: "2022-12-13T02:00:17.720Z",
// panels: [
// {
// id: "Panel_ID607710",
// query: [
// "select count(*) as y_axis, kubernetes.host as x_axis from default WHERE time_range(_timestamp,'2022-12-13T08:06:59+07:00', '2022-12-13T11:06:59+07:00') group by x_axis",
// ],
// title: "This is always a Description Text",
// type: "BarChart",
// },
// {
// id: "Panel_ID7545110",
// query: [
// "select count(*) as y_axis, kubernetes.host as x_axis from default WHERE time_range(_timestamp,'2022-12-13T08:09:33+07:00', '2022-12-13T11:09:33+07:00') group by x_axis",
// ],
// title: "This is always a Description Text",
// type: "Table",
// },
// ],
// },
draggable: true,
resizable: true,
draggable: false,
index: 0,
eventLog: [],
};
},
methods: {
goBacToDashboardList(evt, row) {
this.goBack();
isDraggableClick(evt, row) {
this.draggable = !this.draggable;
},
deleteDashboardOnClick(evt, row) {
this.deleteDashboard(this.$route.query.dashboard);
},
saveDashboardOnClick(evt, row) {
this.saveDashboard(this.$route.query.dashboard);
},
addNewPanelOnClick(evt, row) {
this.addNewPanel(this.$route.query.dashboard);
},
onUpdatePanel(panelDataElementValue: any) {
let dashboardList = toRaw(this.store.state.allCurrentDashboards);
this.deleteExistingPanel(
panelDataElementValue,
async onUpdatePanel(panelDataElementValue: any) {
// let dashboardList = toRaw(this.store.state.allDashboardList);
// this.deleteExistingPanel(
// panelDataElementValue,
// this.$route.query.dashboard,
// dashboardList
// );
console.log(
"deleting",
this.$route.query.dashboard,
dashboardList
panelDataElementValue,
panelDataElementValue.id
);
await deletePanel(
this.store,
this.$route.query.dashboard,
panelDataElementValue.id
);
this.currentDashboardData.data = await getDashboard(
this.store,
this.$route.query.dashboard
);
},
updateCurrentDateTimeObj() {
@@ -415,6 +395,7 @@ export default defineComponent({
this.eventLog.push(msg);
},
resizedEvent: function (i, newX, newY, newHPx, newWPx) {
window.dispatchEvent(new Event("resize"));
const msg =
"RESIZED i=" +
i +
@@ -476,25 +457,36 @@ export default defineComponent({
return 0;
},
getMinimumHeight(type) {
switch (type) {
case "area":
case "bar":
case "h-bar":
case "line":
case "pie":
case "scatter":
case "table":
return 13;
break;
default:
break;
}
},
},
async activated() {
let dashboardList = toRaw(this.store.state.allCurrentDashboards);
if (Object.keys(dashboardList).length === 0) {
await this.refreshAllDashboardsAndGetCurrentDashboard(
this.$route.query.dashboard
);
} else {
for (const dashboard of dashboardList) {
if (this.$route.query.dashboard === dashboard.name) {
this.store.dispatch(
"setCurrentSelectedDashboard",
JSON.parse(dashboard.details)
);
}
}
}
//await this.refreshAllDashboardsAndGetCurrentDashboard(this.$route.query.dashboard);
//store.dispatch("setCurrentSelectedDashboard", modDashboardObject);
// //get dashboard list from the store
// let dashboardList = toRaw(this.store.state.allDashboardList);
// //find the dashboard details from the dashboard list using dashboardId
// for (const dashboard of dashboardList) {
// if (this.$route.query.dashboard === dashboard.name) {
// this.currentDashboardData.data = JSON.parse(dashboard.details)
// }
// }
this.currentDashboardData.data = await getDashboard(
this.store,
this.$route.query.dashboard
);
},
});
</script>
@@ -511,8 +503,12 @@ export default defineComponent({
// background: #eee;
}
.vue-grid-item:not(.vue-grid-placeholder) {
background: #ccc;
// .vue-grid-item:not(.vue-grid-placeholder) {
// background: #ccc;
// border: 1px solid black;
// }
.vue-grid-item {
border: 1px solid black;
}
@@ -584,7 +580,7 @@ export default defineComponent({
.displayDiv {
clear: both;
padding: 1.625em 0 0;
// padding: 1.625em 0 0;
// overflow: auto;
}
.plotlyBackground {
@@ -0,0 +1,218 @@
<!-- eslint-disable vue/no-unused-components -->
<template>
<div style="height: calc(100vh - 57px); overflow-y: auto" class="scroll">
<div class="flex justify-between items-center q-pa-sm">
<div class="q-table__title q-mr-md">
{{ editMode ? t("panel.editPanel") : t("panel.addPanel") }}
</div>
<div class="flex items-baseline q-gutter-sm">
<date-time @date-change="updateDateTime" />
<q-btn
class="q-ml-md q-mb-xs text-bold"
outline
padding="sm lg"
color="red"
no-caps
:label="t(`Discard`)"
@click="goBackToDashboardList"
/>
<q-btn
class="q-ml-md q-mb-xs text-bold"
outline
padding="sm lg"
color="white"
text-color="black"
no-caps
:label="t(`Save`)"
@click="savePanelOnClick"
/>
<q-btn
class="q-ml-md q-mb-xs text-bold no-border"
padding="sm lg"
color="secondary"
no-caps
:label="t(`Apply`)"
@click="runQuery"
/>
</div>
</div>
<q-separator></q-separator>
<div class="row" style="height: calc(100vh - 118px); overflow-y: auto">
<div
class="col"
style="overflow: hidden; min-width: 75px; max-width: 75px"
>
<ChartSelection
v-model:selectedChartType="dashboardPanelData.data.type"
/>
</div>
<q-separator vertical />
<div class="col scroll" style="height: 100%">
<GetFields />
</div>
<q-separator vertical />
<div class="col scroll" style="height: 100%">
<Layout />
</div>
<q-separator vertical />
<div class="col-7 scroll" style="height: 100%">
<SearchBar />
<q-separator />
<ChartRender
:data="chartData"
:selectedTimeDate="dashboardPanelData.meta.dateTime"
/>
</div>
</div>
</div>
</template>
<script lang="ts">
import {
defineComponent,
ref,
computed,
toRaw,
onActivated,
nextTick,
} from "vue";
import ChartSelection from "../../../components/dashboards/addPanel/ChartSelection.vue";
import GetFields from "../../../components/dashboards/addPanel/GetFields.vue";
import { useQuasar, date } from "quasar";
import { useI18n } from "vue-i18n";
import {
addPanel,
getConsumableDateTime,
getPanel,
updatePanel,
} from "../../../utils/commons";
import { useRoute, useRouter } from "vue-router";
import { useStore } from "vuex";
import Layout from "../../../components/dashboards/addPanel/Layout.vue";
import SearchBar from "../../../components/dashboards/SearchBar.vue";
import useDashboardPanelData from "../../../composables/useDashboardPanel";
import DateTime from "../../../plugins/logs/DateTime.vue";
import ChartRender from "../../../components/dashboards/addPanel/ChartRender.vue";
export default defineComponent({
name: "AddPanel",
components: {
ChartSelection,
GetFields,
Layout,
SearchBar,
DateTime,
ChartRender,
},
setup() {
// This will be used to copy the chart data to the chart renderer component
// This will deep copy the data object without reactivity and pass it on to the chart renderer
const chartData = ref({});
const $q = useQuasar();
const { t } = useI18n();
const router = useRouter();
const route = useRoute();
const store = useStore();
const { dashboardPanelData, resetDashboardPanelData } =
useDashboardPanelData();
const editMode = ref(false);
onActivated(async () => {
// todo check for the edit more
if (route.query.panelId) {
editMode.value = true;
const panelData = await getPanel(
store,
route.query.dashboard,
route.query.panelId
);
console.log("panel data", panelData);
Object.assign(dashboardPanelData.data, panelData);
runQuery();
} else {
editMode.value = false;
resetDashboardPanelData();
chartData.value = {};
}
});
let list = computed(function () {
return [toRaw(store.state.currentSelectedDashboard)];
});
const currentDashboard = toRaw(store.state.currentSelectedDashboard);
const getDashboard = () => {
return currentDashboard.dashboardId;
};
const runQuery = () => {
console.log("query change detected to run");
// copy the data object excluding the reactivity
chartData.value = JSON.parse(JSON.stringify(dashboardPanelData.data));
};
const updateDateTime = (value: object) => {
dashboardPanelData.meta.dateTime = getConsumableDateTime(value);
};
const goBack = () => {
return router.push({
path: "/viewDashboard",
query: { dashboard: route.query.dashboard },
});
};
const savePanelChangesToDashboard = async (dashId: String) => {
if (editMode.value) {
await updatePanel(
store,
dashId,
JSON.parse(JSON.stringify(dashboardPanelData.data))
);
} else {
const panelId =
"Panel_ID" + Math.floor(Math.random() * (99999 - 10 + 1)) + 10;
dashboardPanelData.data.id = panelId;
await addPanel(
store,
dashId,
JSON.parse(JSON.stringify(dashboardPanelData.data))
);
}
await nextTick();
return router.push({
path: "/viewDashboard?dashbpard=" + dashId,
});
};
return {
t,
updateDateTime,
goBack,
savePanelChangesToDashboard,
runQuery,
getDashboard,
currentDashboard,
list,
dashboardPanelData,
chartData,
editMode,
};
},
methods: {
goBackToDashboardList(evt: any, row: any) {
this.goBack();
},
savePanelOnClick() {
this.savePanelChangesToDashboard(this.getDashboard());
},
},
});
</script>
<style lang="sass" scoped>
</style>
-503
View File
@@ -1,503 +0,0 @@
<!-- Copyright 2022 Zinc Labs Inc. and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http:www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- eslint-disable vue/v-on-event-hyphenation -->
<!-- eslint-disable vue/attribute-hyphenation -->
<template>
<q-page class="q-pa-md">
<div class="flex justify-between items-center q-pa-sm">
<div class="q-table__title q-mr-md">{{ t("panel.editPanel") }}</div>
<div class="flex items-baseline q-gutter-sm">
<date-time
ref="refDateTime"
v-model="dateVal"
@update:date="dateChange"
/>
<q-btn
class="q-ml-md q-mb-xs text-bold"
outline
padding="sm lg"
color="red"
no-caps
:label="t(`Discard`)"
@click="goBacToDashboardList"
/>
<q-btn
class="q-ml-md q-mb-xs text-bold"
outline
padding="sm lg"
color="white"
text-color="black"
no-caps
:label="t(`Save`)"
@click="savePanelOnClick"
/>
<q-btn
class="q-ml-md q-mb-xs text-bold no-border"
padding="sm lg"
color="secondary"
no-caps
:label="t(`Apply`)"
@click="runQuery"
/>
</div>
</div>
<q-separator></q-separator>
<div class="row" style="minheight: 82vh">
<div class="col-9">
<div style="marginleft: -45px; minheight: 45vh">
<div v-if="vizualizationType === 'BarChart'">
<ReactiveBarChart
ref="plotChart"
:data="plotData"
@updated:chart="onChartUpdate"
></ReactiveBarChart>
</div>
<div v-else-if="vizualizationType === 'TimeSeries'">
<ReactiveLineChart
ref="plotChart"
:data="plotData"
@updated:chart="onChartUpdate"
/>
</div>
<div id="tableCustom1" v-else-if="vizualizationType === 'Table'">
<ReactiveTableChart
ref="plotChart"
:data="plotData"
@updated:chart="onChartUpdate"
/>
</div>
<div v-else-if="vizualizationType === 'Stat'">
<ReactiveTableChart
ref="plotChart"
:data="plotData"
@updated:chart="onChartUpdate"
/>
</div>
<div v-else>
<!-- <ReactiveTableChart
ref="plotChart"
:data="plotData"
@updated:chart="onChartUpdate"
/> -->
</div>
</div>
<q-separator></q-separator>
<q-tabs v-model="panelTab" class="text-teal" align="left">
<q-tab name="query" :label="t(`Query`)" no-caps />
<q-tab name="transform" :label="t(`Transform`)" no-caps />
<q-tab name="alert" :label="t(`Alert`)" no-caps />
</q-tabs>
<q-tab-panels v-model="panelTab" animated>
<q-tab-panel name="query">
<div class="q-col-gutter-xs" style="width: 100%, height:50%"></div>
<div class="q-pa-md" style="max-width: 800px">
<q-input
v-model="queryText"
ref="queryTextA"
filled
clearable
autogrow
color="green-8"
label="Please provide SQL Query for Visualization"
hint="Press Apply Button to complete query preview"
/>
</div>
<div>
<q-h3 no-caps:label="getDashboard"></q-h3></div
></q-tab-panel>
<q-tab-panel name="transform"> </q-tab-panel>
<q-tab-panel name="alert"> </q-tab-panel>
</q-tab-panels>
</div>
<q-separator vertical class="col-shrink" style="width: 1px"></q-separator>
<div class="col q-pa-md">
<q-select
dense
outlined
v-model="vizualizationType"
:options="options"
label="Type of Visualization"
@update:model-value="(value) => selectVizualization(value)"
/>
<q-expansion-item
switch-toggle-side
:label="t(`Panel Option`)"
class="q-mt-md"
>
<q-form ref="adddashboardForm" class="q-pl-md">
<!-- <q-input
v-if="beingUpdated"
v-model="dashboardData.id"
:readonly="beingUpdated"
:disabled="beingUpdated"
:label="t('panel.id')"
/> -->
<q-input
v-model="panelName"
:label="t('panel.name') + '*'"
class="q-py-md showLabelOnTop"
stack-label
filled
dense
/>
<q-input
v-model="panelDesc"
type="textarea"
:label="t('panel.typeDesc') + '*'"
class="q-py-md showLabelOnTop"
stack-label
filled
dense
/>
</q-form>
</q-expansion-item>
</div>
</div>
</q-page>
</template>
<script lang="ts">
// @ts-nocheck
import { defineComponent, ref, watch, unref, toRaw, onMounted } from "vue";
import { useStore } from "vuex";
import { useQuasar, date } from "quasar";
import { useI18n } from "vue-i18n";
import * as Plotly from "plotly.js";
import {
getCurrentDashboard,
getCurrentPanel,
updateAllDashboardsFromBackend,
setCurrentPanelToDashboardList,
getDateConsumableDateTime,
modifySQLQuery,
} from "../utils/commons.ts";
import queryService from "../services/nativequery";
import ReactiveLineChart from "../components/shared/plotly/linechart.vue";
import ReactiveBarChart from "../components/shared/plotly/barchart.vue";
import ReactiveTableChart from "../components/shared/plotly/tablechart.vue";
import DateTime from "../plugins/logs/DateTime.vue";
import { useRouter } from "vue-router";
export default defineComponent({
name: "PageOrganization",
components: {
ReactiveLineChart,
ReactiveBarChart,
ReactiveTableChart,
DateTime,
},
setup() {
const { t } = useI18n();
const $q = useQuasar();
const selectedPerPage = ref<number>(20);
const router = useRouter();
const plotChart: any = ref({});
// let visualizationSelector = "";
const store = useStore();
const queryTextA: any = "";
const plotData: any = ref(null);
const refDateTime: any = ref(null);
let currentTimeObj = {};
const pagination: any = ref({
rowsPerPage: 20,
});
const queryText = ref(null);
const vizualizationType = ref("BarChart");
const dateVal = ref({
tab: "relative",
startDate: "",
startTime: "",
endDate: "",
endTime: "",
selectedRelativePeriod: "Minutes",
selectedRelativeValue: 15,
selectedFullTime: false,
});
const panelTab = ref("query");
const panelName = ref("");
const panelDesc = ref("");
// when the datetime filter changes then update the results
watch(dateVal.value, () => {
if (dateVal.value) {
dateChange(dateVal);
}
});
const dateChange = (dateValue: any) => {
const c = toRaw(unref(dateValue));
currentTimeObj = getDateConsumableDateTime(c);
return currentTimeObj;
// const b = moment(String(currentTimeObj.start_time)).format('YYYY-MM-DDThh:mm:ssZ')
};
const changePagination = (val: { label: string; value: any }) => {
selectedPerPage.value = val.value;
pagination.value.rowsPerPage = val.value;
};
const runQuery = () => {
const chartParams = {
title: "Found " + "2" + " hits in " + "10" + " ms",
};
currentTimeObj = dateChange(dateVal);
const sqlQueryModified = modifySQLQuery(currentTimeObj, queryText.value);
const query = { query: { sql: sqlQueryModified, sql_mode: "full" } };
queryService
.runquery(query, store.state.selectedOrganization.identifier)
.then((res) => {
if (vizualizationType.value === "Table") {
const header_vals = Object.keys(res.data.hits[0]);
let data_vals = [];
let obj = {};
let key = "";
for (obj in res.data.hits) {
let partobject = [];
for (key in header_vals) {
partobject.push(res.data.hits[obj][header_vals[key]]);
}
data_vals.push(partobject);
partobject = [];
}
let output = data_vals[0].map((_, colIndex) =>
data_vals.map((row) => row[colIndex])
);
const styledHeader = header_vals.map(
(header) => "<b>" + header + "</b>"
);
const columnColor = output[0].map((col, index) => {
if (index % 2 == 0) {
return "#FFF";
} else {
return "#C9CCE6";
}
});
plotData.value = {
id: 1,
type: "table",
header: {
values: styledHeader,
align: "center",
height: 30,
line: { width: 1, color: "black" },
fill: { color: "#7A80C2" },
font: { family: "Arial", size: 14, color: "white" },
},
cells: {
values: output,
align: "center",
height: 25,
line: { color: "black", width: 1 },
fill: { color: [columnColor] },
font: { family: "Arial", size: 11, color: ["black"] },
},
};
} else {
let x_data = [];
let y_data = [];
res.data.hits.map((data: any) => {
x_data.push(data.x_axis);
y_data.push(data.y_axis);
});
plotData.value = {
id: 1,
x: x_data,
y: y_data,
chartParams: chartParams,
};
}
$q.notify({
type: "positive",
message: "Query applied successfully.",
timeout: 5000,
});
})
.catch((error) => {
$q.notify({
type: "negative",
message: "Something went wrong!",
timeout: 5000,
});
});
};
const goBack = () => {
return router.push("/dashboard");
};
const selectVizualization = (value) => {
vizualizationType.value = value;
// visualizationSelector = value;
};
const getDashboard = () => {
return this.$route.query.dashboard;
};
const renderCurrentPanel = async (currentPanel) => {
vizualizationType.value = currentPanel.type || "BarChart";
queryText.value = currentPanel?.query?.[0];
panelName.value = currentPanel?.name || "";
panelDesc.value = currentPanel?.title || "";
if (queryText.value) await runQuery();
};
const onChartUpdate = ({ start, end }: { start: any; end: any }) => {
emit("updated:dates", { start, end });
};
const savePanelChangesToDashboard = async (
dashId: String,
panelId: String
) => {
await updateAllDashboardsFromBackend(store);
let modDashboardObject = toRaw(store.state.currentSelectedDashboard);
let dashboardList = toRaw(store.state.allCurrentDashboards);
const currentDashboard = await getCurrentDashboard(store, dashId);
const currentPanel = await getCurrentPanel(store, dashId, panelId);
currentTimeObj = dateChange(dateVal);
const sqlQueryModified = modifySQLQuery(currentTimeObj, queryText.value);
const maxI =
currentDashboard.layouts?.length > 0
? Math.max(...currentDashboard.layouts?.map((obj) => obj.i))
: 0;
const maxY =
currentDashboard.layouts?.length > 0
? Math.max(...currentDashboard.layouts?.map((obj) => obj.y))
: 0;
const newLayoutObj = {
x: 0,
y: currentDashboard.layouts?.length > 0 ? maxY + 10 : 0,
w: 12,
h: 12,
i: maxI + 1,
panelId: panelId,
static: false,
};
const panelObject2 = {
id: panelId,
query: [sqlQueryModified],
title: panelDesc.value,
type: vizualizationType.value,
name: panelName.value,
};
setCurrentPanelToDashboardList(
dashboardList,
dashId,
panelId,
panelObject2,
store,
newLayoutObj
);
return router.push({
path: "/viewDashboard",
query: { dashboard: dashId },
});
};
return {
t,
goBack,
queryText,
currentTimeObj,
savePanelChangesToDashboard,
onChartUpdate,
selectVizualization,
// getVizualizationSelector,
plotChart,
plotData,
runQuery,
store,
getDashboard,
vizualizationType,
queryTextA,
// visualizationSelector,
filterQuery: ref(""),
renderCurrentPanel,
refDateTime,
dateVal,
dateChange,
filterData(rows: string | any[], terms: string) {
const filtered = [];
terms = terms.toLowerCase();
for (let i = 0; i < rows.length; i++) {
if (rows[i]["name"].toLowerCase().includes(terms)) {
filtered.push(rows[i]);
}
}
return filtered;
},
model: ref(null),
options: ["TimeSeries", "BarChart", "Table"],
panelTab,
panelName,
panelDesc,
};
},
methods: {
goBacToDashboardList(evt, row) {
this.goBack();
},
selectVizualization() {
this.selectVizualization();
},
savePanelOnClick() {
this.savePanelChangesToDashboard(
this.$route.query.dashboard,
this.$route.query.panelId
);
},
},
activated() {
const dashboardList = toRaw(this.store.state.allCurrentDashboards);
let currentPanel = {};
for (const dashboard of dashboardList) {
if (this.$route.query.dashboard === dashboard.name) {
for (const panel of JSON.parse(toRaw(dashboard.details)).panels) {
if (this.$route.query.panelId === panel.id) {
currentPanel = panel;
}
}
}
}
this.renderCurrentPanel(currentPanel);
},
});
</script>
<style lang="scss" scoped>
.q-table {
&__top {
border-bottom: 1px solid $border-color;
justify-content: flex-end;
}
}
</style>