Skip to content
75 changes: 60 additions & 15 deletions pmm-app/src/pmm-qan/panel/QueryAnalytics.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import React, {
FC, useCallback, useContext, useEffect, useRef, useState,
FC,
useCallback,
useContext,
useEffect,
useLayoutEffect,
useRef,
useState,
useMemo,
} from 'react';
import SplitPane from 'react-split-pane';
import { Button, useTheme } from '@grafana/ui';
import type { GrafanaTheme } from '@grafana/data';
import { cx } from '@emotion/css';
import { showSuccessNotification, showWarningNotification } from 'shared/components/helpers';
import { ConfigProvider } from 'antd';
import { antdTheme } from 'shared/core/theme';
import { getAntdTheme } from 'shared/core/theme';
import { applyPmmCssVariables } from 'shared/components/helpers/getPmmTheme';
import { QueryAnalyticsProvider, UrlParametersProvider } from './provider/provider';
import {
Details, Filters, ManageColumns, Overview,
Details,
Filters,
ManageColumns,
Overview,
} from './components';
import 'shared/styles.scss';
import 'shared/style.less';
Expand All @@ -18,15 +30,21 @@ import { getStyles } from './QueryAnalytics.styles';
import { Messages } from './QueryAnalytics.messages';
import { buildShareLink, toUnixTimestamp } from './QueryAnalytics.tools';

const QueryAnalyticsPanel: FC = () => {
const theme = useTheme();
const styles = getStyles(theme);
// Panel now receives theme from root.
interface QueryAnalyticsPanelProps {
grafanaTheme: GrafanaTheme;
}

const QueryAnalyticsPanel: FC<QueryAnalyticsPanelProps> = ({ grafanaTheme }) => {
const styles = getStyles(grafanaTheme);

const {
panelState: { querySelected, from, to },
} = useContext(QueryAnalyticsProvider);

const queryAnalyticsWrapper = useRef<HTMLDivElement>(null);
const [, setReload] = useState<object>({});

const copyLinkToClipboard = useCallback(() => {
const link = buildShareLink(toUnixTimestamp(from), toUnixTimestamp(to));

Expand All @@ -50,7 +68,15 @@ const QueryAnalyticsPanel: FC = () => {
}, [querySelected]);

return (
<div className="query-analytics-grid" id="antd" ref={queryAnalyticsWrapper}>
// Force remount when theme changes to ensure Ant Design components (Table, Select, Checkbox)
// pick up the new theme from ConfigProvider. Without this, components render with wrong colors
// until page refresh (e.g., dark mode Table in light theme).
<div
key={grafanaTheme.type}
className="query-analytics-grid"
id="antd"
ref={queryAnalyticsWrapper}
>
<div className="overview-filters">
<Filters />
</div>
Expand Down Expand Up @@ -82,7 +108,9 @@ const QueryAnalyticsPanel: FC = () => {
pane2Style={{ minHeight: '20%', zIndex: 999 }}
>
<Overview />
<div className={styles.detailsWrapper}>{querySelected ? <Details /> : null}</div>
<div className={styles.detailsWrapper}>
{querySelected ? <Details /> : null}
</div>
</SplitPane>
</div>
</div>
Expand All @@ -91,10 +119,27 @@ const QueryAnalyticsPanel: FC = () => {
);
};

export default (props) => (
<ConfigProvider theme={antdTheme}>
<UrlParametersProvider {...props}>
<QueryAnalyticsPanel />
</UrlParametersProvider>
</ConfigProvider>
);
const QueryAnalyticsRoot: FC<any> = (props) => {
const grafanaTheme = useTheme();

const antdTheme = useMemo(
() => getAntdTheme(grafanaTheme),
[grafanaTheme],
);

// Apply CSS variables for QAN theme (dropdowns, backgrounds, text colors)
// useLayoutEffect runs synchronously before browser paint, ensuring styles are applied before render
useLayoutEffect(() => {
applyPmmCssVariables(grafanaTheme);
}, [grafanaTheme, grafanaTheme.type]);

return (
<ConfigProvider theme={antdTheme}>
<UrlParametersProvider {...props}>
<QueryAnalyticsPanel grafanaTheme={grafanaTheme} />
</UrlParametersProvider>
</ConfigProvider>
);
};

export default QueryAnalyticsRoot;
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Databases } from 'shared/core';
import { getServiceType } from './Filters.tools';
import { FilterGroup } from './Filters.types';

export const FILTERS_BODY_HEIGHT = 600;
export const FILTERS_HEADER_SIZE = 50;
Expand All @@ -20,7 +21,7 @@ export const HIDDEN_FILTER_LABELS = [
'top_queryid',
];

export const FILTERS_GROUPS = [
export const FILTERS_GROUPS: FilterGroup[] = [
{
name: 'Environment',
dataKey: 'environment',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const getStyles = stylesFactory((theme: GrafanaTheme) => {

return {
getFiltersWrapper: (height) => css`
border: 1px solid rgba(40, 40, 40);
border: 1px solid ${theme.colors.border2};
height: ${height}px;
padding: 10px 16px !important;
border-radius: 3px;
Expand All @@ -22,7 +22,7 @@ export const getStyles = stylesFactory((theme: GrafanaTheme) => {
}
`,
icon: css`
fill: #c6c6c6;
fill: ${theme.colors.textWeak};
`,
filtersHeader: css`
display: flex;
Expand Down
13 changes: 8 additions & 5 deletions pmm-app/src/pmm-qan/panel/components/Filters/Filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,17 @@ export const Filters: FC = () => {
setFilter('');
}}
>
<div ref={filtersWrapperRef} className={cx({ [styles.filtersDisabled]: loadingDetails })}>
<div
ref={filtersWrapperRef}
className={cx({ [styles.filtersDisabled]: loadingDetails })}
>
<FiltersHeader loading={loading} />
<Overlay isPending={loading}>
<Scrollbar className={styles.getFiltersWrapper(height)}>
<FilterInput filter={filter} />
{filtersGroups.filter((group) => filters[group.dataKey]).map(
({ name, dataKey, getDashboardURL }) => (
{filtersGroups
.filter((group) => filters[group.dataKey])
.map(({ name, dataKey, getDashboardURL }) => (
<CheckboxGroup
key={name}
name={name}
Expand All @@ -130,8 +134,7 @@ export const Filters: FC = () => {
getDashboardURL={getDashboardURL}
rawTime={rawTime}
/>
),
)}
))}
</Scrollbar>
</Overlay>
</div>
Expand Down
19 changes: 19 additions & 0 deletions pmm-app/src/pmm-qan/panel/components/Filters/Filters.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface FilterGroup {
name: string;
dataKey: string;
getDashboardURL?: (value: string) => string;
}

export interface FilterItem {
value: string;
checked: boolean;
main_metric_percent?: number;
}

export interface FilterData {
name: FilterItem[];
}

export interface Filters {
[key: string]: FilterData;
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ export const getStyles = stylesFactory((theme: GrafanaTheme) => {
margin-top: 3px !important;
margin-bottom: 12px !important;
height: 1px !important;
background-color: #3d3d3d !important;
background-color: ${theme.colors.border2} !important;
`,
showModeSwitcher: css`
color: rgb(50, 179, 227) !important;
color: ${theme.colors.linkExternal} !important;
`,
};
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import {
COMMENT_NAME_LENGTH,
HIDDEN_FILTER_LABELS,
} from '../Filters.constants';
import { Filters, FilterGroup } from '../Filters.types';

export const useFilters = (): [any, boolean, any, boolean] => {
const [filters, setFilters] = useState({});
export const useFilters = (): [Filters, boolean, FilterGroup[], boolean] => {
const [filters, setFilters] = useState<Filters>({});
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const [filtersGroups, setFiltersGroups] = useState(FILTERS_GROUPS);
const [filtersGroups, setFiltersGroups] = useState<FilterGroup[]>(FILTERS_GROUPS);

const {
panelState: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@
border-radius: 2px;
}

.ant-select-dropdown,
li.ant-select-dropdown-menu-item {
background-color: #3d3d3d;
color: white;
/* QAN dropdown theme styles are now in qan.scss for all dropdowns */
/* Additional styles specific to add/manage columns dropdown */
.ant-select-dropdown.add-columns-selector-dropdown,
.ant-select-dropdown.manage-columns-selector-dropdown {
border-radius: 0;
opacity: 1;

/* Make sure badges/spans inside options inherit the text color */
.ant-select-item-option-content span {
color: var(--qan-dropdown-text) !important;
background-color: transparent !important;
}
}

/* Legacy rule for other dropdowns */
.fields__select-field .ant-select-dropdown-menu-item-active .select-item {
background-color: #cccccc;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@ export const getStyles = stylesFactory((theme: GrafanaTheme) => {
margin: 0 !important;
margin-right: 4px !important;
`,
// Action buttons at the bottom of dropdown (Remove column, Swap with main metric)
// Light theme: light gray background for readability of black text
// Dark theme: dark gray background
actionElement: css`
padding: 4px 8px;
cursor: pointer;
background-color: #3d3d3d;
background-color: ${theme.isLight ? '#e0e0e0' : '#3d3d3d'};
transition: background 0.3s ease;
&:hover {
background-color: #2d2e2f;
background-color: ${theme.isLight ? '#d0d0d0' : '#2d2e2f'};
}
`,
metricsTooltip: css`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import { getPmmTheme } from 'shared/components/helpers/getPmmTheme';
export const getStyles = stylesFactory((theme: GrafanaTheme) => {
const parameters = getPmmTheme(theme);

const selectedRowColor = theme.isLight ? 'deepskyblue' : '#234682';

return {
tableWrap: (size) => css`
display: block;
Expand All @@ -33,7 +31,7 @@ export const getStyles = stylesFactory((theme: GrafanaTheme) => {

.selected-overview-row {
.td {
background-color: ${selectedRowColor};
background-color: ${parameters.table.selectedRowColor};
}
}
.tr {
Expand Down Expand Up @@ -126,7 +124,6 @@ export const getStyles = stylesFactory((theme: GrafanaTheme) => {
sortBy: css`
display: flex;
flex-direction: column;
padding: 10px;

.sort-by:before,
.sort-by:after {
Expand All @@ -148,11 +145,11 @@ export const getStyles = stylesFactory((theme: GrafanaTheme) => {
margin-top: 1px;
}
.sort-by.asc:after {
border-top-color: deepskyblue;
border-top-color: ${parameters.table.sortIconColor};
}

.sort-by.desc:before {
border-bottom-color: deepskyblue;
border-bottom-color: ${parameters.table.sortIconColor};
}
`,
headerRow: css`
Expand Down
40 changes: 40 additions & 0 deletions pmm-app/src/pmm-qan/panel/qan.scss
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,43 @@
.ant-pagination-total-text {
margin-right: 0 !important;
}

/* Global QAN dropdown theme support - applies to all AntD Select dropdowns in QAN */
/* Dropdowns render to body, so we need global selectors with high specificity */
.ant-select-dropdown.group-by-selector-dropdown,
.ant-select-dropdown.add-columns-selector-dropdown,
.ant-select-dropdown.manage-columns-selector-dropdown {
background-color: var(--qan-dropdown-bg) !important;
color: var(--qan-dropdown-text) !important;

/* Force background on wrapper/container elements */
& > div,
.rc-virtual-list,
.rc-virtual-list-holder,
.rc-virtual-list-holder-inner {
background-color: var(--qan-dropdown-bg) !important;
}

.ant-select-item {
background-color: var(--qan-dropdown-bg) !important;
color: var(--qan-dropdown-text) !important;
}

.ant-select-item-option-active,
.ant-select-item-option-selected {
background-color: var(--qan-dropdown-hover-bg) !important;
color: var(--qan-dropdown-text) !important;
}

.ant-select-item-option-content {
color: var(--qan-dropdown-text) !important;
}
}

/* Select arrow icon color - make it visible in light theme */
.query-analytics-grid {
.ant-select-arrow,
.ant-select-suffix {
color: var(--qan-dropdown-text) !important;
}
}
Loading
Loading