Files
openobserve/web/src/components/AutoRefreshInterval.vue
T

411 lines
12 KiB
Vue

<!-- Copyright 2026 OpenObserve Inc.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div class="flex items-center">
<!-- Compact mode: Simple toggle button with dropdown menu -->
<ODropdown v-if="isCompact" v-model:open="btnRefreshInterval" side="bottom" align="start">
<template #trigger>
<OButton
data-test="logs-search-bar-refresh-interval-btn"
size="icon-toolbar"
:variant="variant"
:active="isAnimating"
>
<OIcon
name="update"
:class="isAnimating ? 'auto-refresh-icon--spinning' : ''"
size="sm"
/>
<OTooltip :content="t('search.autoRefreshWithValue', { value: selectedLabel })" />
</OButton>
</template>
<div class="w-75 p-2">
<div class="flex">
<div class="flex w-full flex-col p-2 text-center">
<OButton
data-test="logs-search-off-refresh-interval"
:variant="modelValue.toString() === '0' ? 'primary' : 'ghost'"
size="sm"
:block="true"
@click="
() => {
onItemClick({ label: t('common.off'), value: 0 });
btnRefreshInterval = false;
}
"
>
{{ t("common.off") }}
</OButton>
</div>
</div>
<hr class="border-border-default my-0 border-0 border-t border-solid" />
<div v-for="(items, i) in refreshTimes" :key="'row_' + i" class="flex">
<div
v-for="(item, j) in items"
:key="'col_' + i + '_' + j"
class="flex w-1/3 flex-col p-2 text-center"
>
<OButton
:data-test="`logs-search-bar-refresh-time-${item.value}`"
:variant="Number(modelValue) === item.value ? 'primary' : 'ghost'"
size="sm"
@click="
() => {
onItemClick(item);
btnRefreshInterval = false;
}
"
:disabled="item.disabled"
>
<OTooltip
v-if="item.disabled"
side="right"
align="center"
max-width="18.75rem"
:content="raw(minRangeRestrictionMessageVal)"
/>
{{ item.label }}
</OButton>
</div>
</div>
</div>
</ODropdown>
<!-- Full mode: Dropdown with label -->
<ODropdown v-else v-model:open="btnRefreshInterval" side="bottom" align="start">
<template #trigger>
<OButton
data-test="logs-search-bar-refresh-interval-btn-dropdown"
:variant="variant"
size="sm-toolbar"
>
<div class="flex flex-nowrap items-center">
<OIcon
left
name="update"
size="sm"
:class="[
isAnimating ? 'auto-refresh-icon--spinning' : '',
isAnimating ? 'text-primary' : '',
'me-0.5',
]"
/>
<div class="text-compact text-center leading-4">{{ selectedLabel }}</div>
<OIcon name="arrow-drop-down" size="sm" class="ms-0.5" />
</div>
</OButton>
</template>
<div class="w-75 p-2">
<div class="flex">
<div class="flex w-full flex-col p-2 text-center">
<OButton
data-test="logs-search-off-refresh-interval"
:variant="modelValue.toString() === '0' ? 'primary' : 'ghost'"
size="sm"
:block="true"
@click="
() => {
onItemClick({ label: t('common.off'), value: 0 });
btnRefreshInterval = false;
}
"
>
{{ t("common.off") }}
</OButton>
</div>
</div>
<ODropdownSeparator />
<div v-for="(items, i) in refreshTimes" :key="'row_' + i" class="flex">
<div
v-for="(item, j) in items"
:key="'col_' + i + '_' + j"
class="flex w-1/3 flex-col p-2 text-center"
>
<OButton
:data-test="`logs-search-bar-refresh-time-${item.value}`"
:variant="Number(modelValue) === item.value ? 'primary' : 'ghost'"
size="sm"
@click="
() => {
onItemClick(item);
btnRefreshInterval = false;
}
"
:disabled="item.disabled"
>
<OTooltip
v-if="item.disabled"
side="right"
align="center"
max-width="18.75rem"
:content="raw(minRangeRestrictionMessageVal)"
/>
{{ item.label }}
</OButton>
</div>
</div>
</div>
</ODropdown>
</div>
</template>
<script lang="ts">
import {
defineComponent,
ref,
computed,
watch,
onActivated,
onDeactivated,
onMounted,
onUnmounted,
type PropType,
} from "vue";
import { raw, useI18nTyped } from "@/types/i18n";
import { useRouter } from "vue-router";
import { generateDurationLabel } from "../utils/date";
import OButton from "@/lib/core/Button/OButton.vue";
import type { ButtonVariant } from "@/lib/core/Button/OButton.types";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownSeparator from "@/lib/overlay/Dropdown/ODropdownSeparator.vue";
export default defineComponent({
name: "AutoRefreshInterval",
components: { OButton, ODropdown, ODropdownSeparator, OTooltip, OIcon },
props: {
modelValue: {
type: Number,
default: 0,
},
trigger: {
type: Boolean,
default: false,
},
minRangeRestrictionMessage: {
type: String,
default: "",
},
minRefreshInterval: {
type: Number,
default: 0,
},
isCompact: {
type: Boolean,
default: false,
},
variant: {
type: String as PropType<ButtonVariant>,
default: "outline",
},
},
emits: ["update:modelValue", "trigger"],
setup(props: any, { emit }) {
const router = useRouter();
const { t } = useI18nTyped();
const btnRefreshInterval = ref(false);
let intervalInstance = 0;
// The count lives inside each message ("{count} sec") rather than being
// prefixed to a translated unit — number/unit order is per-language.
const refreshTimes = computed(() => [
[
{ label: t("common.secShort", { count: 5 }), value: 5, disabled: false },
{ label: t("common.minShort", { count: 1 }), value: 60, disabled: false },
{ label: t("common.hrShort", { count: 1 }), value: 3600, disabled: false },
],
[
{ label: t("common.secShort", { count: 10 }), value: 10, disabled: false },
{ label: t("common.minShort", { count: 5 }), value: 300, disabled: false },
{ label: t("common.hrShort", { count: 2 }), value: 7200, disabled: false },
],
[
{ label: t("common.secShort", { count: 15 }), value: 15, disabled: false },
{ label: t("common.minShort", { count: 15 }), value: 900, disabled: false },
{ label: t("common.dayShort", { count: 1 }), value: 86400, disabled: false },
],
[
{ label: t("common.secShort", { count: 30 }), value: 30, disabled: false },
{ label: t("common.minShort", { count: 30 }), value: 1800, disabled: false },
],
]);
// v-model computed value
const selectedValue = computed({
get() {
return props.modelValue;
},
set(value) {
if (isDisabled(value)) {
emit("update:modelValue", 0);
}
emit("update:modelValue", Number(value));
},
});
// computed label based on the selected value
const selectedLabel = computed(() => {
// Handle "Off" case (value 0)
if (selectedValue.value === 0) {
return t("common.off");
}
// Find the label from refreshTimes
const found = refreshTimes.value.flat().find((it: any) => it.value == selectedValue.value);
return found?.label || generateDurationLabel(selectedValue.value);
});
// Check if animation should be active
const isAnimating = computed(() => {
return props.isCompact && selectedValue.value > 0;
});
// Default refresh interval (5 seconds)
const defaultRefreshInterval = 5;
let lastSelectedInterval = 5;
// Toggle refresh on/off
const toggleRefresh = () => {
if (selectedValue.value > 0) {
// Currently ON, turn it OFF
lastSelectedInterval = selectedValue.value;
selectedValue.value = 0;
} else {
// Currently OFF, turn it ON with last interval or default
selectedValue.value = lastSelectedInterval || defaultRefreshInterval;
}
};
// Open dropdown for selecting specific interval (for right-click or future use)
const openDropdown = () => {
btnRefreshInterval.value = true;
};
// update model when the selection has changed
const onItemClick = (item: any) => {
selectedValue.value = item.value;
if (item.value > 0) {
lastSelectedInterval = item.value;
}
};
// watch on the selected value and update the timers
watch(selectedValue, () => {
resetTimers();
});
const resetTimers = () => {
// first reset the current interval callback
clearInterval(intervalInstance);
// return if the interval value is zero
if (selectedValue.value == 0 || !props.trigger) {
return;
}
// if we have the value, continue and set the interval
intervalInstance = window?.setInterval(() => {
emit("trigger");
}, selectedValue.value * 1000);
};
const minRangeRestrictionMessageVal = ref("");
watch(
() => props.minRefreshInterval,
() => {
updateDisabledFlags();
},
);
const isDisabled = (value: number) => {
return value < props.minRefreshInterval;
};
const updateDisabledFlags = () => {
refreshTimes.value.forEach((row) => {
row.forEach((item) => {
item.disabled = isDisabled(item.value);
});
});
minRangeRestrictionMessageVal.value = t("common.minRefreshIntervalMessage", {
interval: props.minRefreshInterval,
});
};
onMounted(() => {
// Initialize the disabled flags on component mount
updateDisabledFlags();
});
// on component mount and unmount, update the intervals
onActivated(() => {
resetTimers();
});
onDeactivated(() => {
clearInterval(intervalInstance);
});
onUnmounted(() => {
clearInterval(intervalInstance);
});
return {
raw,
t,
router,
btnRefreshInterval,
selectedLabel,
refreshTimes,
onItemClick,
minRangeRestrictionMessageVal,
isAnimating,
selectedValue,
toggleRefresh,
openDropdown,
};
},
});
</script>
<style scoped>
/* keep(keyframes): the spinning refresh icon is used only by this component (both
the icon-only and the labelled dropdown trigger share this one class). The
`animation` is declared here rather than as a template `[animation:…]` utility
so Vue's scoped compiler renames the keyframe and this reference together.
The class lands on an OIcon root, which carries this component's scope id too.
`!important` is retained from the original `!`-prefixed utilities. */
.auto-refresh-icon--spinning {
display: inline-block !important;
transform-origin: center center !important;
animation: rotate 2s linear infinite !important;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>