Merge branch 'main' into evictorero/saved-queries-rbac-support
This commit is contained in:
@@ -2,18 +2,16 @@ import { Locator } from '@playwright/test';
|
||||
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import { setVisualization } from './vizpicker-utils';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
canvasPanelPanZoom: true,
|
||||
},
|
||||
});
|
||||
test.describe('Canvas Panel - Scene Tests', () => {
|
||||
test.beforeEach(async ({ page, gotoDashboardPage, selectors }) => {
|
||||
test.beforeEach(async ({ page, gotoDashboardPage }) => {
|
||||
const dashboardPage = await gotoDashboardPage({});
|
||||
const panelEditPage = await dashboardPage.addPanel();
|
||||
await setVisualization(panelEditPage, 'Canvas', selectors);
|
||||
await panelEditPage.setVisualization('Canvas');
|
||||
|
||||
// Wait for canvas panel to load
|
||||
await page.waitForSelector('[data-testid="canvas-scene-pan-zoom"]', { timeout: 10000 });
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { expect, E2ESelectorGroups, PanelEditPage } from '@grafana/plugin-e2e';
|
||||
|
||||
// this replaces the panelEditPage.setVisualization method used previously in tests, since it
|
||||
// does not know how to use the updated 12.4 viz picker UI to set the visualization
|
||||
export const setVisualization = async (panelEditPage: PanelEditPage, vizName: string, selectors: E2ESelectorGroups) => {
|
||||
const vizPicker = panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker);
|
||||
await expect(vizPicker, '"Change" button should be visible').toBeVisible();
|
||||
await vizPicker.click();
|
||||
|
||||
const allVizTabBtn = panelEditPage.getByGrafanaSelector(selectors.components.Tab.title('All visualizations'));
|
||||
await expect(allVizTabBtn, '"All visualiations" button should be visible').toBeVisible();
|
||||
await allVizTabBtn.click();
|
||||
|
||||
const vizItem = panelEditPage.getByGrafanaSelector(selectors.components.PluginVisualization.item(vizName));
|
||||
await expect(vizItem, `"${vizName}" item should be visible`).toBeVisible();
|
||||
await vizItem.scrollIntoViewIfNeeded();
|
||||
await vizItem.click();
|
||||
|
||||
await expect(vizPicker, '"Change" button should be visible again').toBeVisible();
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header),
|
||||
'Panel header should have the new viz type name'
|
||||
).toHaveText(vizName);
|
||||
};
|
||||
+4
-5
@@ -1,6 +1,5 @@
|
||||
import { expect, test } from '@grafana/plugin-e2e';
|
||||
|
||||
import { setVisualization } from '../../../panels-suite/vizpicker-utils';
|
||||
import { formatExpectError } from '../errors';
|
||||
import { successfulDataQuery } from '../mocks/queries';
|
||||
|
||||
@@ -25,10 +24,10 @@ test.describe(
|
||||
).toContainText(['Field', 'Max', 'Mean', 'Last']);
|
||||
});
|
||||
|
||||
test('table panel data assertions', async ({ panelEditPage, selectors }) => {
|
||||
test('table panel data assertions', async ({ panelEditPage }) => {
|
||||
await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200);
|
||||
await panelEditPage.datasource.set('gdev-testdata');
|
||||
await setVisualization(panelEditPage, 'Table', selectors);
|
||||
await panelEditPage.setVisualization('Table');
|
||||
await panelEditPage.refreshPanel();
|
||||
await expect(
|
||||
panelEditPage.panel.locator,
|
||||
@@ -44,10 +43,10 @@ test.describe(
|
||||
).toContainText(['val1', 'val2', 'val3', 'val4']);
|
||||
});
|
||||
|
||||
test('timeseries panel - table view assertions', async ({ panelEditPage, selectors }) => {
|
||||
test('timeseries panel - table view assertions', async ({ panelEditPage }) => {
|
||||
await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200);
|
||||
await panelEditPage.datasource.set('gdev-testdata');
|
||||
await setVisualization(panelEditPage, 'Time series', selectors);
|
||||
await panelEditPage.setVisualization('Time series');
|
||||
await panelEditPage.refreshPanel();
|
||||
await panelEditPage.toggleTableView();
|
||||
await expect(
|
||||
|
||||
+25
-26
@@ -1,6 +1,5 @@
|
||||
import { expect, test } from '@grafana/plugin-e2e';
|
||||
|
||||
import { setVisualization } from '../../../panels-suite/vizpicker-utils';
|
||||
import { formatExpectError } from '../errors';
|
||||
import { successfulDataQuery } from '../mocks/queries';
|
||||
import { scenarios } from '../mocks/resources';
|
||||
@@ -54,10 +53,10 @@ test.describe(
|
||||
).toHaveText(scenarios.map((s) => s.name));
|
||||
});
|
||||
|
||||
test('mocked query data response', async ({ panelEditPage, page, selectors }) => {
|
||||
test('mocked query data response', async ({ panelEditPage, page }) => {
|
||||
await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200);
|
||||
await panelEditPage.datasource.set('gdev-testdata');
|
||||
await setVisualization(panelEditPage, TABLE_VIZ_NAME, selectors);
|
||||
await panelEditPage.setVisualization(TABLE_VIZ_NAME);
|
||||
await panelEditPage.refreshPanel();
|
||||
await expect(
|
||||
panelEditPage.panel.getErrorIcon(),
|
||||
@@ -76,7 +75,7 @@ test.describe(
|
||||
selectors,
|
||||
page,
|
||||
}) => {
|
||||
await setVisualization(panelEditPage, TABLE_VIZ_NAME, selectors);
|
||||
await panelEditPage.setVisualization(TABLE_VIZ_NAME);
|
||||
await expect(
|
||||
panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header),
|
||||
formatExpectError('Expected panel visualization to be set to table')
|
||||
@@ -93,8 +92,8 @@ test.describe(
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('Select time zone in timezone picker', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('Select time zone in timezone picker', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const axisOptions = await panelEditPage.getCustomOptions('Axis');
|
||||
const timeZonePicker = axisOptions.getSelect('Time zone');
|
||||
|
||||
@@ -102,8 +101,8 @@ test.describe(
|
||||
await expect(timeZonePicker).toHaveSelected('Europe/Stockholm');
|
||||
});
|
||||
|
||||
test('select unit in unit picker', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('select unit in unit picker', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const standardOptions = panelEditPage.getStandardOptions();
|
||||
const unitPicker = standardOptions.getUnitPicker('Unit');
|
||||
|
||||
@@ -112,8 +111,8 @@ test.describe(
|
||||
await expect(unitPicker).toHaveSelected('Pixels');
|
||||
});
|
||||
|
||||
test('enter value in number input', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('enter value in number input', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const axisOptions = panelEditPage.getCustomOptions('Axis');
|
||||
const lineWith = axisOptions.getNumberInput('Soft min');
|
||||
|
||||
@@ -122,8 +121,8 @@ test.describe(
|
||||
await expect(lineWith).toHaveValue('10');
|
||||
});
|
||||
|
||||
test('enter value in slider', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('enter value in slider', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const graphOptions = panelEditPage.getCustomOptions('Graph styles');
|
||||
const lineWidth = graphOptions.getSliderInput('Line width');
|
||||
|
||||
@@ -132,8 +131,8 @@ test.describe(
|
||||
await expect(lineWidth).toHaveValue('10');
|
||||
});
|
||||
|
||||
test('select value in single value select', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('select value in single value select', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const standardOptions = panelEditPage.getStandardOptions();
|
||||
const colorSchemeSelect = standardOptions.getSelect('Color scheme');
|
||||
|
||||
@@ -141,8 +140,8 @@ test.describe(
|
||||
await expect(colorSchemeSelect).toHaveSelected('Classic palette');
|
||||
});
|
||||
|
||||
test('clear input', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('clear input', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const panelOptions = panelEditPage.getPanelOptions();
|
||||
const title = panelOptions.getTextInput('Title');
|
||||
|
||||
@@ -151,8 +150,8 @@ test.describe(
|
||||
await expect(title).toHaveValue('');
|
||||
});
|
||||
|
||||
test('enter value in input', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('enter value in input', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const panelOptions = panelEditPage.getPanelOptions();
|
||||
const description = panelOptions.getTextInput('Description');
|
||||
|
||||
@@ -161,8 +160,8 @@ test.describe(
|
||||
await expect(description).toHaveValue('This is a panel');
|
||||
});
|
||||
|
||||
test('unchecking switch', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('unchecking switch', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const axisOptions = panelEditPage.getCustomOptions('Axis');
|
||||
const showBorder = axisOptions.getSwitch('Show border');
|
||||
|
||||
@@ -174,8 +173,8 @@ test.describe(
|
||||
await expect(showBorder).toBeChecked({ checked: false });
|
||||
});
|
||||
|
||||
test('checking switch', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('checking switch', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const axisOptions = panelEditPage.getCustomOptions('Axis');
|
||||
const showBorder = axisOptions.getSwitch('Show border');
|
||||
|
||||
@@ -184,8 +183,8 @@ test.describe(
|
||||
await expect(showBorder).toBeChecked();
|
||||
});
|
||||
|
||||
test('re-selecting value in radio button group', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('re-selecting value in radio button group', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const axisOptions = panelEditPage.getCustomOptions('Axis');
|
||||
const placement = axisOptions.getRadioGroup('Placement');
|
||||
|
||||
@@ -196,8 +195,8 @@ test.describe(
|
||||
await expect(placement).toHaveChecked('Auto');
|
||||
});
|
||||
|
||||
test('selecting value in radio button group', async ({ panelEditPage, selectors }) => {
|
||||
await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors);
|
||||
test('selecting value in radio button group', async ({ panelEditPage }) => {
|
||||
await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME);
|
||||
const axisOptions = panelEditPage.getCustomOptions('Axis');
|
||||
const placement = axisOptions.getRadioGroup('Placement');
|
||||
|
||||
|
||||
@@ -421,6 +421,10 @@ export interface FeatureToggles {
|
||||
*/
|
||||
jitterAlertRulesWithinGroups?: boolean;
|
||||
/**
|
||||
* Enable audit logging with Kubernetes under app platform
|
||||
*/
|
||||
auditLoggingAppPlatform?: boolean;
|
||||
/**
|
||||
* Enable the secrets management API and services under app platform
|
||||
*/
|
||||
secretsManagementAppPlatform?: boolean;
|
||||
|
||||
+6
-2
@@ -48,7 +48,7 @@ describe('MetricsModal', () => {
|
||||
operations: [],
|
||||
};
|
||||
|
||||
setup(query, ['with-labels'], true);
|
||||
setup(query, ['with-labels']);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('with-labels')).toBeInTheDocument();
|
||||
});
|
||||
@@ -220,6 +220,10 @@ function createDatasource(withLabels?: boolean) {
|
||||
// display different results if their labels are selected in the PromVisualQuery
|
||||
if (withLabels) {
|
||||
languageProvider.queryMetricsMetadata = jest.fn().mockResolvedValue({
|
||||
ALERTS: {
|
||||
type: 'gauge',
|
||||
help: 'alerts help text',
|
||||
},
|
||||
'with-labels': {
|
||||
type: 'with-labels-type',
|
||||
help: 'with-labels-help',
|
||||
@@ -297,7 +301,7 @@ function createProps(query: PromVisualQuery, datasource: PrometheusDatasource, m
|
||||
};
|
||||
}
|
||||
|
||||
function setup(query: PromVisualQuery, metrics: string[], withlabels?: boolean) {
|
||||
function setup(query: PromVisualQuery, metrics: string[]) {
|
||||
const withLabels: boolean = query.labels.length > 0;
|
||||
const datasource = createDatasource(withLabels);
|
||||
const props = createProps(query, datasource, metrics);
|
||||
|
||||
+1
-1
@@ -138,7 +138,7 @@ const MetricsModalContent = (props: MetricsModalProps) => {
|
||||
|
||||
export const MetricsModal = (props: MetricsModalProps) => {
|
||||
return (
|
||||
<MetricsModalContextProvider languageProvider={props.datasource.languageProvider}>
|
||||
<MetricsModalContextProvider languageProvider={props.datasource.languageProvider} timeRange={props.timeRange}>
|
||||
<MetricsModalContent {...props} />
|
||||
</MetricsModalContextProvider>
|
||||
);
|
||||
|
||||
+20
-4
@@ -4,6 +4,7 @@ import { ReactNode } from 'react';
|
||||
import { TimeRange } from '@grafana/data';
|
||||
|
||||
import { PrometheusLanguageProviderInterface } from '../../../language_provider';
|
||||
import { getMockTimeRange } from '../../../test/mocks/datasource';
|
||||
|
||||
import { DEFAULT_RESULTS_PER_PAGE, MetricsModalContextProvider, useMetricsModal } from './MetricsModalContext';
|
||||
import { generateMetricData } from './helpers';
|
||||
@@ -25,7 +26,9 @@ const mockLanguageProvider: PrometheusLanguageProviderInterface = {
|
||||
// Helper to create wrapper component
|
||||
const createWrapper = (languageProvider = mockLanguageProvider) => {
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<MetricsModalContextProvider languageProvider={languageProvider}>{children}</MetricsModalContextProvider>
|
||||
<MetricsModalContextProvider languageProvider={languageProvider} timeRange={getMockTimeRange()}>
|
||||
{children}
|
||||
</MetricsModalContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -167,6 +170,7 @@ describe('MetricsModalContext', () => {
|
||||
|
||||
it('should handle empty metadata response', async () => {
|
||||
(mockLanguageProvider.queryMetricsMetadata as jest.Mock).mockResolvedValue({});
|
||||
(mockLanguageProvider.queryLabelValues as jest.Mock).mockResolvedValue(['metric1', 'metric2']);
|
||||
|
||||
const { result } = renderHook(() => useMetricsModal(), {
|
||||
wrapper: createWrapper(),
|
||||
@@ -176,7 +180,18 @@ describe('MetricsModalContext', () => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.filteredMetricsData).toEqual([]);
|
||||
expect(result.current.filteredMetricsData).toEqual([
|
||||
{
|
||||
value: 'metric1',
|
||||
type: 'counter',
|
||||
description: 'Test metric',
|
||||
},
|
||||
{
|
||||
value: 'metric2',
|
||||
type: 'counter',
|
||||
description: 'Test metric',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle metadata fetch error', async () => {
|
||||
@@ -239,6 +254,7 @@ describe('MetricsModalContext', () => {
|
||||
}));
|
||||
|
||||
(mockLanguageProvider.queryMetricsMetadata as jest.Mock).mockResolvedValue({
|
||||
ALERTS: { type: 'gauge', help: 'Test alerts help' },
|
||||
test_metric: { type: 'counter', help: 'Test metric' },
|
||||
});
|
||||
|
||||
@@ -250,7 +266,7 @@ describe('MetricsModalContext', () => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.filteredMetricsData).toHaveLength(1);
|
||||
expect(result.current.filteredMetricsData).toHaveLength(2);
|
||||
expect(result.current.selectedTypes).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -318,7 +334,7 @@ describe('MetricsModalContext', () => {
|
||||
};
|
||||
|
||||
const { getByTestId } = render(
|
||||
<MetricsModalContextProvider languageProvider={mockLanguageProvider}>
|
||||
<MetricsModalContextProvider languageProvider={mockLanguageProvider} timeRange={getMockTimeRange()}>
|
||||
<TestComponent />
|
||||
</MetricsModalContextProvider>
|
||||
);
|
||||
|
||||
+13
-3
@@ -52,11 +52,13 @@ const MetricsModalContext = createContext<MetricsModalContextValue | undefined>(
|
||||
|
||||
type MetricsModalContextProviderProps = {
|
||||
languageProvider: PrometheusLanguageProviderInterface;
|
||||
timeRange: TimeRange;
|
||||
};
|
||||
|
||||
export const MetricsModalContextProvider: FC<PropsWithChildren<MetricsModalContextProviderProps>> = ({
|
||||
children,
|
||||
languageProvider,
|
||||
timeRange,
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [metricsData, setMetricsData] = useState<MetricsData>([]);
|
||||
@@ -111,8 +113,16 @@ export const MetricsModalContextProvider: FC<PropsWithChildren<MetricsModalConte
|
||||
setIsLoading(true);
|
||||
const metadata = await languageProvider.queryMetricsMetadata(PROMETHEUS_QUERY_BUILDER_MAX_RESULTS);
|
||||
|
||||
if (Object.keys(metadata).length === 0) {
|
||||
setMetricsData([]);
|
||||
// We receive ALERTS metadata in any case
|
||||
if (Object.keys(metadata).length <= 1) {
|
||||
const fetchedMetrics = await languageProvider.queryLabelValues(
|
||||
timeRange,
|
||||
METRIC_LABEL,
|
||||
undefined,
|
||||
PROMETHEUS_QUERY_BUILDER_MAX_RESULTS
|
||||
);
|
||||
const processedData = fetchedMetrics.map((m) => generateMetricData(m, languageProvider));
|
||||
setMetricsData(processedData);
|
||||
} else {
|
||||
const processedData = Object.keys(metadata).map((m) => generateMetricData(m, languageProvider));
|
||||
setMetricsData(processedData);
|
||||
@@ -122,7 +132,7 @@ export const MetricsModalContextProvider: FC<PropsWithChildren<MetricsModalConte
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [languageProvider]);
|
||||
}, [languageProvider, timeRange]);
|
||||
|
||||
const debouncedBackendSearch = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -20,9 +20,10 @@ const (
|
||||
|
||||
// Typed errors
|
||||
var (
|
||||
ErrUserTokenNotFound = errors.New("user token not found")
|
||||
ErrInvalidSessionToken = usertoken.ErrInvalidSessionToken
|
||||
ErrExternalSessionNotFound = errors.New("external session not found")
|
||||
ErrUserTokenNotFound = errors.New("user token not found")
|
||||
ErrInvalidSessionToken = usertoken.ErrInvalidSessionToken
|
||||
ErrExternalSessionNotFound = errors.New("external session not found")
|
||||
ErrExternalSessionTokenNotFound = errors.New("session token was nil")
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
@@ -688,6 +688,14 @@ var (
|
||||
HideFromDocs: true,
|
||||
RequiresRestart: true,
|
||||
},
|
||||
{
|
||||
Name: "auditLoggingAppPlatform",
|
||||
Description: "Enable audit logging with Kubernetes under app platform",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaOperatorExperienceSquad,
|
||||
HideFromDocs: true,
|
||||
RequiresRestart: true,
|
||||
},
|
||||
{
|
||||
Name: "secretsManagementAppPlatform",
|
||||
Description: "Enable the secrets management API and services under app platform",
|
||||
|
||||
Generated
+1
@@ -95,6 +95,7 @@ kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad
|
||||
cloudRBACRoles,preview,@grafana/identity-access-team,false,true,false
|
||||
alertingQueryOptimization,GA,@grafana/alerting-squad,false,false,false
|
||||
jitterAlertRulesWithinGroups,preview,@grafana/alerting-squad,false,true,false
|
||||
auditLoggingAppPlatform,experimental,@grafana/grafana-operator-experience-squad,false,true,false
|
||||
secretsManagementAppPlatform,experimental,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
secretsManagementAppPlatformUI,experimental,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,false,false,false
|
||||
|
||||
|
Generated
+4
@@ -279,6 +279,10 @@ const (
|
||||
// Distributes alert rule evaluations more evenly over time, including spreading out rules within the same group. Disables sequential evaluation if enabled.
|
||||
FlagJitterAlertRulesWithinGroups = "jitterAlertRulesWithinGroups"
|
||||
|
||||
// FlagAuditLoggingAppPlatform
|
||||
// Enable audit logging with Kubernetes under app platform
|
||||
FlagAuditLoggingAppPlatform = "auditLoggingAppPlatform"
|
||||
|
||||
// FlagSecretsManagementAppPlatform
|
||||
// Enable the secrets management API and services under app platform
|
||||
FlagSecretsManagementAppPlatform = "secretsManagementAppPlatform"
|
||||
|
||||
+14
@@ -658,6 +658,20 @@
|
||||
"frontend": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "auditLoggingAppPlatform",
|
||||
"resourceVersion": "1767013056996",
|
||||
"creationTimestamp": "2025-12-29T12:57:36Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enable audit logging with Kubernetes under app platform",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/grafana-operator-experience-squad",
|
||||
"requiresRestart": true,
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "authZGRPCServer",
|
||||
|
||||
@@ -660,6 +660,10 @@ func (o *Service) getExternalSession(ctx context.Context, usr identity.Requester
|
||||
return externalSessions[0], nil
|
||||
}
|
||||
|
||||
if sessionToken == nil {
|
||||
return nil, auth.ErrExternalSessionTokenNotFound
|
||||
}
|
||||
|
||||
// For regular users, we use the session token ID to fetch the external session
|
||||
return o.sessionService.GetExternalSession(ctx, sessionToken.ExternalSessionId)
|
||||
}
|
||||
|
||||
+72
@@ -60,4 +60,76 @@ describe('LogRecordViewerByTimestamp', () => {
|
||||
expect(within(errorRows[1]).getByText(/Error message:/)).toBeInTheDocument();
|
||||
expect(within(errorRows[1]).getByText(/explicit message/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Numeric Value Formatting', () => {
|
||||
it('should format numeric values correctly in AlertInstanceValues', () => {
|
||||
const records: LogRecord[] = [
|
||||
{
|
||||
timestamp: 1681739580000,
|
||||
line: {
|
||||
current: 'Alerting',
|
||||
previous: 'Pending',
|
||||
labels: {},
|
||||
values: {
|
||||
cpu_usage: 42.987654321,
|
||||
memory_mb: 1234567.89,
|
||||
disk_io: 0.001234,
|
||||
request_count: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
render(<LogRecordViewerByTimestamp records={records} commonLabels={[]} />);
|
||||
|
||||
expect(screen.getByText(/cpu_usage/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/4\.299e\+1/i)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/memory_mb/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/1\.235e\+6/i)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/disk_io/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/1\.234e-3/i)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/request_count/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/10000/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should format various numeric ranges correctly', () => {
|
||||
const records: LogRecord[] = [
|
||||
{
|
||||
timestamp: 1681739580000,
|
||||
line: {
|
||||
current: 'Alerting',
|
||||
previous: 'Pending',
|
||||
labels: {},
|
||||
values: {
|
||||
small: 0.001,
|
||||
normal: 42.5,
|
||||
large: 123456,
|
||||
boundary_low: 0.01,
|
||||
boundary_high: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
render(<LogRecordViewerByTimestamp records={records} commonLabels={[]} />);
|
||||
|
||||
expect(screen.getByText(/small/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/1\.000e-3/i)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/normal/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/42\.5/)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/large/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/1\.235e\+5/i)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/boundary_low/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/0\.01/)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/boundary_high/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/10000/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+2
-1
@@ -13,6 +13,7 @@ import { AlertStateTag } from '../AlertStateTag';
|
||||
|
||||
import { ErrorMessageRow } from './ErrorMessageRow';
|
||||
import { LogRecord, omitLabels } from './common';
|
||||
import { formatNumericValue } from './numberFormatter';
|
||||
|
||||
type LogRecordViewerProps = {
|
||||
records: LogRecord[];
|
||||
@@ -182,7 +183,7 @@ const AlertInstanceValues = memo(({ record }: { record: Record<string, number> }
|
||||
return (
|
||||
<>
|
||||
{values.map(([key, value]) => (
|
||||
<AlertLabel key={key} labelKey={key} value={String(value)} />
|
||||
<AlertLabel key={key} labelKey={key} value={formatNumericValue(value)} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import { formatNumericValue } from './numberFormatter';
|
||||
|
||||
describe('formatNumericValue', () => {
|
||||
describe('Zero and special values', () => {
|
||||
it('should format zero correctly', () => {
|
||||
expect(formatNumericValue(0)).toBe('0');
|
||||
expect(formatNumericValue(-0)).toBe('0');
|
||||
});
|
||||
|
||||
it('should handle NaN', () => {
|
||||
expect(formatNumericValue(NaN)).toBe('NaN');
|
||||
});
|
||||
|
||||
it('should handle Infinity', () => {
|
||||
expect(formatNumericValue(Infinity)).toBe('Infinity');
|
||||
expect(formatNumericValue(-Infinity)).toBe('-Infinity');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Very small numbers (scientific notation)', () => {
|
||||
it('should use scientific notation for values less than 1e-2', () => {
|
||||
const result1 = formatNumericValue(1e-3);
|
||||
expect(result1).toMatch(/^1\.000e-3$/i);
|
||||
|
||||
const result2 = formatNumericValue(0.001);
|
||||
expect(result2).toMatch(/^1\.000e-3$/i);
|
||||
|
||||
const result3 = formatNumericValue(0.009);
|
||||
expect(result3).toMatch(/^9\.000e-3$/i);
|
||||
});
|
||||
|
||||
it('should use scientific notation for values just below 1e-2', () => {
|
||||
const result = formatNumericValue(0.00999);
|
||||
expect(result).toMatch(/^9\.990e-3$/i);
|
||||
});
|
||||
|
||||
it('should format the example from requirements correctly', () => {
|
||||
// 1.4153928131348452 has > 4 decimal places, so should use scientific notation
|
||||
const result = formatNumericValue(1.4153928131348452);
|
||||
expect(result).toMatch(/^1\.415e\+0$/i);
|
||||
});
|
||||
|
||||
it('should handle negative very small numbers', () => {
|
||||
const result = formatNumericValue(-1e-3);
|
||||
expect(result).toMatch(/^-1\.000e-3$/i);
|
||||
|
||||
const result2 = formatNumericValue(-0.001);
|
||||
expect(result2).toMatch(/^-1\.000e-3$/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Human-readable range (standard notation)', () => {
|
||||
it('should use standard notation for boundary value 1e-2', () => {
|
||||
expect(formatNumericValue(0.01)).toBe('0.01');
|
||||
});
|
||||
|
||||
it('should use standard notation for values in readable range', () => {
|
||||
expect(formatNumericValue(0.1)).toBe('0.1');
|
||||
expect(formatNumericValue(1)).toBe('1');
|
||||
expect(formatNumericValue(1.234)).toBe('1.234');
|
||||
expect(formatNumericValue(42.5)).toBe('42.5');
|
||||
});
|
||||
|
||||
it('should limit to 4 decimal places without rounding integer parts', () => {
|
||||
expect(formatNumericValue(123.456)).toBe('123.456');
|
||||
expect(formatNumericValue(1234.567)).toBe('1234.567');
|
||||
expect(formatNumericValue(9999.9)).toBe('9999.9');
|
||||
expect(formatNumericValue(9999.1234)).toBe('9999.1234');
|
||||
});
|
||||
|
||||
it('should use scientific notation for numbers with more than 4 decimal places', () => {
|
||||
// Numbers with > 4 decimals should use scientific notation even in readable range
|
||||
const result1 = formatNumericValue(123.456789);
|
||||
expect(result1).toMatch(/^1\.235e\+2$/i);
|
||||
|
||||
const result2 = formatNumericValue(1.23456789);
|
||||
expect(result2).toMatch(/^1\.235e\+0$/i);
|
||||
|
||||
const result3 = formatNumericValue(42.987654321);
|
||||
expect(result3).toMatch(/^4\.299e\+1$/i);
|
||||
});
|
||||
|
||||
it('should use standard notation for boundary value 1e4', () => {
|
||||
expect(formatNumericValue(10000)).toBe('10000');
|
||||
});
|
||||
|
||||
it('should handle negative numbers in readable range', () => {
|
||||
expect(formatNumericValue(-0.1)).toBe('-0.1');
|
||||
expect(formatNumericValue(-123.456)).toBe('-123.456');
|
||||
expect(formatNumericValue(-9999.9)).toBe('-9999.9');
|
||||
});
|
||||
|
||||
it('should use scientific notation for negative numbers with excessive precision', () => {
|
||||
const result = formatNumericValue(-42.987654321);
|
||||
expect(result).toMatch(/^-4\.299e\+1$/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Very large numbers (scientific notation)', () => {
|
||||
it('should use scientific notation for values greater than 1e4', () => {
|
||||
const result1 = formatNumericValue(10001);
|
||||
expect(result1).toMatch(/^1\.000e\+4$/i);
|
||||
|
||||
const result2 = formatNumericValue(123456);
|
||||
expect(result2).toMatch(/^1\.235e\+5$/i);
|
||||
});
|
||||
|
||||
it('should handle negative very large numbers', () => {
|
||||
const result = formatNumericValue(-1e5);
|
||||
expect(result).toMatch(/^-1\.000e\+5$/i);
|
||||
|
||||
const result2 = formatNumericValue(-123456);
|
||||
expect(result2).toMatch(/^-1\.235e\+5$/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle numbers exactly at boundaries', () => {
|
||||
expect(formatNumericValue(0.01)).toBe('0.01');
|
||||
|
||||
const justBelow = formatNumericValue(0.009999);
|
||||
expect(justBelow).toMatch(/^9\.999e-3$/i);
|
||||
|
||||
expect(formatNumericValue(10000)).toBe('10000');
|
||||
|
||||
const justAbove = formatNumericValue(10001);
|
||||
expect(justAbove).toMatch(/^1\.000e\+4$/i);
|
||||
});
|
||||
|
||||
it('should use scientific notation for very precise decimals with > 4 decimal places', () => {
|
||||
expect(formatNumericValue(1.23456789)).toMatch(/^1\.235e\+0$/i);
|
||||
expect(formatNumericValue(123.456789)).toMatch(/^1\.235e\+2$/i);
|
||||
expect(formatNumericValue(0.123456789)).toMatch(/^1\.235e-1$/i);
|
||||
});
|
||||
|
||||
it('should use standard notation for numbers with exactly 4 or fewer decimal places', () => {
|
||||
expect(formatNumericValue(1.2345)).toBe('1.2345');
|
||||
expect(formatNumericValue(0.1234)).toBe('0.1234');
|
||||
expect(formatNumericValue(123.4567)).toBe('123.4567');
|
||||
});
|
||||
});
|
||||
|
||||
describe('countDecimalPlaces edge cases', () => {
|
||||
it('should handle numbers that toString() would convert to scientific notation', () => {
|
||||
const result = formatNumericValue(1e-10);
|
||||
expect(result).toMatch(/^1\.000e-10$/i);
|
||||
|
||||
const result2 = formatNumericValue(1e10);
|
||||
expect(result2).toMatch(/^1\.000e\+10$/i);
|
||||
});
|
||||
|
||||
it('should correctly count decimals for numbers with trailing zeros', () => {
|
||||
expect(formatNumericValue(1.234)).toBe('1.234');
|
||||
expect(formatNumericValue(1.2)).toBe('1.2');
|
||||
expect(formatNumericValue(1.0)).toBe('1');
|
||||
});
|
||||
|
||||
it('should handle boundary values correctly', () => {
|
||||
expect(formatNumericValue(0.01)).toBe('0.01');
|
||||
expect(formatNumericValue(10000)).toBe('10000');
|
||||
|
||||
expect(formatNumericValue(0.01001)).toMatch(/^1\.001e-2$/i);
|
||||
expect(formatNumericValue(9999.1234)).toBe('9999.1234');
|
||||
expect(formatNumericValue(9999.12345)).toMatch(/^9\.999e\+3$/i);
|
||||
});
|
||||
|
||||
it('should handle numbers in readable range that have many decimals', () => {
|
||||
expect(formatNumericValue(1.4153928131348452)).toMatch(/^1\.415e\+0$/i);
|
||||
expect(formatNumericValue(42.987654321)).toMatch(/^4\.299e\+1$/i);
|
||||
expect(formatNumericValue(123.456789)).toMatch(/^1\.235e\+2$/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
const SCIENTIFIC_NOTATION_THRESHOLD_SMALL = 1e-2;
|
||||
const SCIENTIFIC_NOTATION_THRESHOLD_LARGE = 1e4;
|
||||
const MAX_DECIMAL_PLACES = 4;
|
||||
const EXPONENTIAL_DECIMALS = 3; // 4 significant digits = 1 digit + 3 decimals
|
||||
|
||||
const readableRangeFormatter = new Intl.NumberFormat(undefined, {
|
||||
maximumFractionDigits: MAX_DECIMAL_PLACES,
|
||||
useGrouping: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Counts the number of decimal places in a number.
|
||||
* Only processes numbers in readable range (1e-2 to 1e4) to avoid
|
||||
* toString() scientific notation issues for very large/small numbers.
|
||||
*
|
||||
* Uses toFixed(10) to ensure standard notation representation.
|
||||
* 10 decimal places is sufficient to detect if a number has > 4 decimal places.
|
||||
*/
|
||||
function countDecimalPlaces(value: number): number {
|
||||
if (Number.isInteger(value)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const absValue = Math.abs(value);
|
||||
|
||||
// Only count decimals for numbers in readable range
|
||||
if (absValue < SCIENTIFIC_NOTATION_THRESHOLD_SMALL || absValue > SCIENTIFIC_NOTATION_THRESHOLD_LARGE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const str = value.toFixed(10);
|
||||
const decimalIndex = str.indexOf('.');
|
||||
|
||||
if (decimalIndex === -1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Count decimal places, removing trailing zeros
|
||||
const decimalPart = str.substring(decimalIndex + 1).replace(/0+$/, '');
|
||||
return decimalPart.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a numeric value for display in alert rule history.
|
||||
* - For values in human-readable range (1e-2 to 1e4) with ≤ 4 decimal places: shows up to 4 decimal places
|
||||
* - For very small values (< 1e-2): uses scientific notation with 4 significant digits
|
||||
* - For very large values (> 1e4): uses scientific notation with 4 significant digits
|
||||
* - For numbers with > 4 decimal places: uses scientific notation with 4 significant digits
|
||||
*
|
||||
* @param value - The number to format
|
||||
* @returns A formatted string representation of the number
|
||||
*/
|
||||
export function formatNumericValue(value: number): string {
|
||||
if (!Number.isFinite(value)) {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
if (value === 0) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
const absValue = Math.abs(value);
|
||||
|
||||
if (absValue < SCIENTIFIC_NOTATION_THRESHOLD_SMALL || absValue > SCIENTIFIC_NOTATION_THRESHOLD_LARGE) {
|
||||
return value.toExponential(EXPONENTIAL_DECIMALS);
|
||||
}
|
||||
|
||||
const decimalPlaces = countDecimalPlaces(value);
|
||||
|
||||
if (decimalPlaces > MAX_DECIMAL_PLACES) {
|
||||
return value.toExponential(EXPONENTIAL_DECIMALS);
|
||||
}
|
||||
|
||||
return readableRangeFormatter.format(value);
|
||||
}
|
||||
Reference in New Issue
Block a user