@@ -17,12 +26,13 @@
import ChevronDown from "vue-material-design-icons/ChevronDown.vue"
import Close from "vue-material-design-icons/Close.vue"
- const emit = defineEmits(["previous", "next", "close"])
+ const emit = defineEmits(["previous", "next", "close", "select"])
const props = defineProps<{
cursorIdx?: number;
totalCount: number;
level: string;
+ filterMode?: boolean;
}>()
const isSelected = computed(() => props.cursorIdx !== undefined)
@@ -37,11 +47,21 @@
diff --git a/ui/src/components/logs/LogsWrapper.vue b/ui/src/components/logs/LogsWrapper.vue
index ebeb2540e13..7be78a4a1ac 100644
--- a/ui/src/components/logs/LogsWrapper.vue
+++ b/ui/src/components/logs/LogsWrapper.vue
@@ -24,13 +24,10 @@
@filter="onFilterRouteSync"
/>
setLevelRouteValue({value, direction: 'min'})"
@update:time-range="onQuickFilterTimeRange"
/>
@@ -41,15 +38,36 @@
+
+
+ {{ t('download_logs_description') }}
+ (downloadLevel = value)"
+ @update:time-range="(value: string) => (downloadTimeRange = value)"
+ />
+
+ {{ t('cancel') }}
+
+ {{ t('download') }}
+
+
+
@@ -101,6 +139,13 @@
import YAML_CHART from "../dashboard/assets/logs_timeseries_chart.yaml?raw"
import {useLogsStore} from "../../stores/logs"
import useRouteContext from "../../composables/useRouteContext"
+ import * as Utils from "../../utils/utils"
+ import {useToast} from "../../utils/toast"
+ import Download from "vue-material-design-icons/Download.vue"
+ import ContentCopy from "vue-material-design-icons/ContentCopy.vue"
+ import LogDisplaySettings from "./LogDisplaySettings.vue"
+ import LogLevelNavigator from "./LogLevelNavigator.vue"
+ import {buildValueFilterQuery} from "./logValueFilter"
const props = withDefaults(defineProps<{
logLevel?: string;
@@ -124,6 +169,7 @@
const route = useRoute()
const router = useRouter()
const {t} = useI18n()
+ const toast = useToast()
const logsStore = useLogsStore()
const logFilter = useLogFilter()
const {VALUES} = useValues("logs")
@@ -158,7 +204,6 @@
const {
effectiveValue: effectiveLogLevel,
syncFromAppliedFilters: syncLevelFromAppliedFilters,
- setRouteValue: setLevelRouteValue,
} = useRouteFilterPolicy({
enabled: () => !props.filters && hasLevelFilterUI.value,
explicitValue: () => props.logLevel ? {value: props.logLevel, direction: "min"} : undefined,
@@ -182,6 +227,11 @@
)
},
})
+ const searchTerm = computed(() => {
+ const key = Object.keys(route.query).find((k) => k.startsWith("filters[q]"))
+ return key ? String(route.query[key] ?? "") : ""
+ })
+
const selectedTimeRange = computed(() => {
if (route.query.timeRange) {
return route.query.timeRange as string
@@ -270,6 +320,92 @@
})
}
+ const downloadOpen = ref(false)
+ const downloadLevel = ref(undefined)
+ const downloadTimeRange = ref(undefined)
+ const downloading = ref(false)
+
+ const openDownload = () => {
+ downloadLevel.value = effectiveLogLevel.value?.value
+ downloadTimeRange.value = selectedTimeRange.value ?? undefined
+ downloadOpen.value = true
+ }
+
+ const downloadLogs = () => {
+ const {
+ page: _p, size: _s, sort: _so, logsPage: _lp, logsSize: _ls,
+ level: _l, startDate: _sd, endDate: _ed, ...routeFilters
+ } = route.query
+ const params: Record = props.filters ? {...props.filters} : {...routeFilters}
+
+ if (isFlowEdit.value) {
+ params["filters[namespace][EQUALS]"] = routeNamespace.value
+ params["filters[flowId][EQUALS]"] = flowId.value
+ } else if (isNamespaceEdit.value) {
+ params["filters[namespace][EQUALS]"] = routeNamespace.value
+ }
+
+ Object.keys(params)
+ .filter((k) => k.startsWith("filters[level]"))
+ .forEach((k) => delete params[k])
+ if (downloadLevel.value) {
+ params["filters[level][GREATER_THAN_OR_EQUAL_TO]"] = downloadLevel.value
+ }
+
+ if (downloadTimeRange.value) {
+ params.startDate = moment()
+ .subtract(moment.duration(downloadTimeRange.value).as("milliseconds"))
+ .toISOString(true)
+ params.endDate = moment().toISOString(true)
+ } else {
+ if (startDate.value) params.startDate = startDate.value
+ if (endDate.value) params.endDate = endDate.value
+ }
+ params.sort = "timestamp:desc"
+
+ downloading.value = true
+ logsStore.downloadLogs(params)
+ .then(() => (downloadOpen.value = false))
+ .finally(() => (downloading.value = false))
+ }
+
+ const LEVEL_ORDER = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]
+ const serverLevelCounts = ref>({})
+ const presentLevels = computed(() => LEVEL_ORDER.filter((level) => (serverLevelCounts.value[level] ?? 0) > 0))
+
+ let lastCountedKey = ""
+ const refreshLevelCounts = () => {
+ if (!loadInit.value || lastCountedKey === filterQueryKey.value) return
+ const key = filterQueryKey.value
+ lastCountedKey = key
+ logsStore.levelCounts(loadQuery({})).then((counts) => {
+ if (key === filterQueryKey.value) serverLevelCounts.value = counts
+ })
+ }
+
+ const selectLevel = (level: string) => {
+ const query: Record = {...route.query}
+ Object.keys(query)
+ .filter((key) => key.startsWith("filters[level]"))
+ .forEach((key) => delete query[key])
+ query["filters[level][GREATER_THAN_OR_EQUAL_TO]"] = level
+ query[pageKey] = "1"
+ router.push({query})
+ }
+
+ const onValueFilter = ({field, value, negate}: {field: string; value: string; negate: boolean}) => {
+ const query = buildValueFilterQuery(route.query, field, value, negate, pageKey)
+ if (query) router.push({query})
+ }
+
+ const copyAllLogs = () => {
+ const text = (logsStore.logs ?? [])
+ .map((l: any) => `${(l.level ?? "").padEnd(5)} ${l.timestamp} ${(l.message ?? "").replace(/\s+$/, "")}`)
+ .join("\n")
+ Utils.copy(text)
+ toast.success(t("logs_copied"))
+ }
+
const onFilterRouteSync = (filters: AppliedFilter[]) => {
if (props.filters || !hasLevelFilterUI.value) {
return
@@ -291,6 +427,7 @@
}
const onLoaded = () => {
+ refreshLevelCounts()
if (!pinToBottom.value) return
pinToBottom.value = false
const main = document.querySelector("main")
@@ -332,10 +469,49 @@