feat: Query Cancellation (#3207)

Co-authored-by: oasisk <ashish.j.kolhe@gmail.com>
Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
This commit is contained in:
ktx-vaidehi
2024-04-30 22:41:56 +05:30
committed by GitHub
co-authored by oasisk Hengfei Yang
parent 2a73b22a2b
commit 4b92c092a9
17 changed files with 889 additions and 58 deletions
+2
View File
@@ -94,6 +94,7 @@ struct ConfigResponse<'a> {
rum: Rum,
custom_logo_img: Option<String>,
custom_hide_menus: String,
meta_org: String,
}
#[derive(Serialize)]
@@ -234,6 +235,7 @@ pub async fn zo_config() -> Result<HttpResponse, Error> {
api_version: CONFIG.rum.api_version.to_string(),
insecure_http: CONFIG.rum.insecure_http,
},
meta_org: CONFIG.common.usage_org.to_string(),
}))
}
+3 -1
View File
@@ -146,7 +146,9 @@ impl ErrorCodes {
format!("Search field has no compatible data type: {field}")
}
ErrorCodes::SearchSQLExecuteError(_) => "Search SQL execute error".to_string(),
ErrorCodes::SearchCancelQuery(_) => "Search query cancelled".to_string(),
ErrorCodes::SearchCancelQuery(_) => {
"Search query was cancelled by the administrator".to_string()
}
}
}
@@ -0,0 +1,17 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2484_25082)">
<mask id="mask0_2484_25082" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
<path d="M0 0.000289917H99.9997V100H0V0.000289917Z" fill="white"/>
</mask>
<g mask="url(#mask0_2484_25082)">
<path d="M40.5858 13.5317V7.63699C40.5858 5.037 42.693 2.92998 45.293 2.92998H54.7069C57.3067 2.92998 59.4139 5.037 59.4139 7.63699V13.5317C62.8242 14.4121 66.0932 15.769 69.1244 17.5625C69.1244 17.5625 71.293 15.3942 73.298 13.3874C75.1369 11.5501 78.1164 11.5501 79.9552 13.3874L86.6126 20.0446C88.4499 21.8834 88.4499 24.8631 86.6126 26.702C84.606 28.707 82.4375 30.8754 82.4375 30.8754C84.2309 33.9067 85.5879 37.1758 86.4683 40.5861H92.3632C94.9628 40.5861 97.07 42.6933 97.07 45.2931V54.7072C97.07 57.307 94.9628 59.4142 92.3632 59.4142H86.4683C85.5878 62.8244 84.2309 66.0934 82.4375 69.1247C82.4375 69.1247 84.606 71.2931 86.6126 73.2983C88.4499 75.1372 88.4499 78.1167 86.6126 79.9555L79.9552 86.6129C78.1164 88.45 75.1369 88.45 73.298 86.6129C71.293 84.6061 69.1244 82.4377 69.1244 82.4377C66.0932 84.2313 62.8242 85.5882 59.4139 86.4684V92.3633C59.4139 94.9631 57.3067 97.0703 54.7069 97.0703H45.293C42.693 97.0703 40.5858 94.9631 40.5858 92.3633V86.4684C37.1755 85.5882 33.9065 84.2313 30.8753 82.4377C30.8753 82.4377 28.707 84.6061 26.7017 86.6129C24.8628 88.45 21.8833 88.45 20.0445 86.6129L13.3871 79.9555C11.5498 78.1167 11.5498 75.1372 13.3871 73.2983C14.779 71.9073 16.1708 70.5161 17.5623 69.1247C15.7689 66.0934 14.412 62.8244 13.5314 59.4142H7.63671C5.03691 59.4142 2.92969 57.307 2.92969 54.7072V45.2931C2.92969 42.6933 5.03691 40.5861 7.63671 40.5861H13.5314C14.4119 37.1758 15.7689 33.9068 17.5623 30.8754C17.5623 30.8754 15.3939 28.707 13.3871 26.702C11.5498 24.8631 11.5498 21.8834 13.3871 20.0446L20.0445 13.3874C21.8833 11.5501 24.8628 11.5501 26.7017 13.3874C28.707 15.3942 30.8753 17.5625 30.8753 17.5625C33.9065 15.769 37.1756 14.4121 40.5858 13.5317Z" stroke="black" stroke-width="6" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M50.0003 28.0335C62.1239 28.0335 71.9665 37.8759 71.9665 49.9997C71.9665 62.1235 62.1239 71.9658 50.0003 71.9658C37.8767 71.9658 28.0342 62.1235 28.0342 49.9997C28.0342 37.8759 37.8767 28.0335 50.0003 28.0335Z" stroke="black" stroke-width="6" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M46.375 57.4062V56.875C46.3854 55.0521 46.5469 53.599 46.8594 52.5156C47.1823 51.4323 47.651 50.5573 48.2656 49.8906C48.8802 49.224 49.6198 48.6198 50.4844 48.0781C51.1302 47.6615 51.7083 47.2292 52.2187 46.7812C52.7292 46.3333 53.1354 45.8385 53.4375 45.2969C53.7396 44.7448 53.8906 44.1302 53.8906 43.4531C53.8906 42.7344 53.7188 42.1042 53.375 41.5625C53.0313 41.0208 52.5677 40.6042 51.9844 40.3125C51.4115 40.0208 50.776 39.875 50.0781 39.875C49.401 39.875 48.7604 40.026 48.1563 40.3281C47.5521 40.6198 47.0573 41.0573 46.6719 41.6406C46.2865 42.2135 46.0781 42.9271 46.0469 43.7812H39.6719C39.724 41.6979 40.224 39.9792 41.1719 38.625C42.1198 37.2604 43.375 36.2448 44.9375 35.5781C46.5 34.901 48.224 34.5625 50.1094 34.5625C52.1823 34.5625 54.0156 34.9062 55.6094 35.5937C57.2031 36.2708 58.4531 37.2552 59.3594 38.5469C60.2656 39.8385 60.7188 41.3958 60.7188 43.2187C60.7188 44.4375 60.5156 45.5208 60.1094 46.4687C59.7135 47.4062 59.1563 48.2396 58.4375 48.9687C57.7188 49.6875 56.8698 50.3385 55.8906 50.9219C55.0677 51.4115 54.3906 51.9219 53.8594 52.4531C53.3385 52.9844 52.9479 53.599 52.6875 54.2969C52.4375 54.9948 52.3073 55.8542 52.2969 56.875V57.4062H46.375ZM49.4687 67.4063C48.4271 67.4063 47.5365 67.0417 46.7969 66.3125C46.0677 65.5729 45.7083 64.6875 45.7188 63.6562C45.7083 62.6354 46.0677 61.7604 46.7969 61.0313C47.5365 60.3021 48.4271 59.9375 49.4687 59.9375C50.4583 59.9375 51.3281 60.3021 52.0781 61.0313C52.8281 61.7604 53.2083 62.6354 53.2188 63.6562C53.2083 64.3437 53.026 64.974 52.6719 65.5469C52.3281 66.1094 51.875 66.5625 51.3125 66.9062C50.75 67.2396 50.1354 67.4063 49.4687 67.4063Z" fill="black"/>
</g>
</g>
<defs>
<clipPath id="clip0_2484_25082">
<rect width="100" height="100" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

@@ -0,0 +1,58 @@
<template>
<svg
width="100"
height="100"
viewBox="0 0 100 100"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_2484_25082)">
<mask
id="mask0_2484_25082"
style="mask-type: luminance"
maskUnits="userSpaceOnUse"
x="0"
y="0"
width="100"
height="100"
>
<path d="M0 0.000289917H99.9997V100H0V0.000289917Z" fill="white" />
</mask>
<g mask="url(#mask0_2484_25082)">
<path
d="M40.5858 13.5317V7.63699C40.5858 5.037 42.693 2.92998 45.293 2.92998H54.7069C57.3067 2.92998 59.4139 5.037 59.4139 7.63699V13.5317C62.8242 14.4121 66.0932 15.769 69.1244 17.5625C69.1244 17.5625 71.293 15.3942 73.298 13.3874C75.1369 11.5501 78.1164 11.5501 79.9552 13.3874L86.6126 20.0446C88.4499 21.8834 88.4499 24.8631 86.6126 26.702C84.606 28.707 82.4375 30.8754 82.4375 30.8754C84.2309 33.9067 85.5879 37.1758 86.4683 40.5861H92.3632C94.9628 40.5861 97.07 42.6933 97.07 45.2931V54.7072C97.07 57.307 94.9628 59.4142 92.3632 59.4142H86.4683C85.5878 62.8244 84.2309 66.0934 82.4375 69.1247C82.4375 69.1247 84.606 71.2931 86.6126 73.2983C88.4499 75.1372 88.4499 78.1167 86.6126 79.9555L79.9552 86.6129C78.1164 88.45 75.1369 88.45 73.298 86.6129C71.293 84.6061 69.1244 82.4377 69.1244 82.4377C66.0932 84.2313 62.8242 85.5882 59.4139 86.4684V92.3633C59.4139 94.9631 57.3067 97.0703 54.7069 97.0703H45.293C42.693 97.0703 40.5858 94.9631 40.5858 92.3633V86.4684C37.1755 85.5882 33.9065 84.2313 30.8753 82.4377C30.8753 82.4377 28.707 84.6061 26.7017 86.6129C24.8628 88.45 21.8833 88.45 20.0445 86.6129L13.3871 79.9555C11.5498 78.1167 11.5498 75.1372 13.3871 73.2983C14.779 71.9073 16.1708 70.5161 17.5623 69.1247C15.7689 66.0934 14.412 62.8244 13.5314 59.4142H7.63671C5.03691 59.4142 2.92969 57.307 2.92969 54.7072V45.2931C2.92969 42.6933 5.03691 40.5861 7.63671 40.5861H13.5314C14.4119 37.1758 15.7689 33.9068 17.5623 30.8754C17.5623 30.8754 15.3939 28.707 13.3871 26.702C11.5498 24.8631 11.5498 21.8834 13.3871 20.0446L20.0445 13.3874C21.8833 11.5501 24.8628 11.5501 26.7017 13.3874C28.707 15.3942 30.8753 17.5625 30.8753 17.5625C33.9065 15.769 37.1756 14.4121 40.5858 13.5317Z"
stroke="currentColor"
stroke-width="6"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M50.0003 28.0335C62.1239 28.0335 71.9665 37.8759 71.9665 49.9997C71.9665 62.1235 62.1239 71.9658 50.0003 71.9658C37.8767 71.9658 28.0342 62.1235 28.0342 49.9997C28.0342 37.8759 37.8767 28.0335 50.0003 28.0335Z"
stroke="currentColor"
stroke-width="6"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M46.375 57.4062V56.875C46.3854 55.0521 46.5469 53.599 46.8594 52.5156C47.1823 51.4323 47.651 50.5573 48.2656 49.8906C48.8802 49.224 49.6198 48.6198 50.4844 48.0781C51.1302 47.6615 51.7083 47.2292 52.2187 46.7812C52.7292 46.3333 53.1354 45.8385 53.4375 45.2969C53.7396 44.7448 53.8906 44.1302 53.8906 43.4531C53.8906 42.7344 53.7188 42.1042 53.375 41.5625C53.0313 41.0208 52.5677 40.6042 51.9844 40.3125C51.4115 40.0208 50.776 39.875 50.0781 39.875C49.401 39.875 48.7604 40.026 48.1563 40.3281C47.5521 40.6198 47.0573 41.0573 46.6719 41.6406C46.2865 42.2135 46.0781 42.9271 46.0469 43.7812H39.6719C39.724 41.6979 40.224 39.9792 41.1719 38.625C42.1198 37.2604 43.375 36.2448 44.9375 35.5781C46.5 34.901 48.224 34.5625 50.1094 34.5625C52.1823 34.5625 54.0156 34.9062 55.6094 35.5937C57.2031 36.2708 58.4531 37.2552 59.3594 38.5469C60.2656 39.8385 60.7188 41.3958 60.7188 43.2187C60.7188 44.4375 60.5156 45.5208 60.1094 46.4687C59.7135 47.4062 59.1563 48.2396 58.4375 48.9687C57.7188 49.6875 56.8698 50.3385 55.8906 50.9219C55.0677 51.4115 54.3906 51.9219 53.8594 52.4531C53.3385 52.9844 52.9479 53.599 52.6875 54.2969C52.4375 54.9948 52.3073 55.8542 52.2969 56.875V57.4062H46.375ZM49.4687 67.4063C48.4271 67.4063 47.5365 67.0417 46.7969 66.3125C46.0677 65.5729 45.7083 64.6875 45.7188 63.6562C45.7083 62.6354 46.0677 61.7604 46.7969 61.0313C47.5365 60.3021 48.4271 59.9375 49.4687 59.9375C50.4583 59.9375 51.3281 60.3021 52.0781 61.0313C52.8281 61.7604 53.2083 62.6354 53.2188 63.6562C53.2083 64.3437 53.026 64.974 52.6719 65.5469C52.3281 66.1094 51.875 66.5625 51.3125 66.9062C50.75 67.2396 50.1354 67.4063 49.4687 67.4063Z"
fill="currentColor"
/>
</g>
</g>
<defs>
<clipPath id="clip0_2484_25082">
<rect width="100" height="100" fill="white" />
</clipPath>
</defs>
</svg>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "ManagementIcon",
});
</script>
+14 -8
View File
@@ -1,15 +1,21 @@
<template>
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px" height="24px">
<path
d="M9 7C8.359 7 4.639 7 4 7 2.895 7 2 7.895 2 9c0 1.105.895 2 2 2 .639 0 4.359 0 5 0 1.105 0 2-.895 2-2C11 7.895 10.105 7 9 7zM11 4c0 .598 0 2 0 2S9.507 6 9 6C7.895 6 7 5.105 7 4s.895-2 2-2S11 2.895 11 4zM7 14c0 .641 0 4.361 0 5 0 1.105.895 2 2 2 1.105 0 2-.895 2-2 0-.639 0-4.359 0-5 0-1.105-.895-2-2-2C7.895 12 7 12.895 7 14zM4 12c.598 0 2 0 2 0s0 1.493 0 2c0 1.105-.895 2-2 2s-2-.895-2-2S2.895 12 4 12zM14 16c.641 0 4.361 0 5 0 1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-.639 0-4.359 0-5 0-1.105 0-2 .895-2 2C12 15.104 12.895 16 14 16zM12 19c0-.598 0-2 0-2s1.493 0 2 0c1.105 0 2 .895 2 2 0 1.105-.895 2-2 2S12 20.104 12 19zM16 9c0-.641 0-4.361 0-5 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 .639 0 4.359 0 5 0 1.105.895 2 2 2C15.104 11 16 10.104 16 9zM19 11c-.598 0-2 0-2 0s0-1.493 0-2c0-1.105.895-2 2-2 1.105 0 2 .895 2 2S20.104 11 19 11z" />
</svg>
<svg
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24px"
height="24px"
>
<path
d="M9 7C8.359 7 4.639 7 4 7 2.895 7 2 7.895 2 9c0 1.105.895 2 2 2 .639 0 4.359 0 5 0 1.105 0 2-.895 2-2C11 7.895 10.105 7 9 7zM11 4c0 .598 0 2 0 2S9.507 6 9 6C7.895 6 7 5.105 7 4s.895-2 2-2S11 2.895 11 4zM7 14c0 .641 0 4.361 0 5 0 1.105.895 2 2 2 1.105 0 2-.895 2-2 0-.639 0-4.359 0-5 0-1.105-.895-2-2-2C7.895 12 7 12.895 7 14zM4 12c.598 0 2 0 2 0s0 1.493 0 2c0 1.105-.895 2-2 2s-2-.895-2-2S2.895 12 4 12zM14 16c.641 0 4.361 0 5 0 1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-.639 0-4.359 0-5 0-1.105 0-2 .895-2 2C12 15.104 12.895 16 14 16zM12 19c0-.598 0-2 0-2s1.493 0 2 0c1.105 0 2 .895 2 2 0 1.105-.895 2-2 2S12 20.104 12 19zM16 9c0-.641 0-4.361 0-5 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 .639 0 4.359 0 5 0 1.105.895 2 2 2C15.104 11 16 10.104 16 9zM19 11c-.598 0-2 0-2 0s0-1.493 0-2c0-1.105.895-2 2-2 1.105 0 2 .895 2 2S20.104 11 19 11z"
/>
</svg>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "SlackIcon",
})
</script>
name: "SlackIcon",
});
</script>
+165
View File
@@ -0,0 +1,165 @@
<template>
<q-card class="column full-height no-wrap">
<!-- Header -->
<div style="width: 40vw" class="q-px-sm q-py-md">
<q-card-section class="q-pb-sm q-px-sm q-pt-none">
<div class="row items-center no-wrap">
<div class="col">
<div class="text-body1 text-bold" data-test="queryList-title-text">
{{ t("queries.queryList") }}
</div>
</div>
<div class="col-auto">
<q-btn
v-close-popup="true"
data-test="queryList-cancel"
round
flat
icon="cancel"
/>
</div>
</div>
</q-card-section>
<q-separator />
</div>
<q-table
class="my-sticky-virtscroll-table"
virtual-scroll
v-model:pagination="pagination"
:rows-per-page-options="[0]"
:virtual-scroll-sticky-size-start="48"
dense
:rows="getRows(schemaData)"
hide-bottom
hide-header
row-key="index"
wrap-cells
data-test="queryList-table"
>
</q-table>
</q-card>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import { useI18n } from "vue-i18n";
import { timestampToTimezoneDate } from "@/utils/zincutils";
import { useStore } from "vuex";
export default defineComponent({
name: "QueryList",
components: {},
props: {
schemaData: Object,
},
emits: ["save"],
setup(props: any) {
const { t } = useI18n();
const queryData = props.metaData?.queries || [];
const store = useStore();
const getRows = (query: any) => {
const timestampOfStartTime = query?.start_time;
const formattedStartTime = timestampToTimezoneDate(
timestampOfStartTime / 1000,
store.state.timezone,
"yyyy-MM-dd HH:mm:ss"
);
const startTimeEntry = `${formattedStartTime} ${store.state.timezone} (${timestampOfStartTime})`;
const timestampOfEndTime = query?.end_time;
const formattedEndTime = timestampToTimezoneDate(
timestampOfEndTime / 1000,
store.state.timezone,
"yyyy-MM-dd HH:mm:ss"
);
const endTimeEntry = `${formattedEndTime} ${store.state.timezone} (${timestampOfEndTime})`;
const localTimeToMicroseconds = () => {
// Create a Date object representing the current local time
var date = new Date();
// Get the timestamp in milliseconds
var timestampMilliseconds = date.getTime();
// Convert milliseconds to microseconds
var timestampMicroseconds = timestampMilliseconds * 1000;
return timestampMicroseconds;
};
const getDuration = (createdAt: number) => {
const currentTime = localTimeToMicroseconds();
const durationInSeconds = Math.floor(
(currentTime - createdAt) / 1000000
);
let formattedDuration;
if (durationInSeconds < 0) {
formattedDuration = "Invalid duration";
} else if (durationInSeconds < 60) {
formattedDuration = `${durationInSeconds}s`;
} else if (durationInSeconds < 3600) {
const minutes = Math.floor(durationInSeconds / 60);
formattedDuration = `${minutes}m`;
} else {
const hours = Math.floor(durationInSeconds / 3600);
formattedDuration = `${hours}h`;
}
return formattedDuration;
};
//different between start and end time to show in UI as queryRange
const queryRange = (startTime: number, endTime: number) => {
const queryDuration = Math.floor((endTime - startTime) / 1000000);
let formattedDuration;
if (queryDuration < 0) {
formattedDuration = "Invalid duration";
} else if (queryDuration < 60) {
formattedDuration = `${queryDuration}s`;
} else if (queryDuration < 3600) {
const minutes = Math.floor(queryDuration / 60);
formattedDuration = `${minutes}m`;
} else {
const hours = Math.floor(queryDuration / 3600);
formattedDuration = `${hours}h`;
}
return formattedDuration;
};
const rows: any[] = [
["Trace ID", query?.trace_id],
["Status", query?.status],
["User ID", query?.user_id],
["Org ID", query?.org_id],
["Stream Type", query?.stream_type],
["SQL", query?.sql],
["Start Time", startTimeEntry],
["End Time", endTimeEntry],
["Exec. Duration", getDuration(query?.created_at)],
["Query Range", queryRange(query?.start_time, query?.end_time)],
["Scan Records", query?.records],
["Files", query?.files],
["Original Size", query?.original_size],
["Compressed Size", query?.compressed_size],
];
return rows;
};
return {
queryData,
t,
getRows,
pagination: ref({
rowsPerPage: 0,
}),
};
},
});
</script>
<style scoped></style>
@@ -0,0 +1,482 @@
<template>
<div class="running-queries-page" v-if="isMetaOrg">
<q-table
data-test="running-queries-table"
ref="qTable"
:rows="rowsQuery"
:columns="columns"
row-key="trace_id"
style="width: 100%"
>
<template #no-data>
<div v-if="!loadingState" class="text-center full-width full-height">
<NoData />
</div>
<div v-else class="text-center full-width full-height q-mt-lg">
<q-spinner-hourglass color="primary" size="lg" />
</div>
</template>
<template #header-selection="scope">
<q-checkbox v-model="scope.selected" size="sm" color="secondary" />
</template>
<template #body-selection="scope">
<q-checkbox v-model="scope.selected" size="sm" color="secondary" />
</template>
<template #body-cell-actions="props">
<q-td :props="props">
<q-btn
icon="list_alt"
:title="t('queries.queryList')"
class="q-ml-xs"
padding="sm"
unelevated
size="sm"
round
flat
@click="listSchema(props)"
data-test="queryList-btn"
/>
<q-btn
:icon="outlinedCancel"
:title="t('queries.cancelQuery')"
class="q-ml-xs"
padding="sm"
unelevated
size="sm"
style="color: red"
round
flat
@click="confirmDeleteAction(props)"
data-test="cancelQuery-btn"
/>
</q-td>
</template>
<template #top>
<div class="flex justify-between items-center full-width">
<div class="q-table__title" data-test="log-stream-title-text">
{{ t("queries.runningQueries") }}
</div>
<div class="flex items-start">
<div
data-test="streams-search-stream-input"
class="flex items-center"
>
<q-input
v-model="filterQuery"
borderless
filled
dense
class="q-ml-auto q-mb-xs no-border search-input"
:placeholder="t('queries.search')"
data-test="running-queries-search-input"
>
<template #prepend>
<q-icon name="search" />
</template>
</q-input>
<q-btn
data-test="running-queries-refresh-btn"
class="q-ml-md q-mb-xs text-bold no-border"
padding="sm lg"
color="secondary"
no-caps
icon="refresh"
:label="t(`queries.refreshQuery`)"
@click="refreshData"
/>
</div>
</div>
</div>
<div class="label-container">
<label class="q-my-sm text-bold"
>Last Data Refresh Time: {{ lastRefreshed }}</label
>
</div>
</template>
<template #bottom="scope">
<q-table-pagination
data-test="query-stream-table-pagination"
:scope="scope"
:resultTotal="resultTotal"
:perPageOptions="perPageOptions"
position="bottom"
@update:changeRecordPerPage="changePagination"
v-model="filterQuery"
/>
</template>
</q-table>
<confirm-dialog
v-model="deleteDialog.show"
:title="deleteDialog.title"
:message="deleteDialog.message"
@update:ok="deleteQuery"
@update:cancel="deleteDialog.show = false"
/>
<q-dialog
v-model="showListSchemaDialog"
position="right"
full-height
maximized
data-test="list-schema-dialog"
>
<QueryList :schemaData="schemaData" />
</q-dialog>
</div>
</template>
<script lang="ts">
import ConfirmDialog from "@/components/ConfirmDialog.vue";
import useIsMetaOrg from "@/composables/useIsMetaOrg";
import SearchService from "@/services/search";
import {
onBeforeMount,
ref,
type Ref,
defineComponent,
computed,
toRaw,
watch,
} from "vue";
import { useQuasar, type QTableProps, QTable } from "quasar";
import QTablePagination from "@/components/shared/grid/Pagination.vue";
import { useI18n } from "vue-i18n";
import { outlinedCancel } from "@quasar/extras/material-icons-outlined";
import NoData from "@/components/shared/grid/NoData.vue";
import { useStore } from "vuex";
import QueryList from "@/components/queries/QueryList.vue";
export default defineComponent({
name: "RunningQueriesList",
components: { QueryList, ConfirmDialog, QTablePagination, NoData },
setup() {
const store = useStore();
const schemaData = ref({});
const lastRefreshed = ref("");
const { isMetaOrg } = useIsMetaOrg();
const resultTotal = ref<number>(0);
const refreshData = () => {
getRunningQueries();
lastRefreshed.value = getCurrentTime();
};
// Function to get current time in a desired format
const getCurrentTime = () => {
const now = new Date();
const year = now.getFullYear().toString().padStart(4, "0");
const month = (now.getMonth() + 1).toString().padStart(2, "0");
const day = now.getDate().toString().padStart(2, "0");
const hours = now.getHours().toString().padStart(2, "0");
const minutes = now.getMinutes().toString().padStart(2, "0");
const seconds = now.getSeconds().toString().padStart(2, "0");
const timezone = store.state.timezone;
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`;
};
const loadingState = ref(false);
const queries = ref([]);
const deleteDialog = ref({
show: false,
title: "Delete Running Query",
message: "Are you sure you want to delete this running query?",
data: null as any,
});
const qTable: Ref<InstanceType<typeof QTable> | null> = ref(null);
const { t } = useI18n();
const showListSchemaDialog = ref(false);
const listSchema = (props: any) => {
//pass whole props.row to schemaData
schemaData.value = props.row;
showListSchemaDialog.value = true;
};
const perPageOptions: any = [
{ label: "5", value: 5 },
{ label: "10", value: 10 },
{ label: "20", value: 20 },
{ label: "50", value: 50 },
{ label: "100", value: 100 },
];
const selectedPerPage = ref(20);
const pagination: any = ref({
rowsPerPage: 20,
});
const changePagination = (val: { label: string; value: any }) => {
selectedPerPage.value = val.value;
pagination.value.rowsPerPage = val.value;
qTable.value?.setPagination(pagination.value);
};
const filterQuery = ref("");
const q = useQuasar();
const localTimeToMicroseconds = () => {
// Create a Date object representing the current local time
var date = new Date();
// Get the timestamp in milliseconds
var timestampMilliseconds = date.getTime();
// Convert milliseconds to microseconds
var timestampMicroseconds = timestampMilliseconds * 1000;
return timestampMicroseconds;
};
const getDuration = (createdAt: number) => {
const currentTime = localTimeToMicroseconds();
const durationInSeconds = Math.floor((currentTime - createdAt) / 1000000);
let formattedDuration;
if (durationInSeconds < 0) {
formattedDuration = "Invalid duration";
} else if (durationInSeconds < 60) {
formattedDuration = `${durationInSeconds}s`;
} else if (durationInSeconds < 3600) {
const minutes = Math.floor(durationInSeconds / 60);
formattedDuration = `${minutes}m`;
} else {
const hours = Math.floor(durationInSeconds / 3600);
formattedDuration = `${hours}h`;
}
return formattedDuration;
};
//different between start and end time to show in UI as queryRange
const queryRange = (startTime: number, endTime: number) => {
const queryDuration = Math.floor((endTime - startTime) / 1000000);
let formattedDuration;
if (queryDuration < 0) {
formattedDuration = "Invalid duration";
} else if (queryDuration < 60) {
formattedDuration = `${queryDuration}s`;
} else if (queryDuration < 3600) {
const minutes = Math.floor(queryDuration / 60);
formattedDuration = `${minutes}m`;
} else {
const hours = Math.floor(queryDuration / 3600);
formattedDuration = `${hours}h`;
}
return formattedDuration;
};
const columns = ref<QTableProps["columns"]>([
{
name: "#",
label: "#",
field: "#",
align: "left",
},
{
name: "user_id",
field: "user_id",
label: t("user.email"),
align: "left",
sortable: true,
},
{
name: "org_id",
field: "org_id",
label: t("organization.id"),
align: "left",
sortable: true,
},
{
name: "duration",
label: t("queries.duration"),
align: "left",
sortable: true,
field: "duration",
},
{
name: "queryRange",
label: t("queries.queryRange"),
align: "left",
sortable: true,
field: "queryRange",
},
{
name: "status",
field: "status",
label: t("queries.status"),
align: "left",
sortable: true,
},
{
name: "stream_type",
field: "stream_type",
label: t("alerts.streamType"),
align: "left",
sortable: true,
},
{
name: "actions",
field: "actions",
label: t("common.actions"),
align: "center",
},
]);
onBeforeMount(() => {
getRunningQueries();
lastRefreshed.value = getCurrentTime();
});
// Watcher to filter queries based on user_id
const filteredQueries = ref([]);
const filteredRows = computed(() => {
const newVal = filterQuery.value;
if (!newVal) {
return queries.value;
} else {
return queries.value.filter((query: any) =>
query.user_id.toLowerCase().includes(newVal.toLowerCase())
);
}
});
// Watcher to filter queries based on user_id
watch(filterQuery, () => {
// Update the result total based on the filtered array length
resultTotal.value = filteredRows.value.length;
});
const getRunningQueries = () => {
const dismiss = q.notify({
message: "Fetching running queries...",
color: "primary",
position: "bottom",
spinner: true,
});
SearchService.get_running_queries()
.then((response: any) => {
// resultTotal.value = response?.data?.status?.length;
queries.value = response?.data?.status;
resultTotal.value = queries.value.length;
})
.catch((error: any) => {
q.notify({
message:
error.response?.data?.message ||
"Failed to fetch running queries",
color: "negative",
position: "bottom",
timeout: 2500,
});
})
.finally(() => {
dismiss();
});
};
const deleteQuery = () => {
SearchService.delete_running_query(deleteDialog.value.data)
.then(() => {
getRunningQueries();
deleteDialog.value.show = false;
q.notify({
message: "Running query deleted successfully",
color: "positive",
position: "bottom",
timeout: 2500,
});
})
.catch((error: any) => {
q.notify({
message:
error.response?.data?.message || "Failed to delete running query",
color: "negative",
position: "bottom",
timeout: 2500,
});
});
};
const confirmDeleteAction = (props: any) => {
deleteDialog.value.data = props.row.trace_id;
deleteDialog.value.show = true;
};
const rowsQuery = computed(function () {
const rows = toRaw(filteredRows.value) ?? [];
rows.sort((a: any, b: any) => b.created_at - a.created_at);
return rows.map((row: any, index) => {
return {
"#": index < 9 ? `0${index + 1}` : index + 1,
user_id: row?.user_id,
org_id: row?.org_id,
duration: getDuration(row?.created_at),
queryRange: queryRange(row?.query?.start_time, row?.query?.end_time),
status: row?.status,
stream_type: row?.stream_type,
actions: "true",
trace_id: row?.trace_id,
created_at: row?.created_at,
started_at: row?.started_at,
sql: row?.query?.sql,
start_time: row?.query?.start_time,
end_time: row?.query?.end_time,
files: row?.scan_stats?.files,
records: row?.scan_stats?.records,
original_size: row?.scan_stats?.original_size,
compressed_size: row?.scan_stats?.compressed_size,
};
});
});
return {
t,
store,
queries,
columns,
getRunningQueries,
deleteQuery,
confirmDeleteAction,
deleteDialog,
perPageOptions,
listSchema,
showListSchemaDialog,
filterQuery,
changePagination,
outlinedCancel,
schemaData,
loadingState,
refreshData,
lastRefreshed,
isMetaOrg,
resultTotal,
selectedPerPage,
qTable,
rowsQuery,
filteredQueries,
};
},
});
</script>
<style scoped>
.label-container {
display: flex;
width: 100%;
justify-content: flex-end;
}
</style>
<style lang="scss">
.running-queries-page {
.search-input {
width: 400px;
}
}
</style>
+4 -4
View File
@@ -106,9 +106,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
</div>
<div v-else style="margin-top: 17px">
<q-label class="q-pt-md text-bold">{{
<div class="q-pt-md text-bold">{{
t("settings.customLogoText")
}}</q-label
}}</div
><br />
{{ store.state.zoConfig.custom_logo_text || "No Text Available" }}
<q-btn
@@ -124,8 +124,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
<q-separator class="q-mt-sm"></q-separator>
<div class="q-gutter-sm row q-mt-xs">
<q-label class="q-pt-sm text-bold full-width"
>{{ t("settings.customLogoTitle") }} </q-label
<div class="q-pt-sm text-bold full-width"
>{{ t("settings.customLogoTitle") }} </div
><br />
<div
v-if="
+36 -18
View File
@@ -34,8 +34,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
vertical
>
<q-route-tab
exact
default
name="queryManagement"
:to="'/settings/query_management'"
icon="query_stats"
:label="t('settings.queryManagement')"
content-class="tab_content"
v-if="isMetaOrg"
/>
<q-route-tab
name="general"
:to="'/settings/general'"
:icon="outlinedSettings"
@@ -67,6 +74,7 @@ import { useRouter } from "vue-router";
import { useQuasar } from "quasar";
import config from "@/aws-exports";
import { outlinedSettings } from "@quasar/extras/material-icons-outlined";
import useIsMetaOrg from "@/composables/useIsMetaOrg";
export default defineComponent({
name: "PageIngestion",
@@ -76,31 +84,40 @@ export default defineComponent({
const q = useQuasar();
const router: any = useRouter();
const settingsTab = ref("general");
const { isMetaOrg } = useIsMetaOrg();
const handleSettingsRouting = () => {
if (router.currentRoute.value.name === "settings") {
if (isMetaOrg.value && config.isEnterprise === "true") {
settingsTab.value = "queryManagement";
router.push({
path: "/settings/query_management",
query: {
org_identifier: store.state.selectedOrganization.identifier,
},
});
} else {
settingsTab.value = "general";
router.push({
path: "/settings/general",
query: {
org_identifier: store.state.selectedOrganization.identifier,
},
});
}
}
};
onBeforeMount(() => {
if (router.currentRoute.value.name == "settings") {
settingsTab.value = "general";
router.push({ path: "/settings/general" });
}
handleSettingsRouting();
});
// render general settings component
onActivated(() => {
settingsTab.value = "general";
router.push({ path: "/settings/general" });
handleSettingsRouting();
});
onUpdated(() => {
if (router.currentRoute.value.name === "settings") {
settingsTab.value = "general";
router.push({
name: "general",
query: {
org_identifier: store.state.selectedOrganization.identifier,
},
});
return;
}
handleSettingsRouting();
});
return {
@@ -111,6 +128,7 @@ export default defineComponent({
settingsTab,
splitterModel: ref(200),
outlinedSettings,
isMetaOrg,
};
},
});
+2 -21
View File
@@ -62,6 +62,7 @@ import { routeGuard } from "@/utils/zincutils";
import useIngestionRoutes from "./useIngestionRoutes";
import useIamRoutes from "./useIamRoutes";
import config from "@/aws-exports";
import useManagementRoutes from "./useManagementRoutes";
const useRoutes = () => {
const parentRoutes: never[] = [];
@@ -202,27 +203,7 @@ const useRoutes = () => {
routeGuard(to, from, next);
},
},
{
path: "settings",
name: "settings",
component: Settings,
meta: {
keepAlive: true,
},
beforeEnter(to: any, from: any, next: any) {
routeGuard(to, from, next);
},
children: [
{
path: "general",
name: "general",
component: () => import("@/components/settings/General.vue"),
beforeEnter(to: any, from: any, next: any) {
routeGuard(to, from, next);
},
},
],
},
...useManagementRoutes(),
{
path: "functions",
name: "functions",
@@ -0,0 +1,50 @@
import config from "@/aws-exports";
import { routeGuard } from "@/utils/zincutils";
const Settings = () => import("@/components/settings/index.vue");
const useManagementRoutes = () => {
const routes: any = [
{
path: "settings",
name: "settings",
component: Settings,
meta: {
keepAlive: true,
},
beforeEnter(to: any, from: any, next: any) {
routeGuard(to, from, next);
},
children: [
{
path: "general",
name: "general",
component: () => import("@/components/settings/General.vue"),
beforeEnter(to: any, from: any, next: any) {
routeGuard(to, from, next);
},
},
],
},
];
if (config.isEnterprise == "true") {
routes[0].children.push(
...[
{
path: "query_management",
name: "query_management",
component: () =>
import("@/components/queries/RunningQueriesList.vue"),
meta: {
keepAlive: true,
},
beforeEnter(to: any, from: any, next: any) {
routeGuard(to, from, next);
},
},
]
);
}
return routes;
};
export default useManagementRoutes;
+15
View File
@@ -0,0 +1,15 @@
import { computed } from "vue";
import { useStore } from "vuex";
export default function useIsMetaOrg() {
const store = useStore();
const isMetaOrg = computed(() => {
return store.state.selectedOrganization.label === store.state.zoConfig.meta_org;
});
return {
isMetaOrg,
};
}
+5
View File
@@ -1416,6 +1416,11 @@ const useLogs = () => {
if (customMessage != "") {
searchObj.data.errorMsg = t(customMessage);
}
if (err?.response?.data?.code == 429) {
notificationMsg.value = err.response.data.message;
searchObj.data.errorMsg = err.response.data.message;
}
reject(false);
});
});
+6 -1
View File
@@ -72,7 +72,12 @@ const useEnvRoutes = () => {
path: "/cb",
name: "callback",
component: LoginCallback,
},
},
{
path: "/web/cb",
name: "callback",
component: LoginCallback,
},
];
const homeChildRoutes = [
+3 -1
View File
@@ -353,6 +353,7 @@ import {
outlinedDescription,
} from "@quasar/extras/material-icons-outlined";
import SlackIcon from "@/components/icons/SlackIcon.vue";
import ManagementIcon from "@/components/icons/ManagementIcon.vue";
import organizations from "@/services/organizations";
import useStreams from "@/composables/useStreams";
import { openobserveRum } from "@openobserve/browser-rum";
@@ -389,6 +390,7 @@ export default defineComponent({
"q-select": QSelect,
ThemeSwitcher,
SlackIcon,
ManagementIcon,
},
methods: {
navigateToDocs() {
@@ -532,7 +534,7 @@ export default defineComponent({
},
{
title: t("menu.settings"),
icon: outlinedSettings,
iconComponent: markRaw(ManagementIcon),
link: "/settings/",
name: "settings",
},
+19 -4
View File
@@ -51,7 +51,8 @@
"actions": "Actions",
"type": "Type",
"search": "Search",
"expand": "Expand"
"expand": "Expand",
"date": "Date"
},
"search": {
"selectIndex": "Select Stream First",
@@ -134,7 +135,7 @@
"docs": "Docs",
"billings": "Billing",
"rum": "Real User Monitoring",
"settings": "Settings",
"settings": "Management",
"iam": "IAM",
"report": "Reports"
},
@@ -788,7 +789,7 @@
"addToDashboard": "Add to dashboard"
},
"settings": {
"header": "Settings",
"header": "Management",
"userKeyLabel": "User Key",
"rumKeyLabel": "RUM Key",
"newUserKeyBtnLbl": "Add User Key",
@@ -800,7 +801,8 @@
"updateuserKey": "Update User Key",
"id": "Id",
"update": "Update",
"generalLabel": "General",
"generalLabel": "Settings",
"queryManagement": "Query Management",
"apikeyLabel": "API Keys",
"generalPageTitle": "General Settings",
"scrapintervalLabel": "Scrape Interval (In Seconds) *",
@@ -828,5 +830,18 @@
"description": "Description",
"cron": "Cron Job",
"cronExpression": "Cron expression"
},
"queries": {
"runningQueries": "Running Queries",
"search": "Search",
"sessionId": "Session ID",
"initiatedAt": "Initiated At",
"query": "Query",
"queryList": "Query Details",
"duration": "Exec. Duration",
"status": "Status",
"cancelQuery": "Cancel Query",
"queryRange": "Query Range",
"refreshQuery": "Refresh Query"
}
}
+8
View File
@@ -128,6 +128,14 @@ const search = {
const url = `/api/${org_identifier}/_search_partition?type=${page_type}`;
return http().post(url, query);
},
get_running_queries: () => {
const url = `/api/query_manager/status`;
return http().get(url);
},
delete_running_query: (traceID: string) => {
const url = `/api/query_manager/${traceID}`;
return http().delete(url);
},
};
export default search;