From 4fa43fdff700364d7e1b1fc44fe67f7ae648bab4 Mon Sep 17 00:00:00 2001 From: Sai Nikhil Date: Tue, 23 Sep 2025 15:00:23 +0530 Subject: [PATCH] fix: stream detail page (schema) layout changed (#8585) ### **User description** 1. This PR introduces a new tab called configuration where we moved all the configuration settings to that tab 2. This PR introduces a compact table so that more columns are visible and also all the stream stats above will stay at viewport always ___ ### **PR Type** Enhancement ___ ### **Description** - Redesigned schema page layout and header - Added Configuration tab for settings - New compact stats tiles and sticky table - Added distinct values toggle and UX polish ___ ### Diagram Walkthrough ```mermaid flowchart LR Header["Header: title + name + timeline"] -- "context" --> Tabs["Tabs: Schema Settings / Configuration / Extended Retention"] Tabs -- "schema fields" --> SchemaTbl["Schema table: compact, sticky header, mini-select"] Tabs -- "settings" --> Config["Configuration panel: retention, max range, flatten, toggles"] Tabs -- "retention" --> ExtRet["Extended Retention: date selection + table"] Stats["Stats tiles: Events, Storage, Compressed, Index"] -- "overview" --> Header ```

File Walkthrough

Relevant files
Enhancement
schema.vue
Schema page UI/UX overhaul with new configuration               

web/src/components/logstream/schema.vue
  • Revamped header with stream name badge and timeline.
  • Added stats tiles grid and compact layout.
  • Introduced 'Configuration' tab with inputs and toggles.
  • Improved schema table: sticky header, mini-select, tooltips,
    pagination.
  • Added 'Enable Distinct Values' setting wired to backend.
  • Updated date formatting and preserved original timestamps.
  • Adjusted per-page options and floating footer behavior.
  • Added new icons and theme-aware styling; extensive CSS updates.
+1068/-604
___ --- .../ui-testing/pages/generalPages/streams.js | 7 +- .../playwright-tests/Logs/unflattened.spec.js | 13 +- web/src/assets/images/streams/config.svg | 31 + .../assets/images/streams/config_light.svg | 7 + web/src/assets/images/streams/timeline.svg | 5 + .../assets/images/streams/timeline_light.svg | 12 + web/src/components/logstream/schema.vue | 1646 +++++++++-------- web/src/styles/schema.scss | 293 +++ 8 files changed, 1250 insertions(+), 764 deletions(-) create mode 100644 web/src/assets/images/streams/config.svg create mode 100644 web/src/assets/images/streams/config_light.svg create mode 100644 web/src/assets/images/streams/timeline.svg create mode 100644 web/src/assets/images/streams/timeline_light.svg create mode 100644 web/src/styles/schema.scss diff --git a/tests/ui-testing/pages/generalPages/streams.js b/tests/ui-testing/pages/generalPages/streams.js index 32715226b7..10cf643ff0 100644 --- a/tests/ui-testing/pages/generalPages/streams.js +++ b/tests/ui-testing/pages/generalPages/streams.js @@ -16,6 +16,7 @@ export default class StreamSettingsPage { '[data-test="schema-update-settings-button"]' ); this.closeButton = page.locator('[data-test="schema-cancel-button"]'); + this.configurationTab = page.getByRole('tab', { name: 'Configuration' }) } async updateStreamMaxQueryRange(streamName, newValue) { @@ -35,9 +36,11 @@ export default class StreamSettingsPage { // Wait for stream details to appear and click - await this.streamDetailButton.waitFor({ state: "visible", timeout: 5000 }); + await this.streamDetailButton.waitFor({ state: "visible", timeout: 2000 }); await this.streamDetailButton.click(); - + //before clicking for max query range input we need to go to configuration tab due to schema UI layout change + await this.configurationTab.waitFor({ state: "visible", timeout: 2000 }); + await this.configurationTab.click(); // Wait and update max query range input await this.maxQueryInput.waitFor({ state: "visible", timeout: 15000 }); await this.maxQueryInput.click(); diff --git a/tests/ui-testing/playwright-tests/Logs/unflattened.spec.js b/tests/ui-testing/playwright-tests/Logs/unflattened.spec.js index 58b9dd7e35..78a5e462d9 100644 --- a/tests/ui-testing/playwright-tests/Logs/unflattened.spec.js +++ b/tests/ui-testing/playwright-tests/Logs/unflattened.spec.js @@ -131,7 +131,10 @@ test.describe("Unflattened testcases", () => { await pageManager.unflattenedPage.streamDetailButton.click(); // Strategic 500ms wait for operation completion - this is functionally necessary await page.waitForTimeout(500); - + + //before toggling we need to make sure that we shift to configuration tab + await page.getByRole('tab', { name: 'Configuration' }).waitFor({ state: "visible", timeout: 2000 }); + await page.getByRole('tab', { name: 'Configuration' }).click(); // Toggle 'Store Original Data' and update schema await pageManager.unflattenedPage.storeOriginalDataToggle.waitFor(); // Wait for the toggle to be visible await pageManager.unflattenedPage.storeOriginalDataToggle.click(); @@ -206,7 +209,9 @@ test.describe("Unflattened testcases", () => { await pageManager.unflattenedPage.streamDetailButton.click(); // Strategic 500ms wait for operation completion - this is functionally necessary await page.waitForTimeout(500); - + //before toggling we need to make sure that we shift to configuration tab + await page.getByRole('tab', { name: 'Configuration' }).waitFor({ state: "visible", timeout: 2000 }); + await page.getByRole('tab', { name: 'Configuration' }).click(); // Toggle 'Store Original Data' and update schema await pageManager.unflattenedPage.storeOriginalDataToggle.waitFor(); await pageManager.unflattenedPage.storeOriginalDataToggle.click(); @@ -318,7 +323,9 @@ test.describe("Unflattened testcases", () => { await pageManager.unflattenedPage.streamDetailButton.click(); // Strategic 500ms wait for operation completion - this is functionally necessary await page.waitForTimeout(500); - + //before toggling we need to make sure that we shift to configuration tab + await page.getByRole('tab', { name: 'Configuration' }).waitFor({ state: "visible", timeout: 2000 }); + await page.getByRole('tab', { name: 'Configuration' }).click(); await pageManager.unflattenedPage.storeOriginalDataToggle.waitFor(); await pageManager.unflattenedPage.storeOriginalDataToggle.click(); diff --git a/web/src/assets/images/streams/config.svg b/web/src/assets/images/streams/config.svg new file mode 100644 index 0000000000..f2bc58c100 --- /dev/null +++ b/web/src/assets/images/streams/config.svg @@ -0,0 +1,31 @@ + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/streams/config_light.svg b/web/src/assets/images/streams/config_light.svg new file mode 100644 index 0000000000..85ea797719 --- /dev/null +++ b/web/src/assets/images/streams/config_light.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/streams/timeline.svg b/web/src/assets/images/streams/timeline.svg new file mode 100644 index 0000000000..64f83bb96b --- /dev/null +++ b/web/src/assets/images/streams/timeline.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/web/src/assets/images/streams/timeline_light.svg b/web/src/assets/images/streams/timeline_light.svg new file mode 100644 index 0000000000..fd9b667fc8 --- /dev/null +++ b/web/src/assets/images/streams/timeline_light.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/components/logstream/schema.vue b/web/src/components/logstream/schema.vue index 7d27289c51..8dfd62f3f7 100644 --- a/web/src/components/logstream/schema.vue +++ b/web/src/components/logstream/schema.vue @@ -24,10 +24,54 @@ along with this program. If not, see .
{{ t("logStream.schemaHeader") }} + + + {{ indexData.name }} + + {{ indexData.name }} + + +
+ Timeline Icon +
+ + UTC + +
+ {{ indexData.stats.doc_time_min }} → {{ indexData.stats.doc_time_max }} +
+
+
@@ -40,6 +84,7 @@ along with this program. If not, see . +
. >
-
-
-
-
- {{ t("alerts.stream_name") }} - {{ indexData.name }} -
+ +
+ +
+
- {{ t("logStream.docsCount") }} - - {{ - parseInt(indexData.stats.doc_num).toLocaleString("en-US") - }} - -
-
- {{ t("logStream.storageSize") }} - - {{ formatSizeFromMB(indexData.stats.storage_size) }} -
-
- {{ t("logStream.compressedSize") }} - - {{ formatSizeFromMB(indexData.stats.compressed_size) }} -
-
-
-
- {{ t("logStream.indexSize") }} - - {{ formatSizeFromMB(indexData.stats.index_size) }} -
-
- - Start Time: - {{ indexData.stats.doc_time_min }} - - - - End Time: - {{ indexData.stats.doc_time_max }} - -
-
-
- - -
-
- - - - - - - -
-
- -
-
-
- -
- -
-
-
-
- -
- +
- - Other fields show only the schema fields that existed - before the stream was configured to use a user-defined - schema. - - -
-
- -
- - - - - Add Field(s) - -
-
-
- -
- - - -
-
Add Field(s)
-
- + Events +
+
+ Records Icon
-
- - - - -
-
- - -
- - - - - - - - - - - - - - - -
-
- -
-
- - - - Additional - {{ store.state.zoConfig.extended_data_retention_days }} days of - extension will be applied to the selected date ranges -
-
-
-
- Select Date - +
+ {{ parseInt(indexData.stats.doc_num).toLocaleString("en-US") }} +
- (UTC Timezone) +
+ +
+
+
+
+ {{ t("logStream.storageSize") }} +
+
+ Ingested Size Icon +
+
+
+ {{ formatSizeFromMB(indexData.stats.storage_size) }} +
+
+
+ +
+
+
+
+ {{ t("logStream.compressedSize") }} +
+
+ Compressed Size Icon +
+
+
+ {{ formatSizeFromMB(indexData.stats.compressed_size) }} +
+
+
+ +
+
+
+
+ {{ t("logStream.indexSize") }} +
+
+ Index Size Icon +
+
+
+ {{ formatSizeFromMB(indexData.stats.index_size) }} +
+
+
+
+
+ +
+
+
+ + + + + + + + + + +
+
+ +
+
+
+ +
+
+
+
+ +
+ + + Other fields show only the schema fields that existed + before the stream was configured to use a user-defined + schema. + + +
+
+ +
+ + + + + + +
+
+ +
+ + + +
+
Add Field(s)
+
+ +
+
+
+ + + + +
-
+ +
+ - + + + + + + +
-
- + + +
+
+ +
+ +
+ +
+ + + + Global retention is {{ store.state.zoConfig.data_retention_days }} days + + + +
+ + Retention period must be at least 1 day + +
+
-
+ +
+ + + + Maximum time range allowed for queries. Set 0 for unlimited range. + +
+ + +
+ + + + Global is {{ store.state.zoConfig.ingest_flatten_level || 3 }} + +
+ + +
+ + Use Stream Stats for Partitioning + + +
+ +
+ Store Original Data + +
+ +
+ Enable Distinct Values + +
+
+ +
+
+
+ + +
+
+ + + + Additional + {{ store.state.zoConfig.extended_data_retention_days }} days of + extension will be applied to the selected date ranges +
+
+
+
+ Select Date + +
+ (UTC Timezone) +
+ +
+ + + + + + + + +
+
+
+
-
- {{ selectedFields.length }} fields - selected - - - - - - {{ - activeTab === "schemaFields" - ? t("logStream.removeSchemaField") - : t("logStream.addSchemaField") - }} - - - - - {{ t("logStream.delete") }} - - -
-
- - +
+
+ {{ selectedFields.length }} fields + selected + + + + + + {{ + activeTab === "schemaFields" + ? t("logStream.removeSchemaField") + : t("logStream.addSchemaField") + }} + + + + + {{ t("logStream.delete") }} + + +
+
+ + +
+
-


@@ -750,6 +997,7 @@ import { useStore } from "vuex"; import { useQuasar, date, format } from "quasar"; import streamService from "../../services/stream"; import segment from "../../services/segment_analytics"; +import "../../styles/schema.scss"; import { formatSizeFromMB, getImageURL, @@ -817,6 +1065,7 @@ export default defineComponent({ const isCloud = config.isCloud; const dataRetentionDays = ref(0); const storeOriginalData = ref(false); + const enableDistinctFields = ref(false); const maxQueryRange = ref(0); const flattenLevel = ref(null); const confirmQueryModeChangeDialog = ref(false); @@ -843,12 +1092,11 @@ export default defineComponent({ const redDaysList = ref([]); const resultTotal = ref(0); const perPageOptions: any = [ - { label: "5", value: 5 }, - { label: "10", value: 10 }, { label: "20", value: 20 }, { label: "50", value: 50 }, { label: "100", value: 100 }, - { label: "All", value: 0 }, + { label: "250", value: 250 }, + { label: "500", value: 500 }, ]; const changePagination = (val: { label: string; value: any }) => { @@ -938,6 +1186,7 @@ export default defineComponent({ dataRetentionDays.value = store.state.zoConfig.data_retention_days || 0; maxQueryRange.value = 0; storeOriginalData.value = false; + enableDistinctFields.value = false; approxPartition.value = false; }); @@ -1114,15 +1363,19 @@ export default defineComponent({ indexData.value.schema = streamResponse.schema || []; indexData.value.stats = JSON.parse(JSON.stringify(streamResponse.stats)); + indexData.value.stats.original_doc_time_max = streamResponse.stats.doc_time_max; + indexData.value.stats.original_doc_time_min = streamResponse.stats.doc_time_min; + indexData.value.stats.doc_time_max = date.formatDate( parseInt(streamResponse.stats.doc_time_max) / 1000, - "YYYY-MM-DD THH:mm:ss:SS Z", + "YYYY-MM-DDTHH:mm:ss:SS", ); indexData.value.stats.doc_time_min = date.formatDate( parseInt(streamResponse.stats.doc_time_min) / 1000, - "YYYY-MM-DD THH:mm:ss:SS Z", + "YYYY-MM-DDTHH:mm:ss:SS", ); + indexData.value.defined_schema_fields = streamResponse.settings.defined_schema_fields || []; @@ -1136,6 +1389,7 @@ export default defineComponent({ flattenLevel.value = streamResponse.settings.flatten_level || null; storeOriginalData.value = streamResponse.settings.store_original_data || false; + enableDistinctFields.value = streamResponse.settings.enable_distinct_fields || false; approxPartition.value = streamResponse.settings.approx_partition || false; if (!streamResponse.schema) { @@ -1232,6 +1486,7 @@ export default defineComponent({ } settings["store_original_data"] = storeOriginalData.value; + settings["enable_distinct_fields"] = enableDistinctFields.value; settings["approx_partition"] = approxPartition.value; if (flattenLevel.value !== null) { @@ -1933,6 +2188,20 @@ export default defineComponent({ : getImageURL("images/common/quick_mode.svg"); }); + const getConfigIcon = computed(() => { + return store.state.theme === "dark" + ? getImageURL("images/streams/config_light.svg") + : getImageURL("images/streams/config.svg"); + }); + const getTimelineIcon = computed(() => { + return store.state.theme === "dark" + ? getImageURL("images/streams/timeline_light.svg") + : getImageURL("images/streams/timeline.svg"); + }); + + + + return { t, q, @@ -1949,6 +2218,7 @@ export default defineComponent({ getImageURL, dataRetentionDays, storeOriginalData, + enableDistinctFields, approxPartition, maxQueryRange, flattenLevel, @@ -2021,6 +2291,8 @@ export default defineComponent({ updateIndexType, isEnvQuickModeField, quickModeIcon, + getConfigIcon, + getTimelineIcon, }; }, created() { @@ -2042,7 +2314,6 @@ export default defineComponent({ padding: 8px 16px; } .indexDetailsContainer { - padding: 1.25rem; width: 100%; .title { @@ -2145,7 +2416,7 @@ export default defineComponent({ .mapping-warning-msg { background-color: #f9f290; - padding: 8px 16px; + padding: 4px 16px; border-radius: 4px; border: 1px solid #f5a623; color: #865300; @@ -2186,150 +2457,7 @@ export default defineComponent({ padding: 0; min-width: 40px; } - -// .sticky-buttons { -// position: sticky; -// bottom: 0px; -// margin: 0 auto; -// background-color: var(--q-accent); -// box-shadow: 6px 6px 18px var(--q-accent); -// justify-content: right; -// width: 100%; -// padding-right: 20px; -// } - -// .btn-delete { -// left: 20px; -// position: absolute; -// } - -// .sticky-table-header { -// position: sticky; -// top: 0px; -// background: var(--q-accent); -// z-index: 1; -// } - -// .body--dark { -// .sticky-table-header { -// background: var(--q-dark); -// } - -// .sticky-buttons { -// background-color: var(--q-dark); -// box-shadow: 6px 6px 18px var(--q-dark); -// } -// } .single-line-tab { display: inline-flex; } - - - + \ No newline at end of file diff --git a/web/src/styles/schema.scss b/web/src/styles/schema.scss new file mode 100644 index 0000000000..fd860f417d --- /dev/null +++ b/web/src/styles/schema.scss @@ -0,0 +1,293 @@ + + + +.add-fields-card { + width: 100vw; + max-width: 100%; + display: flex; + flex-direction: column; +} +.indexDetailsContainer { + .q-table__control { + width: 100%; + } + + .q-table { + td:nth-child(2) { + min-width: 15rem; + width: 15rem; + max-width: 15rem; + overflow: auto; + scrollbar-width: thin; + scrollbar-color: #999 #f0f0f0; + } + td:nth-child(3) { + padding: 4px 8px !important; + } + } + + th:first-child, + td:first-child { + padding-left: 8px !important; + } + + th:nth-child(5), + td:nth-child(5){ + min-width: 15rem; + width: 15rem; + max-width: 15rem; + } +} +.dark-theme-table tr:hover td:nth-child(2) { + background-color: #272a2b !important; +} +.light-theme-table tr:hover td:nth-child(2) { + background-color: #f7f7f7 !important; +} + +.schema-fields-tabs { + height: fit-content; + .rum-tab { + width: fit-content !important; + padding: 4px 12px !important; + border: none !important; + + &.active { + background: #5960b2; + color: #ffffff !important; + } + } +} +.main-fields-tabs { + height: fit-content; + color: var(--q-light); + .rum-tab { + padding: 4px 12px !important; + + &.active { + color: white !important; + border-bottom: 5px solid var(--q-primary) !important; + background-color: rgba(88, 96, 178, 0.6); + } + } +} +.floating-buttons { + position: sticky; + bottom: 0; + z-index: 1; /* Ensure it stays on top of table content */ + width: 100%; + margin-top: auto; /* Push to bottom when there's less content */ +} +.dark-theme-floating-buttons { + background-color: #181a1b; +} +.light-theme-floating-buttons { + background-color: #ffffff +} + +.warning-text { + color: #f5a623; + border: 1px solid #f5a623; + border-radius: 10px; + padding: 6px 3px; +} +.mini-select .q-field__control { + min-height: 24px !important; + padding: 0px 4px !important; +} + +.mini-select .q-field__control-container { + padding-top: 0px !important; + padding-bottom: 0px !important; + min-height: 20px !important; +} + +.mini-select .q-field__control-container span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: inline-block; + font-size: 11px !important; + line-height: 12px !important; +} + +.mini-select .q-field__marginal { + height: 20px !important; +} + +.mini-select.q-field--auto-height.q-field--dense .q-field__control, +.mini-select.q-field--auto-height.q-field--dense .q-field__native { + min-height: 24px !important; + height: 24px !important; +} + +.mini-select.q-field--dense .q-field__control, +.mini-select.q-field--dense .q-field__marginal { + min-height: 12px !important; + height: 12px !important; +} + +.mini-select .q-field--dense .q-field__control, +.mini-select .q-field--dense .q-field__marginal { + min-height: 12px !important; + height: 12px !important; +} + +/* Custom clear icon styling */ +.mini-select .q-icon { + font-size: 14px !important; + width: 14px !important; + height: 14px !important; + opacity: 0.7; + font-weight: 900 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + line-height: 1 !important; + margin-top: 16px !important; +} + +.mini-select .q-icon:hover { + opacity: 1; +} + +/* Move dropdown arrow down and add rotation animation */ +.mini-select .q-field__append .q-icon { + margin-top: 14px; + transition: transform 0.2s ease; +} + +/* Rotate arrow when dropdown is open */ +.mini-select.q-field--focused .q-field__append .q-icon { + transform: rotate(180deg); +} +.schema-index-select-dropdown-light{ + background-color: white !important; + border-radius: 4px !important; + border: 1px solid #C9CCD6 !important; +} + +.schema-index-select-dropdown-light.q-field--focused .q-field__control { + outline: 2px solid #575FC5 !important; + box-shadow: none !important; + background-color: #FFFFFF !important; + border-radius: 4px !important; +} + +.schema-index-select-dropdown-dark{ + background-color: #2B2D30 !important; + border-radius: 4px !important; + border: 1px solid #4E5157 !important; +} + +.schema-index-select-dropdown-dark.q-field--focused .q-field__control { + outline: 2px solid #575FC5 !important; + box-shadow: none !important; + background-color: #2B2D30 !important; + border-radius: 4px !important; +} + +/* Field name ellipsis styling */ +.field-name-ellipsis { + max-width: 200px; +} + +.field-name-text { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + +} + +.o2-schema-table{ + th,td { + height: 14px !important; + min-height: 14px !important; + padding: 2px 3px !important; + } + thead { + position: sticky; + top: 0; + z-index: 1000; + } +} + +.o2-schema-table-header-sticky-dark{ + thead { + background-color: #181A1B; + } +} + +.o2-schema-table-header-sticky-light{ + thead { + background-color: #ffffff; + } +} + + +/* Field type badge styling */ +.field-type-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 6px; + font-size: 11px; + font-weight: 600; +} + +.badge-int64 { + background: #dbeafe; + color: #1d4ed8; +} + +.badge-float64 { + background: #d1fae5; + color: #065f46; +} + +.badge-utf8 { + background: #fef3c7; + color: #92400e; +} + +.schema-input-box{ + background-color: white !important; + border-radius: 4px !important; + padding: 0px 8px !important; + border: 1px solid #C9CCD6 !important; + .q-field__control{ + color: transparent !important; + + } +} + +.schema-input-box-light{ + .q-field__control{ + background-color: #ffffff !important; + outline: 0px 1px 0px 1px solid #C9CCD6 !important; + padding: 0px 6px !important; + } +} + +.schema-input-box-dark{ + background-color: #2B2D30 !important; + border-radius: 4px !important; + border: 1px solid #4E5157 !important; +} +.bottom-btn { + display: flex; + width: 100%; + justify-content: space-between; + align-items: center; +} + +.tile-content-dark{ + background-color: #181a1b; +} + +.tile-content-light{ + background-color: #ffffff; +}