mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-06 08:09:24 +00:00
feat: traces UI (#589)
* added traces initial plugin and route * added composable useTraces * added trace details UI * add: added api call to get trace spans list * add: added span details sidebar * fix: converted bar sizing from px to % * updated: traces UI * added gantt chart in trace details page * fix: on close of trace details pop up reset span details sidebar data * updated sql query to get traces * updated traces UI 1. Removed select stream 2. Moved service_name and operation_name at top 3. Disabled sql mode 4. Remove add field in table icon from field list 5. change query added group by trace_id 6. Removed time interval 6. changed histogram chart to scatter * add: updated custom time range selection in trace details chart * fix: type errors and resplaced _timestamp with dynamic timestamp key from store * fix: removed unused files
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<svg width="40" height="36" viewBox="0 0 40 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M26.2 35.75V29.5H18.5V9H13.85V15.5H0V-0.25H13.85V6H26.2V-0.25H40V15.5H26.2V9H21.5V26.5H26.2V20H40V35.75H26.2ZM29.2 12.5H37V2.75H29.2V12.5ZM29.2 32.75H37V23H29.2V32.75ZM3 12.5H10.85V2.75H3V12.5Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 322 B |
@@ -21,6 +21,7 @@ import AddPanel from "@/views/Dashboards/addPanel/AddPanel.vue";
|
||||
import MemberSubscription from "@/views/MemberSubscription.vue";
|
||||
import Search from "@/views/Search.vue";
|
||||
import AppMetrics from "@/views/AppMetrics.vue";
|
||||
import AppTraces from "@/views/AppTraces.vue";
|
||||
import LogStream from "@/views/LogStream.vue";
|
||||
import {
|
||||
FunctionList,
|
||||
@@ -71,6 +72,14 @@ const useRoutes = () => {
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "traces",
|
||||
name: "traces",
|
||||
component: AppTraces,
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "logstreams",
|
||||
name: "logstreams",
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
// 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 { reactive } from "vue";
|
||||
import { useLocalTraceFilterField } from "@/utils/zincutils";
|
||||
|
||||
const defaultObject = {
|
||||
organizationIdetifier: "",
|
||||
runQuery: false,
|
||||
loading: false,
|
||||
|
||||
config: {
|
||||
splitterModel: 20,
|
||||
lastSplitterPosition: 0,
|
||||
splitterLimit: [0, 40],
|
||||
refreshTimes: [
|
||||
[
|
||||
{ label: "5 sec", value: 5 },
|
||||
{ label: "1 min", value: 60 },
|
||||
{ label: "1 hr", value: 3600 },
|
||||
],
|
||||
[
|
||||
{ label: "10 sec", value: 10 },
|
||||
{ label: "5 min", value: 300 },
|
||||
{ label: "2 hr", value: 7200 },
|
||||
],
|
||||
[
|
||||
{ label: "15 sec", value: 15 },
|
||||
{ label: "15 min", value: 900 },
|
||||
{ label: "1 day", value: 86400 },
|
||||
],
|
||||
[
|
||||
{ label: "30 sec", value: 30 },
|
||||
{ label: "30 min", value: 1800 },
|
||||
],
|
||||
],
|
||||
},
|
||||
meta: {
|
||||
refreshInterval: "0",
|
||||
refreshIntervalLabel: "Off",
|
||||
showFields: true,
|
||||
showQuery: true,
|
||||
showHistogram: true,
|
||||
showDetailTab: false,
|
||||
showTraceDetails: false,
|
||||
sqlMode: false,
|
||||
resultGrid: {
|
||||
wrapCells: false,
|
||||
manualRemoveFields: false,
|
||||
rowsPerPage: 150,
|
||||
chartInterval: "1 second",
|
||||
chartKeyFormat: "HH:mm:ss",
|
||||
navigation: {
|
||||
currentRowIndex: 0,
|
||||
},
|
||||
},
|
||||
scrollInfo: {},
|
||||
},
|
||||
data: {
|
||||
query: "",
|
||||
parsedQuery: {},
|
||||
errorMsg: "",
|
||||
errorCode: 0,
|
||||
additionalErrorMsg: "",
|
||||
stream: {
|
||||
streamLists: [],
|
||||
selectedStream: { label: "", value: "" },
|
||||
selectedStreamFields: [],
|
||||
selectedFields: <string[]>[],
|
||||
filterField: "",
|
||||
addToFilter: "",
|
||||
functions: [],
|
||||
},
|
||||
resultGrid: {
|
||||
currentDateTime: new Date(),
|
||||
currentPage: 0,
|
||||
columns: <any>[],
|
||||
},
|
||||
transforms: <any>[],
|
||||
queryResults: <any>[],
|
||||
sortedQueryResults: <any>[],
|
||||
streamResults: <any>[],
|
||||
histogram: <any>{},
|
||||
editorValue: "",
|
||||
datetime: {
|
||||
tab: "relative",
|
||||
relative: {
|
||||
period: {
|
||||
label: "Minutes",
|
||||
value: "Minutes",
|
||||
},
|
||||
value: 15,
|
||||
},
|
||||
absolute: {
|
||||
date: {
|
||||
from: "",
|
||||
to: "",
|
||||
},
|
||||
startTime: "00:00",
|
||||
endTime: "23:59",
|
||||
},
|
||||
},
|
||||
searchAround: {
|
||||
indexTimestamp: 0,
|
||||
size: <number>10,
|
||||
histogramHide: false,
|
||||
},
|
||||
traceDetails: {
|
||||
traceId: "",
|
||||
spanList: [],
|
||||
loading: false,
|
||||
selectedSpanId: null,
|
||||
showSpanDetails: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// let searchObj = reactive(structuredClone(defaultObject));
|
||||
// let localLogsObj:any = useLocalLogsObj();
|
||||
// let searchObj = {};
|
||||
// if (typeof localLogsObj === "object") {
|
||||
// searchObj = localLogsObj.value;
|
||||
// } else {
|
||||
let searchObj = reactive(Object.assign({}, defaultObject));
|
||||
// }
|
||||
|
||||
const useLogs = () => {
|
||||
const resetSearchObj = () => {
|
||||
// delete searchObj.data;
|
||||
searchObj = reactive(Object.assign({}, defaultObject));
|
||||
};
|
||||
const updatedLocalLogFilterField = (): void => {
|
||||
const identifier: string = searchObj.organizationIdetifier || "default";
|
||||
const selectedFields: any =
|
||||
useLocalTraceFilterField()?.value != null
|
||||
? useLocalTraceFilterField()?.value
|
||||
: {};
|
||||
selectedFields[
|
||||
`${identifier}_${searchObj.data.stream.selectedStream.value}`
|
||||
] = searchObj.data.stream.selectedFields;
|
||||
useLocalTraceFilterField(selectedFields);
|
||||
};
|
||||
|
||||
return { searchObj, resetSearchObj, updatedLocalLogFilterField };
|
||||
};
|
||||
|
||||
export default useLogs;
|
||||
@@ -348,6 +348,11 @@ export default defineComponent({
|
||||
icon: "img:" + getImageURL("images/left_nav/metrics.svg"),
|
||||
link: "/metrics",
|
||||
},
|
||||
{
|
||||
title: t("menu.traces"),
|
||||
icon: "img:" + getImageURL("images/left_nav/traces.svg"),
|
||||
link: "/traces",
|
||||
},
|
||||
{
|
||||
title: t("menu.dashboard"),
|
||||
icon: "img:" + getImageURL("images/left_nav/dashb2.svg"),
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"user": "Users",
|
||||
"index": "Streams",
|
||||
"search": "Logs",
|
||||
"traces": "Traces",
|
||||
"template": "Template",
|
||||
"signOut": "Sign Out",
|
||||
"account": "Account",
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
|
||||
import Logs from "./logs/Index.vue";
|
||||
import AppMetrics from "./metrics/Index.vue";
|
||||
import AppTraces from "./traces/Index.vue";
|
||||
|
||||
export default {
|
||||
install: (app: any, options: any) => {
|
||||
app.component("zinc-logs", Logs);
|
||||
app.component("zinc-metrics", AppMetrics);
|
||||
app.component("zinc-traces", AppTraces);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<!-- 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>
|
||||
<div class="flex items-start">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<div v-if="isOpen">
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "AppCollapse",
|
||||
props: {
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
headerStyle: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const isOpen: any = ref(props.open);
|
||||
function updateCollapse() {
|
||||
isOpen.value = !isOpen.value;
|
||||
}
|
||||
return {
|
||||
isOpen,
|
||||
updateCollapse,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,398 @@
|
||||
<!-- 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 no-wrap searchdetaildialog">
|
||||
<q-card-section class="q-pa-md q-pb-md">
|
||||
<div class="row items-center no-wrap">
|
||||
<div class="col">
|
||||
<div
|
||||
class="text-body1 text-bold text-dark"
|
||||
data-test="log-detail-title-text"
|
||||
>
|
||||
{{ t("search.rowDetail") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
v-close-popup
|
||||
round
|
||||
flat
|
||||
:icon="'img:' + getImageURL('images/common/close_icon.svg')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<div class="row justify-between">
|
||||
<div class="col-10">
|
||||
<q-tabs v-model="tab" shrink align="left">
|
||||
<q-tab data-test="log-detail-table-tab" name="table" label="Table" />
|
||||
<q-tab data-test="log-detail-json-tab" name="json" label="JSON" />
|
||||
</q-tabs>
|
||||
</div>
|
||||
<div
|
||||
v-show="tab === 'table'"
|
||||
class="col-2 flex justify-end align-center q-pr-md"
|
||||
>
|
||||
<q-toggle
|
||||
data-test="log-detail-wrap-values-toggle-btn"
|
||||
v-model="shouldWrapValues"
|
||||
label="Wrap"
|
||||
color="primary"
|
||||
size="xs"
|
||||
@update:model-value="toggleWrapLogDetails"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-tab-panels data-test="log-detail-tab-container" v-model="tab" animated>
|
||||
<q-tab-panel name="table">
|
||||
<q-card-section
|
||||
class="q-pa-none q-mb-lg"
|
||||
data-test="log-detail-table-content"
|
||||
>
|
||||
<div
|
||||
v-if="rowData.length == 0"
|
||||
class="q-pt-md"
|
||||
style="max-width: 350px"
|
||||
>
|
||||
No data available.
|
||||
</div>
|
||||
<div v-else class="indexDetailsContainer">
|
||||
<q-list
|
||||
separator
|
||||
style="height: calc(100vh - 290)"
|
||||
class="q-px-none q-py-none"
|
||||
>
|
||||
<q-item class="list-head">
|
||||
<q-item-section class="text-bold col-3">
|
||||
{{ t("search.sourceName") }}
|
||||
</q-item-section>
|
||||
<q-item-section class="text-bold col-9">
|
||||
{{ t("search.sourceValue") }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item
|
||||
v-for="(key, value) in rowData"
|
||||
:key="'field_' + value"
|
||||
class="list-item"
|
||||
>
|
||||
<q-item-section
|
||||
:data-test="`log-detail-${value}-key`"
|
||||
class="col-3"
|
||||
>{{ value }}</q-item-section
|
||||
>
|
||||
<q-item-section
|
||||
class="col-9"
|
||||
:class="!shouldWrapValues ? 'ellipsis' : ''"
|
||||
style="display: inline;relative"
|
||||
>
|
||||
<q-btn-dropdown
|
||||
data-test="log-details-include-exclude-field-btn"
|
||||
size="0.5rem"
|
||||
outlined
|
||||
filled
|
||||
dense
|
||||
class="q-mr-sm pointer"
|
||||
:name="'img:' + getImageURL('images/common/add_icon.svg')"
|
||||
>
|
||||
<q-list>
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
data-test="log-details-include-field-btn"
|
||||
@click="
|
||||
toggleIncludeSearchTerm(`${value}='${key}'`)
|
||||
"
|
||||
><q-icon
|
||||
title="Add to search query"
|
||||
:name="
|
||||
'img:' +
|
||||
getImageURL('images/common/add_icon.svg')
|
||||
"
|
||||
size="1rem"
|
||||
outlined
|
||||
filled
|
||||
dense
|
||||
class="q-mr-sm pointer"
|
||||
></q-icon
|
||||
>Inlcude Search Term</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item clickable v-close-popup>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
data-test="log-details-exclude-field-btn"
|
||||
@click="
|
||||
toggleExcludeSearchTerm(`${value}!='${key}'`)
|
||||
"
|
||||
><q-icon
|
||||
title="Add to search query"
|
||||
:name="
|
||||
'img:' +
|
||||
getImageURL('images/common/add_icon.svg')
|
||||
"
|
||||
size="1rem"
|
||||
outlined
|
||||
filled
|
||||
dense
|
||||
class="q-mr-sm pointer"
|
||||
></q-icon
|
||||
>Exclude Search Term</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
<pre
|
||||
:data-test="`log-detail-${value}-value`"
|
||||
class="table-pre"
|
||||
:style="{
|
||||
'white-space': !shouldWrapValues ? 'nowrap' : 'pre-wrap',
|
||||
}"
|
||||
>{{ key }}</pre
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item></q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="json" class="q-pa-none">
|
||||
<q-card-section
|
||||
data-test="log-detail-json-content"
|
||||
class="q-pa-none q-mb-lg"
|
||||
>
|
||||
<div class="indexDetailsContainer">
|
||||
<pre class="json-pre">{{ rowData }}</pre>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
|
||||
<q-separator />
|
||||
<q-card-section class="q-pa-md q-pb-md">
|
||||
<div class="row items-center no-wrap">
|
||||
<div class="col-2">
|
||||
<q-btn
|
||||
class="text-bold"
|
||||
text-color="light-text"
|
||||
no-caps
|
||||
:disabled="currentIndex <= 0"
|
||||
outline
|
||||
@click="$emit('showPrevDetail', false, true)"
|
||||
icon="navigate_before"
|
||||
label="Previous"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-8 row justify-center align-center q-gutter-sm">
|
||||
<div style="line-height: 40px; font-weight: bold">No of Records:</div>
|
||||
<div class="" style="minwidth: 150px">
|
||||
<q-select
|
||||
v-model="selectedRelativeValue"
|
||||
:options="recordSizeOptions"
|
||||
dense
|
||||
filled
|
||||
></q-select>
|
||||
</div>
|
||||
<div class="">
|
||||
<q-btn
|
||||
data-test="logs-detail-table-search-around-btn"
|
||||
class="text-bold"
|
||||
text-color="light-text"
|
||||
no-caps
|
||||
:disabled="currentIndex >= totalLength - 1"
|
||||
outline
|
||||
label="Search Around"
|
||||
@click="searchTimeBoxed(rowData, Number(selectedRelativeValue))"
|
||||
padding="sm md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2 items-end">
|
||||
<q-btn
|
||||
data-test="log-detail-next-detail-btn"
|
||||
class="text-bold"
|
||||
text-color="light-text"
|
||||
no-caps
|
||||
:disabled="currentIndex >= totalLength - 1"
|
||||
outline
|
||||
@click="$emit('showNextDetail', true, false)"
|
||||
icon-right="navigate_next"
|
||||
label="Next"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onBeforeMount, onBeforeUnmount } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getImageURL } from "../../utils/zincutils";
|
||||
|
||||
const defaultValue: any = () => {
|
||||
return {
|
||||
data: {},
|
||||
};
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "SearchDetail",
|
||||
emits: [
|
||||
"showPrevDetail",
|
||||
"showNextDetail",
|
||||
"add:searchterm",
|
||||
"remove:searchterm",
|
||||
"search:timeboxed",
|
||||
],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => defaultValue(),
|
||||
},
|
||||
currentIndex: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
totalLength: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleIncludeSearchTerm(term: string) {
|
||||
// if (flag == false) {
|
||||
this.$emit("add:searchterm", term);
|
||||
// } else {
|
||||
// this.$emit("remove:searchterm", term);
|
||||
// }
|
||||
},
|
||||
toggleExcludeSearchTerm(term: string) {
|
||||
// if (flag == false) {
|
||||
this.$emit("add:searchterm", term);
|
||||
// } else {
|
||||
// this.$emit("remove:searchterm", term);
|
||||
// }
|
||||
},
|
||||
searchTimeBoxed(rowData: any, size: number) {
|
||||
this.$emit("search:timeboxed", {
|
||||
key: rowData._timestamp,
|
||||
size: size,
|
||||
});
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const rowData: any = ref({});
|
||||
const router = useRouter();
|
||||
const tab = ref("table");
|
||||
const selectedRelativeValue = ref("10");
|
||||
const recordSizeOptions: any = ref([10, 20, 50, 100, 200, 500, 1000]);
|
||||
const shouldWrapValues: any = ref(false);
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (window.localStorage.getItem("wrap-log-details") === null) {
|
||||
window.localStorage.setItem("wrap-log-details", "false");
|
||||
}
|
||||
shouldWrapValues.value =
|
||||
window.localStorage.getItem("wrap-log-details") === "true";
|
||||
});
|
||||
|
||||
const toggleWrapLogDetails = () => {
|
||||
window.localStorage.setItem(
|
||||
"wrap-log-details",
|
||||
shouldWrapValues.value ? "true" : "false"
|
||||
);
|
||||
};
|
||||
|
||||
const flattenJSONObject = (obj: any, param: string) => {
|
||||
let newObj: any = {};
|
||||
for (let key in obj) {
|
||||
if (typeof obj[key] === "object") {
|
||||
// let childJSON = JSON.stringify(obj[key]);
|
||||
// let unflatten = flattenJSONObject(obj[key], key + ".");
|
||||
newObj = {
|
||||
...newObj,
|
||||
...flattenJSONObject(obj[key], param + key + "."),
|
||||
};
|
||||
} else {
|
||||
newObj[param + key] = obj[key];
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
};
|
||||
|
||||
return {
|
||||
t,
|
||||
router,
|
||||
rowData,
|
||||
tab,
|
||||
flattenJSONObject,
|
||||
selectedRelativeValue,
|
||||
recordSizeOptions,
|
||||
getImageURL,
|
||||
shouldWrapValues,
|
||||
toggleWrapLogDetails,
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
const newObj = await this.flattenJSONObject(this.modelValue, "");
|
||||
this.rowData = newObj;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.searchdetaildialog {
|
||||
width: 60vw;
|
||||
}
|
||||
|
||||
.q-item__section {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.indexDetailsContainer .q-list .q-item {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.q-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table-pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
display: inline;
|
||||
font-weight: normal;
|
||||
font-family: Nunito Sans, sans-serif;
|
||||
}
|
||||
|
||||
.json-pre {
|
||||
height: calc(100vh - 290px);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,601 @@
|
||||
<!-- 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 class="column index-menu">
|
||||
<div class="index-table q-mt-xs">
|
||||
<q-table
|
||||
data-test="log-search-index-list-fields-table"
|
||||
v-model="searchObj.data.stream.selectedFields"
|
||||
:visible-columns="['name']"
|
||||
:rows="searchObj.data.stream.selectedStreamFields"
|
||||
row-key="name"
|
||||
:filter="searchObj.data.stream.filterField"
|
||||
:filter-method="filterFieldFn"
|
||||
:pagination="{ rowsPerPage: 10000 }"
|
||||
hide-header
|
||||
hide-bottom
|
||||
:wrap-cells="searchObj.meta.resultGrid.wrapCells"
|
||||
class="field-table"
|
||||
id="tracesFieldList"
|
||||
>
|
||||
<template #body-cell-name="props">
|
||||
<q-tr :props="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
class="field_list"
|
||||
:class="
|
||||
searchObj.data.stream.selectedFields.includes(props.row.name)
|
||||
? 'selected'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<!-- TODO OK : Repeated code make seperate component to display field -->
|
||||
<div
|
||||
v-if="props.row.ftsKey || !props.row.showValues"
|
||||
class="field-container flex content-center ellipsis q-pl-lg q-pr-sm"
|
||||
:title="props.row.name"
|
||||
>
|
||||
<div class="field_label ellipsis">
|
||||
{{ props.row.name }}
|
||||
</div>
|
||||
<div class="field_overlay">
|
||||
<q-icon
|
||||
:name="'img:' + getImageURL('images/common/add_icon.svg')"
|
||||
:data-test="`log-search-index-list-filter-${props.row.name}-field-btn`"
|
||||
style="margin-right: 0.375rem"
|
||||
size="1rem"
|
||||
@click.stop="addToFilter(props.row.name)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<q-expansion-item
|
||||
v-else
|
||||
dense
|
||||
switch-toggle-side
|
||||
:label="props.row.name"
|
||||
expand-icon-class="field-expansion-icon"
|
||||
:expand-icon="
|
||||
'img:' + getImageURL('images/common/down-solid.svg')
|
||||
"
|
||||
:expanded-icon="
|
||||
'img:' + getImageURL('images/common/up-solid.svg')
|
||||
"
|
||||
@before-show="(event: any) => openFilterCreator(event, props.row)"
|
||||
>
|
||||
<template v-slot:header>
|
||||
<div
|
||||
class="flex content-center ellipsis"
|
||||
:title="props.row.name"
|
||||
>
|
||||
<div class="field_label ellipsis">
|
||||
{{ props.row.name }}
|
||||
</div>
|
||||
<div class="field_overlay">
|
||||
<q-icon
|
||||
:data-test="`log-search-index-list-filter-${props.row.name}-field-btn`"
|
||||
:name="
|
||||
'img:' + getImageURL('images/common/add_icon.svg')
|
||||
"
|
||||
style="margin-right: 0.375rem"
|
||||
size="1rem"
|
||||
@click.stop="addToFilter(props.row.name)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<q-card>
|
||||
<q-card-section class="q-pl-md q-pr-xs q-py-xs">
|
||||
<div class="filter-values-container">
|
||||
<div
|
||||
v-show="fieldValues[props.row.name]?.isLoading"
|
||||
class="q-pl-md q-py-xs"
|
||||
style="height: 60px"
|
||||
>
|
||||
<q-inner-loading
|
||||
size="xs"
|
||||
:showing="fieldValues[props.row.name]?.isLoading"
|
||||
label="Fetching values..."
|
||||
label-style="font-size: 1.1em"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-show="
|
||||
!fieldValues[props.row.name]?.values?.length &&
|
||||
!fieldValues[props.row.name]?.isLoading
|
||||
"
|
||||
class="q-pl-md q-py-xs text-subtitle2"
|
||||
>
|
||||
No values found
|
||||
</div>
|
||||
<div
|
||||
v-for="value in fieldValues[props.row.name]?.values ||
|
||||
[]"
|
||||
:key="value.key"
|
||||
>
|
||||
<q-list dense>
|
||||
<q-item tag="label" class="q-pr-none">
|
||||
<div
|
||||
class="flex row wrap justify-between"
|
||||
style="width: calc(100% - 36px)"
|
||||
>
|
||||
<div
|
||||
:title="value.key"
|
||||
class="ellipsis q-pr-xs"
|
||||
style="width: calc(100% - 50px)"
|
||||
>
|
||||
{{ value.key }}
|
||||
</div>
|
||||
<div
|
||||
:title="value.count"
|
||||
class="ellipsis text-right q-pr-sm"
|
||||
style="width: 50px"
|
||||
>
|
||||
{{ value.count }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex row">
|
||||
<q-icon
|
||||
:name="
|
||||
'img:' +
|
||||
getImageURL('images/common/add_icon.svg')
|
||||
"
|
||||
class="q-mr-xs"
|
||||
size="1rem"
|
||||
@click="
|
||||
addSearchTerm(
|
||||
`${props.row.name}='${value.key}'`
|
||||
)
|
||||
"
|
||||
/>
|
||||
<q-icon
|
||||
:name="
|
||||
'img:' +
|
||||
getImageURL('images/common/remove_icon.svg')
|
||||
"
|
||||
size="1rem"
|
||||
@click="
|
||||
addSearchTerm(
|
||||
`${props.row.name}!='${value.key}'`
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template #top-right>
|
||||
<q-input
|
||||
data-test="log-search-index-list-field-search-input"
|
||||
v-model="searchObj.data.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, ref, type Ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useStore } from "vuex";
|
||||
import { useQuasar } from "quasar";
|
||||
import { useRouter } from "vue-router";
|
||||
import useTraces from "../../composables/useTraces";
|
||||
import { getImageURL } from "../../utils/zincutils";
|
||||
import streamService from "../../services/stream";
|
||||
import { getConsumableDateTime } from "@/utils/commons";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ComponentSearchIndexSelect",
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const $q = useQuasar();
|
||||
const { searchObj, updatedLocalLogFilterField } = useTraces();
|
||||
const streamOptions: any = ref(searchObj.data.stream.streamLists);
|
||||
const fieldValues: Ref<{
|
||||
[key: string | number]: {
|
||||
isLoading: boolean;
|
||||
values: { key: string; count: string }[];
|
||||
};
|
||||
}> = ref({});
|
||||
|
||||
const filterStreamFn = (val: string, update: any) => {
|
||||
update(() => {
|
||||
streamOptions.value = searchObj.data.stream.streamLists;
|
||||
const needle = val.toLowerCase();
|
||||
streamOptions.value = streamOptions.value.filter(
|
||||
(v: any) => v.label.toLowerCase().indexOf(needle) > -1
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
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 addToFilter = (field: any) => {
|
||||
searchObj.data.stream.addToFilter = field;
|
||||
};
|
||||
|
||||
function clickFieldFn(row: { name: never }, pageIndex: number) {
|
||||
if (searchObj.data.stream.selectedFields.includes(row.name)) {
|
||||
searchObj.data.stream.selectedFields =
|
||||
searchObj.data.stream.selectedFields.filter(
|
||||
(v: any) => v !== row.name
|
||||
);
|
||||
} else {
|
||||
searchObj.data.stream.selectedFields.push(row.name);
|
||||
}
|
||||
searchObj.organizationIdetifier =
|
||||
store.state.selectedOrganization.identifier;
|
||||
updatedLocalLogFilterField();
|
||||
}
|
||||
|
||||
const openFilterCreator = (event: any, { name, ftsKey }: any) => {
|
||||
if (ftsKey) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
const timestamps = getConsumableDateTime(searchObj.data.datetime);
|
||||
const startISOTimestamp: any =
|
||||
new Date(timestamps.start_time.toISOString()).getTime() * 1000;
|
||||
const endISOTimestamp: any =
|
||||
new Date(timestamps.end_time.toISOString()).getTime() * 1000;
|
||||
|
||||
fieldValues.value[name] = {
|
||||
isLoading: true,
|
||||
values: [],
|
||||
};
|
||||
streamService
|
||||
.fieldValues({
|
||||
org_identifier: store.state.selectedOrganization.identifier,
|
||||
stream_name: searchObj.data.stream.selectedStream.value,
|
||||
start_time: startISOTimestamp,
|
||||
end_time: endISOTimestamp,
|
||||
fields: [name],
|
||||
size: 10,
|
||||
type: "traces",
|
||||
})
|
||||
.then((res: any) => {
|
||||
if (res.data.hits.length) {
|
||||
fieldValues.value[name]["values"] = res.data.hits
|
||||
.find((field: any) => field.field === name)
|
||||
.values.map((value: any) => {
|
||||
return {
|
||||
key: value.key ? value.key : "null",
|
||||
count: formatNumberWithPrefix(value.num),
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
$q.notify({
|
||||
type: "negative",
|
||||
message: `Error while fetching values for ${name}`,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
fieldValues.value[name]["isLoading"] = false;
|
||||
});
|
||||
};
|
||||
|
||||
function formatNumberWithPrefix(number: number) {
|
||||
if (number >= 1000000000) {
|
||||
return (number / 1000000000).toFixed(1) + "B";
|
||||
} else if (number >= 1000000) {
|
||||
return (number / 1000000).toFixed(1) + "M";
|
||||
} else if (number >= 1000) {
|
||||
return (number / 1000).toFixed(1) + "K";
|
||||
} else {
|
||||
return number.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const addSearchTerm = (term: string) => {
|
||||
// searchObj.meta.showDetailTab = false;
|
||||
searchObj.data.stream.addToFilter = term;
|
||||
};
|
||||
|
||||
return {
|
||||
t,
|
||||
store,
|
||||
router,
|
||||
searchObj,
|
||||
streamOptions,
|
||||
filterFieldFn,
|
||||
addToFilter,
|
||||
clickFieldFn,
|
||||
getImageURL,
|
||||
filterStreamFn,
|
||||
openFilterCreator,
|
||||
addSearchTerm,
|
||||
fieldValues,
|
||||
};
|
||||
},
|
||||
});
|
||||
</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%;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.index-table {
|
||||
width: 100%;
|
||||
// border: 1px solid rgba(0, 0, 0, 0.02);
|
||||
|
||||
.q-table {
|
||||
display: table;
|
||||
table-layout: fixed !important;
|
||||
}
|
||||
tr {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
tbody,
|
||||
tr,
|
||||
td {
|
||||
width: 100%;
|
||||
display: block;
|
||||
height: fit-content;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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_label {
|
||||
pointer-events: none;
|
||||
font-size: 0.825rem;
|
||||
position: relative;
|
||||
display: inline;
|
||||
z-index: 2;
|
||||
left: 0;
|
||||
// text-transform: capitalize;
|
||||
}
|
||||
|
||||
.field-container {
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.field_overlay {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
background-color: #e8e8e8;
|
||||
padding: 0 6px;
|
||||
visibility: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.q-icon {
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
margin: 0 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
.field_overlay {
|
||||
background-color: rgba(89, 96, 178, 0.3);
|
||||
|
||||
.field_icons {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.field-container {
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
<style lang="scss">
|
||||
.index-table {
|
||||
.q-table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
|
||||
.q-expansion-item {
|
||||
.q-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
height: 25px !important;
|
||||
min-height: 25px !important;
|
||||
}
|
||||
.q-item__section--avatar {
|
||||
min-width: 12px;
|
||||
max-width: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.filter-values-container {
|
||||
.q-item {
|
||||
padding-left: 4px;
|
||||
|
||||
.q-focus-helper {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.q-item-type {
|
||||
&:hover {
|
||||
.field_overlay {
|
||||
visibility: visible;
|
||||
|
||||
.q-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.field-expansion-icon {
|
||||
img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field-container {
|
||||
&:hover {
|
||||
.field_overlay {
|
||||
visibility: visible;
|
||||
|
||||
.q-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field_list {
|
||||
&.selected {
|
||||
.q-expansion-item {
|
||||
background-color: rgba(89, 96, 178, 0.3);
|
||||
}
|
||||
|
||||
.field_overlay {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,332 @@
|
||||
<!-- 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 data-test="query-editor" ref="editorRef" id="editor"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted, onUpdated } from "vue";
|
||||
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
|
||||
import { useStore } from "vuex";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
query: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
functions: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
emits: ["update-query", "run-query"],
|
||||
setup(props, { emit }) {
|
||||
const editorRef: any = ref();
|
||||
let editorObj: any = null;
|
||||
const store = useStore();
|
||||
|
||||
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}' and '${1:params}' ",
|
||||
range: range,
|
||||
insertTextRules:
|
||||
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
},
|
||||
{
|
||||
label: "not between",
|
||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
||||
insertText: "not between '${1:params}' and '${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 == store.state.zoConfig.timestamp_column) {
|
||||
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: "FFFFFF" }],
|
||||
colors: {
|
||||
"editor.foreground": "#000000",
|
||||
"editor.background": "#fafafa",
|
||||
"editorCursor.foreground": "#000000",
|
||||
"editor.lineHighlightBackground": "#FFFFFF",
|
||||
"editorLineNumber.foreground": "#ececec",
|
||||
"editor.border": "#ececec",
|
||||
},
|
||||
});
|
||||
|
||||
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_ignore_case('${lastElement}')`,
|
||||
kind: monaco.languages.CompletionItemKind.Text,
|
||||
insertText: `match_all_ignore_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 },
|
||||
});
|
||||
|
||||
editorObj.onDidChangeModelContent((e: any) => {
|
||||
emit("update-query", editorObj.getValue());
|
||||
});
|
||||
|
||||
editorObj.createContextKey("ctrlenter", true);
|
||||
editorObj.addCommand(
|
||||
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
|
||||
function () {
|
||||
emit("run-query");
|
||||
},
|
||||
"ctrlenter"
|
||||
);
|
||||
|
||||
// editorObj.onDidBlurEditorWidget(() => {
|
||||
// // onBlur();
|
||||
// });
|
||||
|
||||
// // editorObj.dispose();
|
||||
});
|
||||
|
||||
const setValue = (value: string) => {
|
||||
if (editorObj?.setValue) editorObj.setValue(value);
|
||||
};
|
||||
|
||||
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>
|
||||
@@ -0,0 +1,431 @@
|
||||
<!-- 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 class="search-bar-component" id="searchBarComponent">
|
||||
<!-- {{ searchObj.data }} -->
|
||||
<div class="row q-my-xs">
|
||||
<div class="float-right col">
|
||||
<q-toggle
|
||||
data-test="logs-search-bar-show-query-toggle-btn"
|
||||
v-model="searchObj.meta.showQuery"
|
||||
:label="t('search.showQueryLabel')"
|
||||
/>
|
||||
<q-toggle
|
||||
data-test="logs-search-bar-show-fields-toggle-btn"
|
||||
v-model="searchObj.meta.showFields"
|
||||
:label="t('search.showFieldLabel')"
|
||||
/>
|
||||
<q-toggle
|
||||
data-test="logs-search-bar-show-histogram-toggle-btn"
|
||||
v-bind:disable="searchObj.meta.sqlMode"
|
||||
v-model="searchObj.meta.showHistogram"
|
||||
:label="t('search.showHistogramLabel')"
|
||||
/>
|
||||
<syntax-guide
|
||||
data-test="logs-search-bar-sql-mode-toggle-btn"
|
||||
:sqlmode="searchObj.meta.sqlMode"
|
||||
></syntax-guide>
|
||||
</div>
|
||||
<div class="float-right col-auto">
|
||||
<q-btn
|
||||
v-if="searchObj.data.queryResults.hits"
|
||||
class="q-mr-sm float-left download-logs-btn"
|
||||
size="sm"
|
||||
:disable="!searchObj.data.queryResults.hits.length"
|
||||
icon="download"
|
||||
title="Export logs"
|
||||
@click="downloadLogs"
|
||||
></q-btn>
|
||||
<div class="float-left">
|
||||
<date-time
|
||||
data-test="logs-search-bar-date-time-dropdown"
|
||||
@date-change="updateDateTime"
|
||||
/>
|
||||
</div>
|
||||
<div class="search-time q-pl-sm float-left">
|
||||
<q-btn
|
||||
data-test="logs-search-bar-refresh-btn"
|
||||
data-cy="search-bar-refresh-button"
|
||||
dense
|
||||
rounded="sm"
|
||||
flat
|
||||
title="Run query"
|
||||
class="q-pa-none search-button"
|
||||
@click="searchData"
|
||||
:disable="
|
||||
searchObj.loading || searchObj.data.streamResults.length == 0
|
||||
"
|
||||
>Run query</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" v-if="searchObj.meta.showQuery">
|
||||
<div class="col">
|
||||
<query-editor
|
||||
ref="queryEditorRef"
|
||||
class="monaco-editor"
|
||||
v-model:query="searchObj.data.query"
|
||||
v-model:fields="searchObj.data.stream.selectedStreamFields"
|
||||
v-model:functions="searchObj.data.stream.functions"
|
||||
@update-query="updateQueryValue"
|
||||
@run-query="searchData"
|
||||
></query-editor>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useQuasar } from "quasar";
|
||||
|
||||
import DateTime from "@/components/DateTime.vue";
|
||||
import useTraces from "@/composables/useTraces";
|
||||
import QueryEditor from "./QueryEditor.vue";
|
||||
import SyntaxGuide from "./SyntaxGuide.vue";
|
||||
|
||||
import { Parser } from "node-sql-parser";
|
||||
import segment from "@/services/segment_analytics";
|
||||
import config from "@/aws-exports";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ComponentSearchSearchBar",
|
||||
components: {
|
||||
DateTime,
|
||||
QueryEditor,
|
||||
SyntaxGuide,
|
||||
},
|
||||
emits: ["searchdata"],
|
||||
methods: {
|
||||
searchData() {
|
||||
if (this.searchObj.loading == false) {
|
||||
// this.searchObj.runQuery = true;
|
||||
this.$emit("searchdata");
|
||||
}
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const $q = useQuasar();
|
||||
const btnRefreshInterval = ref(null);
|
||||
|
||||
const { searchObj } = useTraces();
|
||||
const queryEditorRef = ref(null);
|
||||
|
||||
const parser = new Parser();
|
||||
let streamName = "";
|
||||
|
||||
const refreshTimeChange = (item) => {
|
||||
searchObj.meta.refreshInterval = item.value;
|
||||
searchObj.meta.refreshIntervalLabel = item.label;
|
||||
btnRefreshInterval.value = false;
|
||||
};
|
||||
|
||||
const updateQueryValue = (value: string) => {
|
||||
searchObj.data.editorValue = value;
|
||||
searchObj.data.query = value;
|
||||
if (searchObj.meta.sqlMode == true) {
|
||||
searchObj.data.parsedQuery = parser.astify(value);
|
||||
if (searchObj.data.parsedQuery.from.length > 0) {
|
||||
if (
|
||||
searchObj.data.parsedQuery.from[0].table !==
|
||||
searchObj.data.stream.selectedStream.value &&
|
||||
searchObj.data.parsedQuery.from[0].table !== streamName
|
||||
) {
|
||||
let streamFound = false;
|
||||
streamName = searchObj.data.parsedQuery.from[0].table;
|
||||
searchObj.data.streamResults.list.forEach((stream) => {
|
||||
if (stream.name == searchObj.data.parsedQuery.from[0].table) {
|
||||
streamFound = true;
|
||||
let itemObj = {
|
||||
label: stream.name,
|
||||
value: stream.name,
|
||||
};
|
||||
searchObj.data.stream.selectedStream = itemObj;
|
||||
stream.schema.forEach((field) => {
|
||||
searchObj.data.stream.selectedStreamFields.push({
|
||||
name: field.name,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (streamFound == false) {
|
||||
searchObj.data.stream.selectedStream = { label: "", value: "" };
|
||||
searchObj.data.stream.selectedStreamFields = [];
|
||||
$q.notify({
|
||||
message: "Stream not found",
|
||||
color: "negative",
|
||||
position: "top",
|
||||
timeout: 2000,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateDateTime = (value: object) => {
|
||||
searchObj.data.datetime = value;
|
||||
|
||||
if (config.isZincObserveCloud == "true" && value.userChangedValue) {
|
||||
let dateTimeVal;
|
||||
if (value.tab === "relative") {
|
||||
dateTimeVal = value.relative;
|
||||
} else {
|
||||
dateTimeVal = value.absolute;
|
||||
}
|
||||
|
||||
segment.track("Button Click", {
|
||||
button: "Date Change",
|
||||
tab: value.tab,
|
||||
value: dateTimeVal,
|
||||
//user_org: this.store.state.selectedOrganization.identifier,
|
||||
//user_id: this.store.state.userInfo.email,
|
||||
stream_name: searchObj.data.stream.selectedStream.value,
|
||||
page: "Search Logs",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const udpateQuery = () => {
|
||||
// alert(searchObj.data.query);
|
||||
if (queryEditorRef.value?.setValue)
|
||||
queryEditorRef.value.setValue(searchObj.data.query);
|
||||
};
|
||||
|
||||
const jsonToCsv = (jsonData) => {
|
||||
const replacer = (key, value) => (value === null ? "" : value);
|
||||
const header = Object.keys(jsonData[0]);
|
||||
let csv = header.join(",") + "\r\n";
|
||||
|
||||
for (let i = 0; i < jsonData.length; i++) {
|
||||
const row = header
|
||||
.map((fieldName) => JSON.stringify(jsonData[i][fieldName], replacer))
|
||||
.join(",");
|
||||
csv += row + "\r\n";
|
||||
}
|
||||
|
||||
return csv;
|
||||
};
|
||||
|
||||
const downloadLogs = () => {
|
||||
const filename = "logs-data.csv";
|
||||
const data = jsonToCsv(searchObj.data.queryResults.hits);
|
||||
const file = new File([data], filename, {
|
||||
type: "text/csv",
|
||||
});
|
||||
const url = URL.createObjectURL(file);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return {
|
||||
t,
|
||||
router,
|
||||
searchObj,
|
||||
queryEditorRef,
|
||||
btnRefreshInterval,
|
||||
refreshTimes: searchObj.config.refreshTimes,
|
||||
refreshTimeChange,
|
||||
updateQueryValue,
|
||||
updateDateTime,
|
||||
udpateQuery,
|
||||
downloadLogs,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
addSearchTerm() {
|
||||
return this.searchObj.data.stream.addToFilter;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
addSearchTerm() {
|
||||
if (this.searchObj.data.stream.addToFilter != "") {
|
||||
let currentQuery = this.searchObj.data.editorValue.split("|");
|
||||
let filter = this.searchObj.data.stream.addToFilter;
|
||||
|
||||
const isFilterValueNull = filter.split(/=|!=/)[1] === "'null'";
|
||||
|
||||
if (isFilterValueNull) {
|
||||
filter = filter
|
||||
.replace(/=|!=/, (match) => {
|
||||
return match === "=" ? " is " : " is not ";
|
||||
})
|
||||
.replace(/'null'/, "null");
|
||||
}
|
||||
|
||||
if (currentQuery.length > 1) {
|
||||
if (currentQuery[1].trim() != "") {
|
||||
currentQuery[1] += " and " + filter;
|
||||
} else {
|
||||
currentQuery[1] = filter;
|
||||
}
|
||||
this.searchObj.data.query = currentQuery.join("| ");
|
||||
} else {
|
||||
if (currentQuery != "") {
|
||||
currentQuery += " and " + filter;
|
||||
} else {
|
||||
currentQuery = filter;
|
||||
}
|
||||
this.searchObj.data.query = currentQuery;
|
||||
}
|
||||
this.searchObj.data.stream.addToFilter = "";
|
||||
if (this.queryEditorRef?.setValue)
|
||||
this.queryEditorRef.setValue(this.searchObj.data.query);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.search-bar-component {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding-bottom: 1px;
|
||||
|
||||
.q-toggle__inner {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.q-toggle__label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.casesensitive-btn {
|
||||
padding: 8px;
|
||||
margin-left: -6px;
|
||||
background-color: #d5d5d5;
|
||||
border-radius: 0px 3px 3px 0px;
|
||||
}
|
||||
.search-field .q-field {
|
||||
&__control {
|
||||
border-radius: 3px 0px 0px 3px !important;
|
||||
}
|
||||
&__native {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
.search-time {
|
||||
// width: 120px;
|
||||
margin-right: 10px;
|
||||
.q-btn-group {
|
||||
border-radius: 3px;
|
||||
|
||||
.q-btn {
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
.search-dropdown {
|
||||
padding: 0px;
|
||||
.block {
|
||||
color: $dark-page;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
.q-btn-dropdown__arrow-container {
|
||||
color: $light-text2;
|
||||
}
|
||||
}
|
||||
.refresh-rate-dropdown-container {
|
||||
width: 220px;
|
||||
* .q-btn {
|
||||
font-size: 12px !important;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-start {
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.resultsOverChart {
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
color: $dark-page;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ddlWrapper {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
.listWrapper {
|
||||
box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.1);
|
||||
transition: height 0.25s ease;
|
||||
height: calc(100vh - 146px);
|
||||
background-color: white;
|
||||
position: absolute;
|
||||
top: 2.75rem;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
|
||||
&:empty {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
&,
|
||||
.q-list {
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.fields_autocomplete {
|
||||
max-height: 250px;
|
||||
}
|
||||
.monaco-editor {
|
||||
width: 100% !important;
|
||||
height: 70px !important;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
width: 96px;
|
||||
line-height: 29px;
|
||||
font-weight: bold;
|
||||
text-transform: initial;
|
||||
font-size: 11px;
|
||||
color: white;
|
||||
|
||||
.q-btn__content {
|
||||
background: $primary;
|
||||
border-radius: 0px 3px 3px 0px;
|
||||
|
||||
.q-icon {
|
||||
font-size: 15px;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.download-logs-btn {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,512 @@
|
||||
<!-- 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>
|
||||
<div class="col column oveflow-hidden">
|
||||
<div class="search-list" style="width: 100%">
|
||||
<trace-chart
|
||||
data-test="logs-search-result-bar-chart"
|
||||
ref="plotChart"
|
||||
id="traces_scatter_chart"
|
||||
v-show="searchObj.meta.showHistogram"
|
||||
:chart="searchObj.data.histogram"
|
||||
/>
|
||||
|
||||
<q-virtual-scroll
|
||||
data-test="logs-search-result-logs-table"
|
||||
id="tracesSearchGridComponent"
|
||||
type="table"
|
||||
ref="searchTableRef"
|
||||
:virtual-scroll-item-size="25"
|
||||
:virtual-scroll-sticky-size-start="0"
|
||||
:virtual-scroll-sticky-size-end="0"
|
||||
:virtual-scroll-slice-size="300"
|
||||
:virtual-scroll-slice-ratio-before="10"
|
||||
:items="searchObj.data.queryResults.hits"
|
||||
@virtual-scroll="onScroll"
|
||||
>
|
||||
<template v-slot:before>
|
||||
<thead class="thead-sticky text-left">
|
||||
<tr>
|
||||
<th
|
||||
v-for="(col, index) in searchObj.data.resultGrid.columns"
|
||||
:key="'result_' + index"
|
||||
class="table-header"
|
||||
:data-test="`log-search-result-table-th-${col.label}`"
|
||||
>
|
||||
<q-chip
|
||||
v-if="col.closable"
|
||||
:data-test="`logs-search-result-table-th-remove-${col.label}-btn`"
|
||||
:icon-remove="
|
||||
'img:' + getImageURL('images/common/close_icon.svg')
|
||||
"
|
||||
class="q-ma-none table-head-chip"
|
||||
removable
|
||||
square
|
||||
@remove="closeColumn(col)"
|
||||
>
|
||||
{{ col.label }}
|
||||
</q-chip>
|
||||
|
||||
<span v-else class="table-head-label">
|
||||
{{ col.label }}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</template>
|
||||
|
||||
<template v-slot="{ item: row, index }">
|
||||
<q-tr
|
||||
:data-test="`logs-search-result-detail-${
|
||||
row[store.state.zoConfig.timestamp_column]
|
||||
}`"
|
||||
:key="'expand_' + index"
|
||||
@click="expandRowDetail(row, index)"
|
||||
style="cursor: pointer"
|
||||
:style="
|
||||
row[store.state.zoConfig.timestamp_column] ==
|
||||
searchObj.data.searchAround.indexTimestamp
|
||||
? 'background-color:lightgray'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<q-td
|
||||
v-for="column in searchObj.data.resultGrid.columns"
|
||||
:key="index + '-' + column.name"
|
||||
class="field_list"
|
||||
>
|
||||
<high-light
|
||||
:content="
|
||||
column.name == 'source'
|
||||
? column.prop(row)
|
||||
: column.prop(row, column.name).length > 100
|
||||
? column.prop(row, column.name).substr(0, 100) + '...'
|
||||
: column.name === 'duration'
|
||||
? column.format(row[column.name])
|
||||
: column.name != '@timestamp'
|
||||
? row[column.name]
|
||||
: column.prop(row, column.name)
|
||||
"
|
||||
:query-string="
|
||||
searchObj.meta.sqlMode
|
||||
? searchObj.data.query.split('where')[1]
|
||||
: searchObj.data.query
|
||||
"
|
||||
:title="
|
||||
column.prop(row, column.name).length > 100 &&
|
||||
column.name != 'source'
|
||||
? column.prop(row, column.name)
|
||||
: ''
|
||||
"
|
||||
></high-light>
|
||||
<div
|
||||
v-if="column.closable && row[column.name]"
|
||||
class="field_overlay"
|
||||
:title="row.name"
|
||||
>
|
||||
<q-icon
|
||||
:name="'img:' + getImageURL('images/common/add_icon.svg')"
|
||||
size="1rem"
|
||||
title="Add to search query"
|
||||
@click.prevent.stop="
|
||||
addSearchTerm(`${column.name}='${row[column.name]}'`)
|
||||
"
|
||||
/>
|
||||
<q-icon
|
||||
:name="'img:' + getImageURL('images/common/remove_icon.svg')"
|
||||
size="1rem"
|
||||
title="Add to search query"
|
||||
@click.prevent.stop="
|
||||
addSearchTerm(`${column.name}!='${row[column.name]}'`)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</q-td> </q-tr
|
||||
></template>
|
||||
</q-virtual-scroll>
|
||||
<q-dialog
|
||||
v-model="searchObj.meta.showTraceDetails"
|
||||
position="right"
|
||||
full-height
|
||||
full-width
|
||||
maximized
|
||||
@hide="closeTraceDetails"
|
||||
>
|
||||
<trace-details />
|
||||
</q-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, nextTick, ref } from "vue";
|
||||
import { useQuasar, date } from "quasar";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
import HighLight from "../../components/HighLight.vue";
|
||||
import { byString } from "../../utils/json";
|
||||
import useTraces from "../../composables/useTraces";
|
||||
import { getImageURL } from "../../utils/zincutils";
|
||||
import TraceDetails from "./TraceDetails.vue";
|
||||
import TraceChart from "./TraceChart.vue";
|
||||
import { redraw } from "plotly.js";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SearchResult",
|
||||
components: {
|
||||
HighLight,
|
||||
TraceDetails,
|
||||
TraceChart,
|
||||
},
|
||||
emits: [
|
||||
"update:scroll",
|
||||
"update:datetime",
|
||||
"remove:searchTerm",
|
||||
"search:timeboxed",
|
||||
"get:traceDetails",
|
||||
],
|
||||
methods: {
|
||||
closeColumn(col: any) {
|
||||
const RGIndex = this.searchObj.data.resultGrid.columns.indexOf(col.name);
|
||||
this.searchObj.data.resultGrid.columns.splice(RGIndex, 1);
|
||||
|
||||
const SFIndex = this.searchObj.data.stream.selectedFields.indexOf(
|
||||
col.name
|
||||
);
|
||||
|
||||
this.searchObj.data.stream.selectedFields.splice(SFIndex, 1);
|
||||
this.searchObj.organizationIdetifier =
|
||||
this.store.state.selectedOrganization.identifier;
|
||||
this.updatedLocalLogFilterField();
|
||||
},
|
||||
onChartUpdate({ start, end }: { start: any; end: any }) {
|
||||
this.searchObj.meta.showDetailTab = false;
|
||||
this.searchObj.runQuery = true;
|
||||
this.searchObj.data.datetime.tab = "absolute";
|
||||
this.searchObj.data.datetime.absolute.date.from = start.split(" ")[0];
|
||||
this.searchObj.data.datetime.absolute.date.to = end.split(" ")[0];
|
||||
this.searchObj.data.datetime.absolute.startTime =
|
||||
start.split(" ")[1].split(":")[0] +
|
||||
":" +
|
||||
start.split(" ")[1].split(":")[1] +
|
||||
":" +
|
||||
start.split(" ")[1].split(":")[2];
|
||||
this.searchObj.data.datetime.absolute.endTime =
|
||||
end.split(" ")[1].split(":")[0] +
|
||||
":" +
|
||||
end.split(" ")[1].split(":")[1] +
|
||||
":" +
|
||||
end.split(" ")[1].split(":")[2];
|
||||
|
||||
this.searchObj.runQuery = true;
|
||||
this.$emit("update:datetime");
|
||||
},
|
||||
onScroll(info: any) {
|
||||
this.searchObj.meta.scrollInfo = info;
|
||||
if (
|
||||
info.ref.items.length / info.index <= 2 &&
|
||||
this.searchObj.loading == false &&
|
||||
this.searchObj.data.resultGrid.currentPage <=
|
||||
this.searchObj.data.queryResults.from /
|
||||
this.searchObj.meta.resultGrid.rowsPerPage
|
||||
) {
|
||||
this.searchObj.data.resultGrid.currentPage += 1;
|
||||
this.$emit("update:scroll");
|
||||
}
|
||||
},
|
||||
onTimeBoxed(obj: any) {
|
||||
this.searchObj.meta.showDetailTab = false;
|
||||
this.searchObj.data.searchAround.indexTimestamp = obj.key;
|
||||
this.$emit("search:timeboxed", obj);
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
// Accessing nested JavaScript objects and arrays by string path
|
||||
// https://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-and-arrays-by-string-path
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const $q = useQuasar();
|
||||
const showTraceDetails = ref(true);
|
||||
const { searchObj, updatedLocalLogFilterField } = useTraces();
|
||||
const totalHeight = ref(0);
|
||||
|
||||
const searchTableRef: any = ref(null);
|
||||
|
||||
const plotChart: any = ref(null);
|
||||
|
||||
const reDrawChart = () => {
|
||||
if (
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
searchObj.data.histogram.data &&
|
||||
searchObj.data.histogram.layout
|
||||
) {
|
||||
nextTick(() => {
|
||||
plotChart.value.reDraw();
|
||||
plotChart.value.forceReLayout();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const changeMaxRecordToReturn = (val: any) => {
|
||||
// searchObj.meta.resultGrid.pagination.rowsPerPage = val;
|
||||
};
|
||||
|
||||
const expandRowDetail = (props: any, index: number) => {
|
||||
searchObj.meta.showTraceDetails = true;
|
||||
emit("get:traceDetails", props.trace_id);
|
||||
};
|
||||
|
||||
const getRowIndex = (next: boolean, prev: boolean, oldIndex: number) => {
|
||||
if (next) {
|
||||
return oldIndex + 1;
|
||||
} else {
|
||||
return oldIndex - 1;
|
||||
}
|
||||
};
|
||||
|
||||
const navigateRowDetail = (isNext: boolean, isPrev: boolean) => {
|
||||
const newIndex = getRowIndex(
|
||||
isNext,
|
||||
isPrev,
|
||||
Number(searchObj.meta.resultGrid.navigation.currentRowIndex)
|
||||
);
|
||||
searchObj.meta.resultGrid.navigation.currentRowIndex = newIndex;
|
||||
};
|
||||
|
||||
const addSearchTerm = (term: string) => {
|
||||
// searchObj.meta.showDetailTab = false;
|
||||
searchObj.data.stream.addToFilter = term;
|
||||
};
|
||||
|
||||
const removeSearchTerm = (term: string) => {
|
||||
emit("remove:searchTerm", term);
|
||||
};
|
||||
|
||||
const closeTraceDetails = () => {
|
||||
searchObj.meta.showTraceDetails = false;
|
||||
searchObj.data.traceDetails.showSpanDetails = false;
|
||||
searchObj.data.traceDetails.selectedSpanId = null;
|
||||
};
|
||||
return {
|
||||
t,
|
||||
store,
|
||||
plotChart,
|
||||
searchObj,
|
||||
updatedLocalLogFilterField,
|
||||
byString,
|
||||
searchTableRef,
|
||||
addSearchTerm,
|
||||
removeSearchTerm,
|
||||
expandRowDetail,
|
||||
changeMaxRecordToReturn,
|
||||
navigateRowDetail,
|
||||
totalHeight,
|
||||
reDrawChart,
|
||||
getImageURL,
|
||||
showTraceDetails,
|
||||
closeTraceDetails,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.max-result {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
.search-list {
|
||||
width: 100%;
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.my-sticky-header-table {
|
||||
.q-table__top,
|
||||
.q-table__bottom,
|
||||
thead tr:first-child th {
|
||||
/* bg color is important for th; just specify one */
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
thead tr th {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
thead tr:first-child th {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* this is when the loading indicator appears */
|
||||
&.q-table--loading thead tr:last-child th {
|
||||
/* height of all previous header rows */
|
||||
top: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.q-table__top {
|
||||
padding-left: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.q-table thead tr,
|
||||
.q-table tbody td,
|
||||
.q-table th,
|
||||
.q-table td {
|
||||
height: 25px;
|
||||
padding: 0px 5px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.q-table__bottom {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.q-table__bottom {
|
||||
min-height: 40px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.q-td {
|
||||
overflow: hidden;
|
||||
min-width: 100px;
|
||||
|
||||
.expanded {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: rgb(255, 213, 0);
|
||||
}
|
||||
|
||||
.table-header {
|
||||
// text-transform: capitalize;
|
||||
|
||||
.table-head-chip {
|
||||
background-color: $accent;
|
||||
padding: 0px;
|
||||
|
||||
.q-chip__content {
|
||||
margin-right: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: $dark;
|
||||
}
|
||||
|
||||
.q-chip__icon--remove {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
opacity: 1;
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.q-table th.sortable {
|
||||
cursor: pointer;
|
||||
text-transform: capitalize;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
&.isClosable {
|
||||
padding-right: 26px;
|
||||
position: relative;
|
||||
|
||||
.q-table-col-close {
|
||||
transform: translateX(26px);
|
||||
position: absolute;
|
||||
margin-top: 2px;
|
||||
color: grey;
|
||||
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.q-table th.sortable {
|
||||
cursor: pointer;
|
||||
text-transform: capitalize;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
.thead-sticky tr > *,
|
||||
.tfoot-sticky tr > * {
|
||||
position: sticky;
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
background: #f5f5f5;
|
||||
color: #090909;
|
||||
}
|
||||
.thead-sticky tr:last-child > * {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.tfoot-sticky tr:first-child > * {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.field_list {
|
||||
padding: 0px;
|
||||
margin-bottom: 0.125rem;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
cursor: default;
|
||||
|
||||
.field_overlay {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
top: 0;
|
||||
background-color: #ffffff;
|
||||
border-radius: 6px;
|
||||
padding: 0 6px;
|
||||
visibility: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s linear;
|
||||
|
||||
.q-icon {
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.3s linear;
|
||||
margin: 0 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.field_overlay {
|
||||
visibility: visible;
|
||||
|
||||
.q-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,203 @@
|
||||
<!-- 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
|
||||
class="flex justify-between items-center cursor-pointer span-block relative-position"
|
||||
:style="{
|
||||
width: '100%',
|
||||
height: spanDimensions.height + 'px',
|
||||
}"
|
||||
:class="!isSpanSelected ? 'defocus' : ''"
|
||||
ref="spanBlock"
|
||||
@click="selectSpan"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
}"
|
||||
class="cursor-pointer flex items-center no-wrap position-relative"
|
||||
:class="!isSpanSelected ? 'defocus' : ''"
|
||||
@click="selectSpan"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
width: getWidth + '%',
|
||||
height: spanDimensions.barHeight + 'px',
|
||||
borderRadius: '2px',
|
||||
left: getLeftPosition + '%',
|
||||
position: 'relative',
|
||||
backgroundColor: span.style?.color || '#58508d',
|
||||
zIndex: 1,
|
||||
}"
|
||||
/>
|
||||
<div
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
...getDurationStyle,
|
||||
transition: 'all 0.5s ease',
|
||||
zIndex: 1,
|
||||
}"
|
||||
class="text-caption"
|
||||
>
|
||||
{{ formatTimeWithSuffix(span.durationMs) }}
|
||||
</div>
|
||||
<q-resize-observer debounce="300" @resize="onResize" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
computed,
|
||||
ref,
|
||||
watch,
|
||||
onMounted,
|
||||
watchEffect,
|
||||
} from "vue";
|
||||
import useTraces from "@/composables/useTraces";
|
||||
import { getImageURL } from "@/utils/zincutils";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SpanBlock",
|
||||
props: {
|
||||
span: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
baseTracePosition: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
depth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
styleObj: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
showCollapse: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isCollapsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
spanDimensions: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
emits: ["toggleCollapse"],
|
||||
setup(props, { emit }) {
|
||||
const { searchObj } = useTraces();
|
||||
const spanBlock: any = ref(null);
|
||||
const spanBlockWidth = ref(0);
|
||||
const onePixelPercent = ref(0);
|
||||
function formatTimeWithSuffix(ns: number) {
|
||||
if (ns < 10000) {
|
||||
return `${ns}ms`;
|
||||
} else {
|
||||
return `${(ns / 1000).toFixed(2)}s`;
|
||||
}
|
||||
}
|
||||
const isSpanSelected = computed(() => {
|
||||
if (!searchObj.data.traceDetails.selectedSpanId) return true;
|
||||
return searchObj.data.traceDetails.selectedSpanId === props.span.spanId;
|
||||
});
|
||||
const selectSpan = () => {
|
||||
searchObj.data.traceDetails.showSpanDetails = true;
|
||||
searchObj.data.traceDetails.selectedSpanId = props.span.spanId;
|
||||
};
|
||||
const toggleSpanCollapse = () => {
|
||||
emit("toggleCollapse", props.span.spanId);
|
||||
};
|
||||
const getLeftPosition = computed(() => {
|
||||
const left =
|
||||
props.span.startTimeMs - props.baseTracePosition["startTimeMs"];
|
||||
return (left / props.baseTracePosition?.durationMs) * 100;
|
||||
});
|
||||
const getWidth = computed(() => {
|
||||
return Number(
|
||||
(
|
||||
(props.span?.durationMs / props.baseTracePosition?.durationMs) *
|
||||
100
|
||||
).toFixed(2)
|
||||
);
|
||||
});
|
||||
const getDurationStyle = computed(() => {
|
||||
const style: any = {
|
||||
top: "2px",
|
||||
};
|
||||
const onePercent = Number((spanBlockWidth.value / 100).toFixed(2));
|
||||
const labelWidth = 60;
|
||||
if (
|
||||
(getLeftPosition.value + getWidth.value) * onePercent + labelWidth >
|
||||
spanBlockWidth.value
|
||||
) {
|
||||
style.right = 0;
|
||||
style.top = "-8px";
|
||||
} else if (getLeftPosition.value > 50) {
|
||||
style.left = getLeftPosition.value * onePercent - labelWidth + "px";
|
||||
} else {
|
||||
style.left =
|
||||
(getLeftPosition.value + getWidth.value) * onePercent + 10 + "px";
|
||||
}
|
||||
return style;
|
||||
});
|
||||
|
||||
const onResize = () => {
|
||||
spanBlockWidth.value = spanBlock.value.clientWidth;
|
||||
};
|
||||
|
||||
return {
|
||||
formatTimeWithSuffix,
|
||||
selectSpan,
|
||||
isSpanSelected,
|
||||
toggleSpanCollapse,
|
||||
getImageURL,
|
||||
getLeftPosition,
|
||||
getWidth,
|
||||
getDurationStyle,
|
||||
spanBlock,
|
||||
onResize,
|
||||
onePixelPercent,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.defocus {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
background-color: #ffffff;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.collapse-container {
|
||||
border: 1px solid #ececec;
|
||||
}
|
||||
|
||||
.light-grey {
|
||||
background-color: #ececec;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<!-- 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 class="relative-position q-pt-sm" :style="{ height: '100%' }">
|
||||
<template v-if="spans?.length">
|
||||
<template v-for="span in spans as any[]" :key="span.spanId">
|
||||
<span-block
|
||||
:span="span"
|
||||
:depth="depth"
|
||||
:baseTracePosition="baseTracePosition"
|
||||
:styleObj="{
|
||||
position: 'absolute',
|
||||
top: span.style.top,
|
||||
left: span.style.left,
|
||||
height: '60px',
|
||||
}"
|
||||
:spanDimensions="spanDimensions"
|
||||
:isCollapsed="collapseMapping[span.spanId]"
|
||||
@toggle-collapse="toggleSpanCollapse"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onActivated, onBeforeMount, ref } from "vue";
|
||||
import SpanBlock from "./SpanBlock.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SpanRenderer",
|
||||
props: {
|
||||
spans: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isCollapsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
collapseMapping: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
baseTracePosition: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
depth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
spanDimensions: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
emits: ["toggleCollapse"],
|
||||
setup(props, { emit }) {
|
||||
function formatTimeWithSuffix(ns: number) {
|
||||
if (ns < 10000) {
|
||||
return `${ns}ms`;
|
||||
} else {
|
||||
return `${(ns / 1000).toFixed(2)}s`;
|
||||
}
|
||||
}
|
||||
function toggleSpanCollapse(spanId: number | string) {
|
||||
emit("toggleCollapse", spanId);
|
||||
}
|
||||
|
||||
return {
|
||||
formatTimeWithSuffix,
|
||||
toggleSpanCollapse,
|
||||
};
|
||||
},
|
||||
components: { SpanBlock },
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.spans-container {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,227 @@
|
||||
<!-- 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-btn
|
||||
data-cy="syntax-guide-button"
|
||||
size="sm"
|
||||
dense
|
||||
flat
|
||||
class="q-ml-sm q-pa-xs syntax-guide-button"
|
||||
:class="sqlmode ? 'sql-mode' : 'normal-mode'"
|
||||
label="Syntax Guide"
|
||||
icon="help"
|
||||
>
|
||||
<q-menu>
|
||||
<q-card flat v-if="!sqlmode">
|
||||
<q-card-section class="syntax-guide-title">
|
||||
<div class="label">Syntax Guide</div>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-section class="q-pt-none answers">
|
||||
<div class="syntax-section">
|
||||
<div class="syntax-guide-text">
|
||||
<ul class="guide-list">
|
||||
<li>
|
||||
For full text search of value 'error' use
|
||||
<span class="bg-lightgray"
|
||||
>match_all('error') in query editor</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
For case-insensitive full text search of value 'error' use
|
||||
<span class="bg-lightgray"
|
||||
>match_all_ignore_case('error')</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
For column search of value 'error' use
|
||||
<span class="bg-lightgray"
|
||||
>str_match(<b>fieldname</b>, 'error')</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
To search value 200 for code column use
|
||||
<span class="bg-lightgray">code=200</span>
|
||||
</li>
|
||||
<li>
|
||||
To search value 'stderr' for stream column use
|
||||
<span class="bg-lightgray">stream='stderr'</span>
|
||||
</li>
|
||||
<li>
|
||||
To search and use query function <i>extract_ip</i> on cloumn
|
||||
log use
|
||||
<span class="bg-lightgray">extract_ip(log) | code=200</span>
|
||||
</li>
|
||||
<li>
|
||||
For additional examples,
|
||||
<a
|
||||
href="https://docs.zinc.dev/example-queries/"
|
||||
target="_blank"
|
||||
>click here</a
|
||||
>.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-card flat v-else>
|
||||
<q-card-section class="syntax-guide-title">
|
||||
<div class="label">Syntax Guide: SQL Mode</div>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-section class="q-pt-none answers">
|
||||
<div class="syntax-section">
|
||||
<div class="syntax-guide-text">
|
||||
<ul class="guide-list">
|
||||
<li>
|
||||
For full text search of value 'error' use
|
||||
<span class="bg-lightgray"
|
||||
>SELECT * FROM <b>stream</b> WHERE match_all('error')</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
For case-insensitive full text search of value 'error' use
|
||||
<span class="bg-lightgray"
|
||||
>SELECT * FROM <b>stream</b> WHERE
|
||||
match_all_ignore_case('error')</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
For column search of value 'error' use
|
||||
<span class="bg-lightgray"
|
||||
>SELECT * FROM <b>stream</b> WHERE
|
||||
str_match(<b>fieldname</b>, 'error')</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
To search value 200 for code column use
|
||||
<span class="bg-lightgray"
|
||||
>SELECT * FROM <b>stream</b> WHERE code=200</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
To search value 'stderr' for stream column use
|
||||
<span class="bg-lightgray"
|
||||
>SELECT * FROM <b>stream</b> WHERE stream='stderr'</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
To search and use query function <i>extract_ip</i> on cloumn
|
||||
log use
|
||||
<span class="bg-lightgray"
|
||||
>SELECT extract_ip(log) FROM <b>stream</b> WHERE
|
||||
code=200</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
For additional examples,
|
||||
<a
|
||||
href="https://docs.zinc.dev/example-queries/"
|
||||
target="_blank"
|
||||
>click here</a
|
||||
>.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ComponentSearchSyntaxGuide",
|
||||
props: {
|
||||
sqlmode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
return {
|
||||
t,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.guide-list {
|
||||
padding: 0 10px;
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
.guide-list li {
|
||||
font-size: 14px;
|
||||
line-height: 23px;
|
||||
}
|
||||
.q-btn:before {
|
||||
border: 0px solid #d5d5d5;
|
||||
}
|
||||
|
||||
.syntax-guide-button {
|
||||
cursor: pointer;
|
||||
text-transform: capitalize;
|
||||
padding: 5px 5px;
|
||||
font-weight: bold;
|
||||
border: 1px solid rgba(89, 96, 178, 0.3);
|
||||
}
|
||||
|
||||
.normal-mode {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
color: #202224;
|
||||
}
|
||||
|
||||
.sql-mode {
|
||||
background-color: rgba(89, 96, 178, 0.8);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.syntax-guide-title {
|
||||
width: 420px;
|
||||
|
||||
.label {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.syntax-guide-sub-title {
|
||||
color: $primary;
|
||||
font-size: 15px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.syntax-guide-text {
|
||||
font-size: 12px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.syntax-section {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.bg-lightgray {
|
||||
background-color: $selected-list-bg;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,215 @@
|
||||
<!-- 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 ref="plotref" :id="id" style="width: 100%" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Plotly from "plotly.js";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
|
||||
import { defineComponent, onMounted, onUpdated, ref } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TraceChart",
|
||||
emits: ["updated:chart"],
|
||||
props: {
|
||||
height: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 600,
|
||||
},
|
||||
chart: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const plotref: any = ref(null);
|
||||
const zoomFlag: any = ref(false);
|
||||
let traces: any = [{}];
|
||||
|
||||
let layout: any = {
|
||||
width: "100%",
|
||||
scrollZoom: true,
|
||||
title: {
|
||||
text: "Trace",
|
||||
font: {
|
||||
size: 12,
|
||||
},
|
||||
},
|
||||
font: { size: 12 },
|
||||
autosize: true,
|
||||
height: 200,
|
||||
margin: {
|
||||
l: 32,
|
||||
r: 16,
|
||||
t: 32,
|
||||
b: 32,
|
||||
},
|
||||
xaxis: {
|
||||
ticklen: 5,
|
||||
nticks: 10,
|
||||
type: "-",
|
||||
ticksuffix: "ms",
|
||||
showgrid: true,
|
||||
zeroline: true,
|
||||
rangeslider: {
|
||||
visible: true,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
showgrid: false,
|
||||
zeroline: true,
|
||||
autorange: true,
|
||||
showticklabels: false,
|
||||
},
|
||||
shapes: [
|
||||
{
|
||||
x0: "5",
|
||||
x1: "10",
|
||||
y0: 0,
|
||||
y1: 0.4,
|
||||
line: {
|
||||
width: 0,
|
||||
},
|
||||
type: "rect",
|
||||
xref: "x",
|
||||
yref: "y",
|
||||
opacity: 1,
|
||||
fillcolor: "rgb(0, 0, 0)",
|
||||
},
|
||||
{
|
||||
x0: "2",
|
||||
x1: "19",
|
||||
y0: 0.4,
|
||||
y1: 0.8,
|
||||
line: {
|
||||
width: 0,
|
||||
},
|
||||
type: "rect",
|
||||
xref: "x",
|
||||
yref: "y",
|
||||
opacity: 1,
|
||||
fillcolor: "rgb(139, 0, 0)",
|
||||
},
|
||||
{
|
||||
x0: "0",
|
||||
x1: "20",
|
||||
y0: 0.8,
|
||||
y1: 1.2,
|
||||
line: {
|
||||
width: 0,
|
||||
},
|
||||
type: "rect",
|
||||
xref: "x",
|
||||
yref: "y",
|
||||
opacity: 1,
|
||||
fillcolor: "rgb(0, 0, 0)",
|
||||
},
|
||||
],
|
||||
hovermode: "closest",
|
||||
showlegend: true,
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
layout = props.chart.layout || {};
|
||||
traces = props.chart.data || [{}];
|
||||
await Plotly.newPlot(props.id, traces, layout, {
|
||||
responsive: true,
|
||||
displayModeBar: false,
|
||||
});
|
||||
|
||||
plotref.value.on("plotly_relayout", onPlotZoom);
|
||||
});
|
||||
|
||||
onUpdated(async () => {
|
||||
const update: any = {
|
||||
"xaxis.autorange": true,
|
||||
"yaxis.autorange": true,
|
||||
};
|
||||
if (document.getElementById(props.id) != null) {
|
||||
Plotly.relayout(plotref.value, update);
|
||||
}
|
||||
});
|
||||
|
||||
const reDraw = () => {
|
||||
traces = props.chart.data;
|
||||
layout = props.chart.layout;
|
||||
if (document.getElementById(props.id) != null) {
|
||||
Plotly.react(props.id, traces, layout);
|
||||
}
|
||||
};
|
||||
|
||||
// created force relayout function to avoid infinite loop
|
||||
const forceReLayout = (yaxis: any, flag = true) => {
|
||||
zoomFlag.value = flag;
|
||||
const update: any = {
|
||||
"xaxis.autorange": true,
|
||||
"yaxis.autorange": true,
|
||||
};
|
||||
if (document.getElementById(props.id) != null) {
|
||||
Plotly.relayout(plotref.value, update);
|
||||
}
|
||||
};
|
||||
|
||||
const onPlotZoom = (e: any) => {
|
||||
if (
|
||||
e &&
|
||||
e["xaxis.range[0]"] &&
|
||||
e["xaxis.range[1]"] &&
|
||||
zoomFlag.value == false
|
||||
) {
|
||||
const start = plotref.value.layout.xaxis.range[0];
|
||||
const end = plotref.value.layout.xaxis.range[1];
|
||||
if (start != "" && end != "") {
|
||||
zoomFlag.value = true;
|
||||
let start_d = new Date(start);
|
||||
let end_d = new Date(end);
|
||||
emit("updated:chart", {
|
||||
data: e,
|
||||
start: start_d.toLocaleString("sv-SE"),
|
||||
end: end_d.toLocaleString("sv-SE"),
|
||||
});
|
||||
}
|
||||
} else if (e["xaxis.range"]) {
|
||||
emit("updated:chart", {
|
||||
data: e,
|
||||
});
|
||||
} else {
|
||||
emit("updated:chart", {
|
||||
data: e,
|
||||
});
|
||||
zoomFlag.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
plotref,
|
||||
reDraw,
|
||||
onPlotZoom,
|
||||
forceReLayout,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,546 @@
|
||||
<!-- 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
|
||||
class="trace-details"
|
||||
:style="{
|
||||
width: '97vw !important',
|
||||
background: '#ffffff',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="row q-px-sm"
|
||||
v-if="traceTree.length && !searchObj.data.traceDetails.loading"
|
||||
>
|
||||
<div class="q-py-sm flex items-center justify-start col-12 toolbar">
|
||||
<div class="text-h6 q-mr-lg">
|
||||
{{ traceTree[0]["operationName"] }}
|
||||
</div>
|
||||
<div>Spans: {{ spanList.length - 1 }}</div>
|
||||
</div>
|
||||
<trace-chart
|
||||
id="trace_details_gantt_chart"
|
||||
ref="plotChart"
|
||||
:chart="traceChart"
|
||||
@updated:chart="updateChart"
|
||||
/>
|
||||
<div
|
||||
:class="
|
||||
isSidebarOpen ? 'histogram-container' : 'histogram-container-full'
|
||||
"
|
||||
>
|
||||
<trace-header
|
||||
:baseTracePosition="baseTracePosition"
|
||||
:splitterWidth="splitterModel"
|
||||
/>
|
||||
<div class="histogram-spans-container">
|
||||
<q-splitter v-model="splitterModel" :style="{ height: '100%' }">
|
||||
<template v-slot:before>
|
||||
<div class="trace-tree-container">
|
||||
<trace-tree
|
||||
:collapseMapping="collapseMapping"
|
||||
:spans="spanPositionList"
|
||||
:baseTracePosition="baseTracePosition"
|
||||
:spanDimensions="spanDimensions"
|
||||
class="trace-tree"
|
||||
@toggle-collapse="toggleSpanCollapse"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:after>
|
||||
<SpanRenderer
|
||||
:collapseMapping="collapseMapping"
|
||||
:spans="spanPositionList"
|
||||
:baseTracePosition="baseTracePosition"
|
||||
:spanDimensions="spanDimensions"
|
||||
ref="traceRootSpan"
|
||||
/>
|
||||
</template>
|
||||
</q-splitter>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator vertical />
|
||||
<div v-if="isSidebarOpen && selectedSpanId" class="histogram-sidebar">
|
||||
<trace-details-sidebar
|
||||
:span="spanMapping[selectedSpanId]"
|
||||
@close="closeSidebar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="searchObj.data.traceDetails.loading"
|
||||
class="flex column items-center justify-center"
|
||||
:style="{ height: '100%' }"
|
||||
>
|
||||
<q-spinner color="primary" size="3em" :thickness="2" />
|
||||
<div class="q-pt-sm">Getting trace data</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
ref,
|
||||
type Ref,
|
||||
onMounted,
|
||||
watch,
|
||||
onActivated,
|
||||
} from "vue";
|
||||
import { cloneDeep, range } from "lodash";
|
||||
import SpanRenderer from "./SpanRenderer.vue";
|
||||
import useTraces from "@/composables/useTraces";
|
||||
import { computed } from "vue";
|
||||
import TraceDetailsSidebar from "./TraceDetailsSidebar.vue";
|
||||
import TraceTree from "./TraceTree.vue";
|
||||
import TraceHeader from "./TraceHeader.vue";
|
||||
import TraceChart from "./TraceChart.vue";
|
||||
import { useStore } from "vuex";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TraceDetails",
|
||||
props: {
|
||||
traceId: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
components: {
|
||||
SpanRenderer,
|
||||
TraceDetailsSidebar,
|
||||
TraceTree,
|
||||
TraceHeader,
|
||||
TraceChart,
|
||||
},
|
||||
|
||||
setup() {
|
||||
const traceTree: any = ref([]);
|
||||
const spanMapping: any = ref({});
|
||||
const { searchObj } = useTraces();
|
||||
const baseTracePosition: any = ref({});
|
||||
const collapseMapping: any = ref({});
|
||||
const traceRootSpan: any = ref(null);
|
||||
const spanPositionList: any = ref([]);
|
||||
const splitterModel = ref(25);
|
||||
const timeRange: any = ref({ start: 0, end: 0 });
|
||||
const store = useStore();
|
||||
const spanDimensions = {
|
||||
height: 25,
|
||||
barHeight: 8,
|
||||
textHeight: 25,
|
||||
gap: 15,
|
||||
collapseHeight: 8,
|
||||
collapseWidth: 8,
|
||||
connectorPadding: 2,
|
||||
paddingLeft: 8,
|
||||
hConnectorWidth: 20,
|
||||
dotConnectorWidth: 6,
|
||||
dotConnectorHeight: 6,
|
||||
colors: ["#b7885e", "#1ab8be", "#ffcb99", "#f89570", "#839ae2"],
|
||||
};
|
||||
|
||||
const traceChart = ref({
|
||||
data: [{}],
|
||||
layout: {},
|
||||
});
|
||||
const plotChart: any = ref(null);
|
||||
|
||||
const spanList: any = computed(() => {
|
||||
return searchObj.data.traceDetails.spanList;
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
buildTracesTree();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
buildTracesTree();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => spanList.value.length,
|
||||
() => {
|
||||
if (spanList.value.length) {
|
||||
buildTracesTree();
|
||||
} else traceTree.value = [];
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const isSidebarOpen = computed(() => {
|
||||
return searchObj.data.traceDetails.showSpanDetails;
|
||||
});
|
||||
|
||||
const selectedSpanId = computed(() => {
|
||||
return searchObj.data.traceDetails.selectedSpanId;
|
||||
});
|
||||
|
||||
const calculateTracePosition = () => {
|
||||
const tics = [];
|
||||
baseTracePosition.value["durationMs"] = timeRange.value.end;
|
||||
baseTracePosition.value["startTimeMs"] =
|
||||
traceTree.value[0].startTimeMs + timeRange.value.start;
|
||||
const quarterMs = (timeRange.value.end - timeRange.value.start) / 4;
|
||||
let time = timeRange.value.start;
|
||||
for (let i = 0; i <= 4; i++) {
|
||||
tics.push({
|
||||
value: Number(time.toFixed(2)),
|
||||
label: `${time.toFixed(2)}ms`,
|
||||
left: `${25 * i}%`,
|
||||
});
|
||||
time += quarterMs;
|
||||
}
|
||||
baseTracePosition.value["tics"] = tics;
|
||||
};
|
||||
|
||||
// Find out spans who has reference_parent_span_id as span_id of first span in sampleTrace
|
||||
const buildTracesTree = async () => {
|
||||
spanMapping.value = {};
|
||||
traceTree.value = [];
|
||||
spanPositionList.value = [];
|
||||
collapseMapping.value = {};
|
||||
let lowestStartTime: number = spanList.value[0].start_time;
|
||||
let highestEndTime: number = spanList.value[0].end_time;
|
||||
|
||||
const traceTreeMock: any = {};
|
||||
const serviceColorMapping: any = {};
|
||||
|
||||
if (!spanList.value?.length) return;
|
||||
spanMapping.value[spanList.value[0].span_id] = spanList.value[0];
|
||||
let colorIndex = 0;
|
||||
let noParentSpans = [];
|
||||
for (let i = 0; i < spanList.value.length; i++) {
|
||||
if (spanList.value[i].start_time < lowestStartTime) {
|
||||
lowestStartTime = spanList.value[i].start_time;
|
||||
}
|
||||
if (spanList.value[i].end_time > highestEndTime) {
|
||||
highestEndTime = spanList.value[i].end_time;
|
||||
}
|
||||
|
||||
spanMapping.value[spanList.value[i].span_id] = cloneDeep(
|
||||
spanList.value[i]
|
||||
);
|
||||
|
||||
const span = getFormattedSpan(spanList.value[i]);
|
||||
|
||||
if (span.serviceName && !serviceColorMapping[span.serviceName]) {
|
||||
serviceColorMapping[span.serviceName] =
|
||||
spanDimensions.colors[colorIndex];
|
||||
colorIndex++;
|
||||
if (colorIndex > spanDimensions.colors.length - 1) colorIndex = 0;
|
||||
}
|
||||
|
||||
span.style.color = serviceColorMapping[span.serviceName];
|
||||
|
||||
span.index = i;
|
||||
|
||||
collapseMapping.value[span.spanId] = true;
|
||||
|
||||
if (span.parentId && !traceTreeMock[span.parentId]) {
|
||||
noParentSpans.push(span);
|
||||
}
|
||||
|
||||
// if (span.parentId && !traceTreeMock[span.parentId]) {
|
||||
// traceTreeMock[span.parentId] = [];
|
||||
// }
|
||||
|
||||
if (span.parentId && traceTreeMock[span.parentId])
|
||||
traceTreeMock[span.parentId].push(span);
|
||||
|
||||
if (!traceTreeMock[span.spanId]) traceTreeMock[span.spanId] = [];
|
||||
|
||||
if (!span["spans"]) span["spans"] = traceTreeMock[span.spanId];
|
||||
}
|
||||
|
||||
traceTree.value = [];
|
||||
traceTree.value.push(getFormattedSpan(spanList.value[0]));
|
||||
traceTree.value[0]["index"] = 0;
|
||||
traceTree.value[0].lowestStartTime =
|
||||
converTimeFromNsToMs(lowestStartTime);
|
||||
traceTree.value[0].highestEndTime = converTimeFromNsToMs(highestEndTime);
|
||||
traceTree.value[0].style.color =
|
||||
serviceColorMapping[traceTree.value[0].serviceName];
|
||||
traceTree.value[0]["spans"] = cloneDeep(
|
||||
traceTreeMock[spanList.value[0]["span_id"]] || []
|
||||
);
|
||||
traceTree.value.push(...noParentSpans);
|
||||
traceTree.value.forEach((span: any) => {
|
||||
addSpansPositions(span, 0);
|
||||
});
|
||||
|
||||
timeRange.value.end = (
|
||||
traceTree.value[0].highestEndTime - traceTree.value[0].lowestStartTime
|
||||
).toFixed(2);
|
||||
timeRange.value.start = 0;
|
||||
|
||||
calculateTracePosition();
|
||||
buildTraceChart();
|
||||
};
|
||||
let index = 0;
|
||||
const addSpansPositions = (span: any, depth: number) => {
|
||||
if (!span.index) index = 0;
|
||||
span.depth = depth;
|
||||
spanPositionList.value.push(
|
||||
Object.assign(span, {
|
||||
style: {
|
||||
color: span.style.color,
|
||||
top: index * spanDimensions.height + "px",
|
||||
left: spanDimensions.gap * depth + "px",
|
||||
},
|
||||
hasChildSpans: !!span.spans.length,
|
||||
currentIndex: index,
|
||||
})
|
||||
);
|
||||
if (collapseMapping.value[span.spanId]) {
|
||||
if (span.spans.length) {
|
||||
span.spans.forEach((childSpan: any) => {
|
||||
index = index + 1;
|
||||
childSpan.totalSpans = addSpansPositions(childSpan, depth + 1);
|
||||
});
|
||||
span.totalSpans = span.spans.reduce(
|
||||
(acc: number, span: any) =>
|
||||
acc + ((span?.spans?.length || 0) + (span?.totalSpans || 0)),
|
||||
0
|
||||
);
|
||||
}
|
||||
return (span?.spans?.length || 0) + (span?.totalSpans || 0);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Convert span object to required format
|
||||
// Converting ns to ms
|
||||
const getFormattedSpan = (span: any) => {
|
||||
return {
|
||||
[store.state.zoConfig.timestamp_column]:
|
||||
span[store.state.zoConfig.timestamp_column],
|
||||
startTimeMs: converTimeFromNsToMs(span.start_time),
|
||||
endTimeMs: converTimeFromNsToMs(span.end_time),
|
||||
durationMs: convertTime(span.duration),
|
||||
idleMs: convertTime(span.idle_ns),
|
||||
busyMs: convertTime(span.busy_ns),
|
||||
spanId: span.span_id,
|
||||
operationName: span.operation_name,
|
||||
serviceName: span.service_name,
|
||||
spanKind: getSpanKind(span.span_kind.toString()),
|
||||
parentId: span.reference_parent_span_id,
|
||||
spans: null,
|
||||
index: 0,
|
||||
style: {
|
||||
color: "",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const convertTime = (time: number) => {
|
||||
return Number((time / 1000000).toFixed(2));
|
||||
};
|
||||
|
||||
const converTimeFromNsToMs = (time: number) => {
|
||||
const nanoseconds = time;
|
||||
const milliseconds = Math.floor(nanoseconds / 1000000);
|
||||
const date = new Date(milliseconds);
|
||||
return date.getTime();
|
||||
};
|
||||
|
||||
const getSpanKind = (spanKind: string) => {
|
||||
const spanKindMapping: { [key: string]: string } = {
|
||||
"1": "Client",
|
||||
"2": "Server",
|
||||
"3": "Producer",
|
||||
"4": "Consumer",
|
||||
"5": "Internal",
|
||||
};
|
||||
return spanKindMapping[spanKind];
|
||||
};
|
||||
|
||||
const closeSidebar = () => {
|
||||
searchObj.data.traceDetails.showSpanDetails = false;
|
||||
searchObj.data.traceDetails.selectedSpanId = null;
|
||||
};
|
||||
const toggleSpanCollapse = (spanId: number | string) => {
|
||||
collapseMapping.value[spanId] = !collapseMapping.value[spanId];
|
||||
index = 0;
|
||||
spanPositionList.value = [];
|
||||
traceTree.value.forEach((span: any) => {
|
||||
addSpansPositions(span, 0);
|
||||
});
|
||||
};
|
||||
const buildTraceChart = () => {
|
||||
const layout: any = {
|
||||
autosize: true,
|
||||
scrollZoom: true,
|
||||
title: {
|
||||
text: "",
|
||||
font: {
|
||||
size: 12,
|
||||
},
|
||||
},
|
||||
font: { size: 12 },
|
||||
height: 200,
|
||||
margin: {
|
||||
l: 16,
|
||||
r: 16,
|
||||
t: 16,
|
||||
b: 16,
|
||||
},
|
||||
xaxis: {
|
||||
ticklen: 5,
|
||||
nticks: 10,
|
||||
tickvals: [],
|
||||
type: "-",
|
||||
ticksuffix: "ms",
|
||||
showgrid: true,
|
||||
zeroline: true,
|
||||
rangeslider: {
|
||||
visible: true,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
showgrid: false,
|
||||
zeroline: false,
|
||||
autorange: true,
|
||||
showticklabels: false,
|
||||
},
|
||||
shapes: [],
|
||||
hovermode: "closest",
|
||||
showlegend: true,
|
||||
};
|
||||
const shapes: any = [];
|
||||
let size = 0;
|
||||
for (let i = spanPositionList.value.length - 1; i > -1; i--) {
|
||||
const absoluteStartTime =
|
||||
spanPositionList.value[i].startTimeMs -
|
||||
traceTree.value[0].lowestStartTime;
|
||||
shapes.push({
|
||||
x0: absoluteStartTime,
|
||||
x1: Number(
|
||||
(absoluteStartTime + spanPositionList.value[i].durationMs).toFixed(
|
||||
2
|
||||
)
|
||||
),
|
||||
y0: size,
|
||||
y1: size + 0.5,
|
||||
line: {
|
||||
width: 0,
|
||||
},
|
||||
type: "rect",
|
||||
xref: "x",
|
||||
yref: "y",
|
||||
opacity: 1,
|
||||
fillcolor: spanPositionList.value[i].style.color,
|
||||
});
|
||||
size += 0.5;
|
||||
}
|
||||
baseTracePosition.value.tics.forEach((tic: any) => {
|
||||
layout.xaxis.tickvals.push(tic.value);
|
||||
});
|
||||
layout.shapes = shapes;
|
||||
traceChart.value.layout = layout;
|
||||
if (plotChart.value) plotChart.value?.reDraw();
|
||||
};
|
||||
const updateChart = ({ data }: { data: any }) => {
|
||||
let range1 = 0;
|
||||
let range2 = 0;
|
||||
if (data["xaxis.range[0]"] && data["xaxis.range[1]"]) {
|
||||
range1 = data["xaxis.range[0]"];
|
||||
range2 = data["xaxis.range[1]"];
|
||||
} else if (data["xaxis.range"]?.length) {
|
||||
range1 = data["xaxis.range"][0];
|
||||
range2 = data["xaxis.range"][1];
|
||||
} else {
|
||||
timeRange.value.start = 0;
|
||||
range2 = Number(
|
||||
(
|
||||
traceTree.value[0].highestEndTime -
|
||||
traceTree.value[0].lowestStartTime
|
||||
).toFixed(2)
|
||||
);
|
||||
}
|
||||
timeRange.value.start = range1;
|
||||
timeRange.value.end = range2;
|
||||
calculateTracePosition();
|
||||
};
|
||||
return {
|
||||
traceTree,
|
||||
collapseMapping,
|
||||
traceRootSpan,
|
||||
baseTracePosition,
|
||||
searchObj,
|
||||
spanList,
|
||||
isSidebarOpen,
|
||||
selectedSpanId,
|
||||
spanMapping,
|
||||
closeSidebar,
|
||||
toggleSpanCollapse,
|
||||
spanPositionList,
|
||||
spanDimensions,
|
||||
splitterModel,
|
||||
plotChart,
|
||||
traceChart,
|
||||
updateChart,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$sidebarWidth: 300px;
|
||||
$seperatorWidth: 2px;
|
||||
$toolbarHeight: 60px;
|
||||
$traceHeaderHeight: 30px;
|
||||
$traceChartHeight: 200px;
|
||||
.toolbar {
|
||||
height: $toolbarHeight;
|
||||
}
|
||||
.trace-details {
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.histogram-container-full {
|
||||
width: 100%;
|
||||
}
|
||||
.histogram-container {
|
||||
width: calc(100% - $sidebarWidth - $seperatorWidth);
|
||||
}
|
||||
|
||||
.histogram-sidebar {
|
||||
width: $sidebarWidth;
|
||||
height: calc(100vh - $toolbarHeight - $traceChartHeight);
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.histogram-spans-container {
|
||||
height: calc(100vh - $toolbarHeight - $traceHeaderHeight - $traceChartHeight);
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.trace-tree-container {
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.trace-details {
|
||||
.q-splitter__before,
|
||||
.q-splitter__after {
|
||||
overflow: revert !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,111 @@
|
||||
<!-- 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
|
||||
class="flex justify-start items-center q-px-sm bg-grey-11"
|
||||
:style="{ height: '30px' }"
|
||||
>
|
||||
<div :style="{ width: 'calc(100% - 22px)' }" class="q-pb-none ellipsis">
|
||||
Span Details
|
||||
</div>
|
||||
<q-btn
|
||||
dense
|
||||
icon="close"
|
||||
class="align-right no-border q-pa-xs"
|
||||
size="xs"
|
||||
@click="closeSidebar"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="q-pb-sm">
|
||||
<div
|
||||
:title="span.operation_name"
|
||||
class="q-px-sm q-pb-none text-h6 ellipsis non-selectable"
|
||||
>
|
||||
{{ span.operation_name }}
|
||||
</div>
|
||||
<div
|
||||
class="q-px-sm text-caption ellipsis non-selectable"
|
||||
:title="span.service_name"
|
||||
>
|
||||
{{ span.service_name }}
|
||||
</div>
|
||||
</div>
|
||||
<q-tabs
|
||||
v-model="activeTab"
|
||||
dense
|
||||
inline-label
|
||||
class="text-bold q-mx-sm span_details_tabs"
|
||||
>
|
||||
<q-tab name="tags" label="Attributes" style="text-transform: capitalize" />
|
||||
</q-tabs>
|
||||
<q-separator style="width: 100%" />
|
||||
<q-tab-panels v-model="activeTab" class="span_details_tab-panels">
|
||||
<q-tab-panel name="tags">
|
||||
<div v-for="key in Object.keys(span)" :key="key">
|
||||
<div class="row q-py-sm q-px-sm border-bottom">
|
||||
<div class="col-12 text-subtitle2 text-grey-8">{{ key }}</div>
|
||||
<div class="col-12 text-subtitle2">{{ span[key] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TraceDetailsSidebar",
|
||||
props: {
|
||||
span: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
},
|
||||
emits: ["close"],
|
||||
setup(props, { emit }) {
|
||||
const activeTab = ref("tags");
|
||||
const closeSidebar = () => {
|
||||
emit("close");
|
||||
};
|
||||
return {
|
||||
activeTab,
|
||||
closeSidebar,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.span_details_tab-panels {
|
||||
height: calc(100% - 130px);
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.span_details_tabs {
|
||||
.span_details_tab-panels {
|
||||
.q-tab-panel {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
.q-tab__indicator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,105 @@
|
||||
<!-- 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
|
||||
:style="{ height: '30px', border: '1px solid #ececec' }"
|
||||
class="flex justify-start items-center bg-grey-11"
|
||||
>
|
||||
<div
|
||||
class="flex justify-start items-center no-wrap row q-px-sm bg-grey-11"
|
||||
:style="{
|
||||
width: splitterWidth + '%',
|
||||
}"
|
||||
>
|
||||
Operation Name
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-start items-center no-wrap row bg-grey-11 relative-position"
|
||||
:style="{
|
||||
width: 100 - splitterWidth + '%',
|
||||
}"
|
||||
>
|
||||
<div class="col-3 text-caption q-pl-xs">
|
||||
{{ baseTracePosition.tics[0].label }}
|
||||
</div>
|
||||
<div class="col-3 text-caption q-pl-xs">
|
||||
{{ baseTracePosition.tics[1].label }}
|
||||
</div>
|
||||
<div class="col-3 text-caption q-pl-xs">
|
||||
{{ baseTracePosition.tics[2].label }}
|
||||
</div>
|
||||
<div class="col-3 text-caption flex justify-between items-center q-px-xs">
|
||||
<div>{{ baseTracePosition.tics[3].label }}</div>
|
||||
<div>{{ baseTracePosition.tics[4].label }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="tick in baseTracePosition['tics']"
|
||||
class="trace-tic"
|
||||
:key="tick.value"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: tick.left,
|
||||
top: '0px',
|
||||
width: '1px',
|
||||
backgroundColor: '#ececec',
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TraceNavbar",
|
||||
props: {
|
||||
baseTracePosition: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
spanDimensions: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
splitterWidth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$toolbarHeight: 60px;
|
||||
$traceHeaderHeight: 30px;
|
||||
$traceChartHeight: 200px;
|
||||
|
||||
.spans-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
.trace-tic {
|
||||
height: calc(100vh - $toolbarHeight - 5px - $traceChartHeight);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<!-- 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 class="q-pl-xs q-pt-sm">
|
||||
<template v-for="span in spans as any[]" :key="span.spanId">
|
||||
<div :style="{ position: 'relative', width: '100%', overflow: 'hidden' }">
|
||||
<div
|
||||
:style="{
|
||||
height: spanDimensions.textHeight - 8 + 'px',
|
||||
margin: `4px 0px 4px ${
|
||||
span.hasChildSpans
|
||||
? span.style.left
|
||||
: parseInt(span.style.left) +
|
||||
spanDimensions.collapseWidth +
|
||||
'px'
|
||||
}`,
|
||||
}"
|
||||
class="flex items-center no-wrap justify-start ellipsis"
|
||||
:title="span.operationName"
|
||||
>
|
||||
<div
|
||||
v-if="span.hasChildSpans"
|
||||
:style="{
|
||||
width: spanDimensions.collapseWidth + 'px',
|
||||
height: spanDimensions.collapseHeight + 'px',
|
||||
}"
|
||||
class="flex justify-center items-center collapse-container cursor-pointer"
|
||||
@click.stop="toggleSpanCollapse(span.spanId)"
|
||||
>
|
||||
<q-icon
|
||||
dense
|
||||
round
|
||||
flat
|
||||
:name="'img:' + getImageURL('images/common/down-solid.svg')"
|
||||
class="collapse-btn"
|
||||
:style="{
|
||||
rotate: collapseMapping[span.spanId] ? '0deg' : '270deg',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="text-blue-grey-6 ellipsis q-ml-xs"
|
||||
:style="{
|
||||
paddingLeft: '4px',
|
||||
borderLeft: `3px solid ${span.style.color}`,
|
||||
}"
|
||||
>
|
||||
{{ span.operationName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { getImageURL } from "@/utils/zincutils";
|
||||
import { computed } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TraceTree",
|
||||
props: {
|
||||
spans: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isCollapsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
collapseMapping: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
baseTracePosition: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
depth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
spanDimensions: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
emits: ["toggleCollapse"],
|
||||
setup(props, { emit }) {
|
||||
function formatTimeWithSuffix(ns: number) {
|
||||
if (ns < 10000) {
|
||||
return `${ns} ms`;
|
||||
} else {
|
||||
return `${(ns / 1000).toFixed(2)} s`;
|
||||
}
|
||||
}
|
||||
function toggleSpanCollapse(spanId: number | string) {
|
||||
emit("toggleCollapse", spanId);
|
||||
}
|
||||
|
||||
return {
|
||||
formatTimeWithSuffix,
|
||||
toggleSpanCollapse,
|
||||
getImageURL,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.spans-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -62,18 +62,25 @@ const stream = {
|
||||
start_time,
|
||||
end_time,
|
||||
query_context,
|
||||
query_fn
|
||||
query_fn,
|
||||
type,
|
||||
}: any) => {
|
||||
const fieldsString = fields.join(",");
|
||||
let url = `/api/${org_identifier}/${stream_name}/_values?fields=${fieldsString}&size=${size}&start_time=${start_time}&end_time=${end_time}&sql=${query_context}`;
|
||||
if (query_fn.trim() != "") {
|
||||
url = url + `&query_fn=${query_fn}`;
|
||||
}
|
||||
let url = `/api/${org_identifier}/${stream_name}/_values?fields=${fieldsString}&size=${size}&start_time=${start_time}&end_time=${end_time}`;
|
||||
if (query_context) url = url + `&sql=${query_context}`;
|
||||
if (query_fn?.trim()) url = url + `&query_fn=${query_fn}`;
|
||||
if (type) url += "&type=" + type;
|
||||
return http().get(url);
|
||||
},
|
||||
|
||||
delete: (org_identifier: string, stream_name: string, stream_type: string) => {
|
||||
return http().delete(`/api/${org_identifier}/${stream_name}?type=${stream_type}`);
|
||||
delete: (
|
||||
org_identifier: string,
|
||||
stream_name: string,
|
||||
stream_type: string
|
||||
) => {
|
||||
return http().delete(
|
||||
`/api/${org_identifier}/${stream_name}?type=${stream_type}`
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -172,6 +172,10 @@ export const useLocalLogFilterField = (val = "", isDelete = false) => {
|
||||
return useLocalStorage("logFilterField", val, isDelete, true);
|
||||
};
|
||||
|
||||
export const useLocalTraceFilterField = (val = "", isDelete = false) => {
|
||||
return useLocalStorage("traceFilterField", val, isDelete, true);
|
||||
};
|
||||
|
||||
export const useLocalUserInfo = (val = "", isDelete = false) => {
|
||||
const userInfo: any = useLocalStorage("userInfo", val, isDelete);
|
||||
return userInfo.value;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<!-- 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/attribute-hyphenation -->
|
||||
<!-- eslint-disable vue/v-on-event-hyphenation -->
|
||||
<template>
|
||||
<!-- <zinc-logs pageType="logs"></zinc-logs> -->
|
||||
<zinc-traces></zinc-traces>
|
||||
</template>
|
||||
Reference in New Issue
Block a user