Merge remote-tracking branch 'origin/main' into ds-apiserver-with-configs
This commit is contained in:
@@ -2358,10 +2358,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"],
|
||||
[0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"]
|
||||
],
|
||||
"public/app/features/datasources/components/EditDataSource.test.tsx:5381": [
|
||||
[0, 0, 0, "React Hook \\"useEffect\\" is called in function \\"component\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "0"],
|
||||
[0, 0, 0, "React Hook \\"useEffect\\" is called in function \\"component\\" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word \\"use\\".", "1"]
|
||||
],
|
||||
"public/app/features/datasources/components/picker/DataSourceCard.tsx:5381": [
|
||||
[0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]
|
||||
],
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Grouping to Matrix generates Matrix ignoring special value when value type is frame 1`] = `
|
||||
[
|
||||
{
|
||||
"config": {},
|
||||
"name": "Row\\Column",
|
||||
"type": "string",
|
||||
"values": [
|
||||
"R1",
|
||||
"R2",
|
||||
],
|
||||
},
|
||||
{
|
||||
"config": {},
|
||||
"name": "C1",
|
||||
"type": "frame",
|
||||
"values": [
|
||||
{},
|
||||
undefined,
|
||||
],
|
||||
},
|
||||
{
|
||||
"config": {},
|
||||
"name": "C2",
|
||||
"type": "frame",
|
||||
"values": [
|
||||
{},
|
||||
undefined,
|
||||
],
|
||||
},
|
||||
]
|
||||
`;
|
||||
@@ -333,4 +333,31 @@ describe('Grouping to Matrix', () => {
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('generates Matrix ignoring special value when value type is frame', async () => {
|
||||
const cfg: DataTransformerConfig<GroupingToMatrixTransformerOptions> = {
|
||||
id: DataTransformerID.groupingToMatrix,
|
||||
options: {
|
||||
columnField: 'Column',
|
||||
rowField: 'Row',
|
||||
valueField: 'Temp',
|
||||
emptyValue: SpecialValue.Zero,
|
||||
},
|
||||
};
|
||||
|
||||
const seriesA = toDataFrame({
|
||||
name: 'C',
|
||||
fields: [
|
||||
{ name: 'Column', type: FieldType.string, values: ['C1', 'C1', 'C2'] },
|
||||
{ name: 'Row', type: FieldType.string, values: ['R1', 'R2', 'R1'] },
|
||||
{ name: 'Temp', type: FieldType.frame, values: [{}, null, {}] },
|
||||
],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [seriesA])).toEmitValuesWith((received) => {
|
||||
const processed = received[0];
|
||||
|
||||
expect(processed[0].fields).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { getFieldDisplayName } from '../../field/fieldState';
|
||||
import { DataFrame, Field } from '../../types/dataFrame';
|
||||
import { DataFrame, Field, FieldType } from '../../types/dataFrame';
|
||||
import {
|
||||
SpecialValue,
|
||||
DataTransformerInfo,
|
||||
@@ -114,7 +114,10 @@ export const groupingToMatrixTransformer: DataTransformerInfo<GroupingToMatrixTr
|
||||
for (const columnName of columnValues) {
|
||||
let values = [];
|
||||
for (const rowName of rowValues) {
|
||||
const value = matrixValues[columnName][rowName] ?? getSpecialValue(emptyValue);
|
||||
// nested dataframes need to be undefined when empty
|
||||
const value =
|
||||
matrixValues[columnName][rowName] ??
|
||||
(valueField.type === FieldType.frame ? undefined : getSpecialValue(emptyValue));
|
||||
values.push(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -527,10 +527,14 @@ export interface FeatureToggles {
|
||||
*/
|
||||
grafanaManagedRecordingRules?: boolean;
|
||||
/**
|
||||
* Enables Query Library feature in Explore
|
||||
* Renamed feature toggle, enables Saved queries feature
|
||||
*/
|
||||
queryLibrary?: boolean;
|
||||
/**
|
||||
* Enables Saved Queries feature
|
||||
*/
|
||||
savedQueries?: boolean;
|
||||
/**
|
||||
* Sets the logs table as default visualisation in logs explore
|
||||
*/
|
||||
logsExploreTableDefaultVisualization?: boolean;
|
||||
|
||||
@@ -25,7 +25,12 @@ export function JSONViewCell(props: TableCellProps): JSX.Element {
|
||||
value = JSON.parse(value);
|
||||
} catch {} // ignore errors
|
||||
} else {
|
||||
displayValue = JSON.stringify(value, null, ' ');
|
||||
try {
|
||||
// JSON may refer to itself, which errors on stringify
|
||||
displayValue = JSON.stringify(value, null, ' ');
|
||||
} catch {
|
||||
displayValue = undefined; // if it won't stringify, mark undefined
|
||||
}
|
||||
}
|
||||
|
||||
const links = getCellLinks(field, row) || [];
|
||||
|
||||
@@ -905,7 +905,15 @@ var (
|
||||
},
|
||||
{
|
||||
Name: "queryLibrary",
|
||||
Description: "Enables Query Library feature in Explore",
|
||||
Description: "Renamed feature toggle, enables Saved queries feature",
|
||||
Stage: FeatureStagePrivatePreview,
|
||||
Owner: grafanaSharingSquad,
|
||||
FrontendOnly: false,
|
||||
AllowSelfServe: false,
|
||||
},
|
||||
{
|
||||
Name: "savedQueries",
|
||||
Description: "Enables Saved Queries feature",
|
||||
Stage: FeatureStagePrivatePreview,
|
||||
Owner: grafanaSharingSquad,
|
||||
FrontendOnly: false,
|
||||
|
||||
@@ -118,6 +118,7 @@ cloudWatchNewLabelParsing,GA,@grafana/aws-datasources,false,false,false
|
||||
disableNumericMetricsSortingInExpressions,experimental,@grafana/oss-big-tent,false,true,false
|
||||
grafanaManagedRecordingRules,experimental,@grafana/alerting-squad,false,false,false
|
||||
queryLibrary,privatePreview,@grafana/sharing-squad,false,false,false
|
||||
savedQueries,privatePreview,@grafana/sharing-squad,false,false,false
|
||||
logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true
|
||||
newDashboardSharingComponent,GA,@grafana/sharing-squad,false,false,true
|
||||
alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true
|
||||
|
||||
|
@@ -480,9 +480,13 @@ const (
|
||||
FlagGrafanaManagedRecordingRules = "grafanaManagedRecordingRules"
|
||||
|
||||
// FlagQueryLibrary
|
||||
// Enables Query Library feature in Explore
|
||||
// Renamed feature toggle, enables Saved queries feature
|
||||
FlagQueryLibrary = "queryLibrary"
|
||||
|
||||
// FlagSavedQueries
|
||||
// Enables Saved Queries feature
|
||||
FlagSavedQueries = "savedQueries"
|
||||
|
||||
// FlagLogsExploreTableDefaultVisualization
|
||||
// Sets the logs table as default visualisation in logs explore
|
||||
FlagLogsExploreTableDefaultVisualization = "logsExploreTableDefaultVisualization"
|
||||
|
||||
@@ -2667,12 +2667,15 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "queryLibrary",
|
||||
"resourceVersion": "1753448760331",
|
||||
"resourceVersion": "1755721444487",
|
||||
"creationTimestamp": "2022-10-07T18:31:45Z",
|
||||
"deletionTimestamp": "2023-03-20T16:00:14Z"
|
||||
"deletionTimestamp": "2023-03-20T16:00:14Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2025-08-20 20:24:04.487598 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables Query Library feature in Explore",
|
||||
"description": "Renamed feature toggle, enables Saved queries feature",
|
||||
"stage": "privatePreview",
|
||||
"codeowner": "@grafana/sharing-squad"
|
||||
}
|
||||
@@ -2849,6 +2852,18 @@
|
||||
"codeowner": "@grafana/identity-access-team"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "savedQueries",
|
||||
"resourceVersion": "1755721444487",
|
||||
"creationTimestamp": "2025-08-20T20:24:04Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables Saved Queries feature",
|
||||
"stage": "privatePreview",
|
||||
"codeowner": "@grafana/sharing-squad"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "scanRowInvalidDashboardParseFallbackEnabled",
|
||||
|
||||
@@ -711,7 +711,9 @@ func (b *backend) ListModifiedSince(ctx context.Context, key resource.Namespaced
|
||||
|
||||
if mr.Key.Name <= lastSeen {
|
||||
// resource names should be sorted alphabetically. So if not, the query is not correct.
|
||||
yield(nil, fmt.Errorf("listModifiedSince: resources are not sorted by name ASC, lastSeen: %q, current: %q", lastSeen, mr.Key.Name))
|
||||
if !yield(nil, fmt.Errorf("listModifiedSince: resources are not sorted by name ASC, lastSeen: %q, current: %q", lastSeen, mr.Key.Name)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
lastSeen = mr.Key.Name
|
||||
|
||||
@@ -226,7 +226,10 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
|
||||
},
|
||||
{
|
||||
path: '/alerting/import-datasource-managed-rules',
|
||||
roles: () => ['Admin'],
|
||||
roles: evaluateAccess([
|
||||
AccessControlAction.AlertingRuleCreate,
|
||||
AccessControlAction.AlertingProvisioningSetStatus,
|
||||
]),
|
||||
component: config.featureToggles.alertingMigrationUI
|
||||
? importAlertingComponent(
|
||||
() =>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { GrafanaTheme2, urlUtil } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Badge, LinkButton, LoadingPlaceholder, Pagination, Spinner, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||
|
||||
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants';
|
||||
@@ -14,7 +16,6 @@ import { usePagination } from '../../hooks/usePagination';
|
||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||
import { getPaginationStyles } from '../../styles/pagination';
|
||||
import { getRulesDataSources, getRulesSourceUid } from '../../utils/datasource';
|
||||
import { isAdmin } from '../../utils/misc';
|
||||
import { isAsyncRequestStatePending } from '../../utils/redux';
|
||||
import { createRelativeUrl } from '../../utils/url';
|
||||
|
||||
@@ -49,7 +50,11 @@ export const CloudRules = ({ namespaces, expandAll }: Props) => {
|
||||
DEFAULT_PER_PAGE_PAGINATION
|
||||
);
|
||||
|
||||
const canMigrateToGMA = hasDataSourcesConfigured && isAdmin() && config.featureToggles.alertingMigrationUI;
|
||||
const canMigrateToGMA =
|
||||
hasDataSourcesConfigured &&
|
||||
config.featureToggles.alertingMigrationUI &&
|
||||
contextSrv.hasPermission(AccessControlAction.AlertingRuleCreate) &&
|
||||
contextSrv.hasPermission(AccessControlAction.AlertingProvisioningSetStatus);
|
||||
|
||||
return (
|
||||
<section className={styles.wrapper}>
|
||||
|
||||
@@ -255,9 +255,12 @@ describe('RuleListActions', () => {
|
||||
describe('Import Alert Rules', () => {
|
||||
testWithFeatureToggles(['alertingMigrationUI']);
|
||||
|
||||
it('should show "Import alert rules" option when user is admin and feature toggle is enabled', async () => {
|
||||
grantUserRole(OrgRole.Admin);
|
||||
grantUserPermissions([AccessControlAction.AlertingRuleRead]);
|
||||
it('should show "Import alert rules" option when user has required permissions and feature toggle is enabled', async () => {
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingRuleRead,
|
||||
AccessControlAction.AlertingRuleCreate,
|
||||
AccessControlAction.AlertingProvisioningSetStatus,
|
||||
]);
|
||||
|
||||
const { user } = render(<RuleListActions />);
|
||||
|
||||
@@ -267,8 +270,8 @@ describe('RuleListActions', () => {
|
||||
expect(ui.menuOptions.importAlertRules.query(menu)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show "Import alert rules" option when user is not admin', async () => {
|
||||
// Keep default Viewer role
|
||||
it('should not show "Import alert rules" option when user lacks required permissions', async () => {
|
||||
// Keep default Viewer role and only read permissions
|
||||
grantUserPermissions([AccessControlAction.AlertingRuleRead]);
|
||||
|
||||
const { user } = render(<RuleListActions />);
|
||||
@@ -280,8 +283,11 @@ describe('RuleListActions', () => {
|
||||
});
|
||||
|
||||
it('should have correct URL for "Import alert rules" menu item', async () => {
|
||||
grantUserRole(OrgRole.Admin);
|
||||
grantUserPermissions([AccessControlAction.AlertingRuleRead]);
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingRuleRead,
|
||||
AccessControlAction.AlertingRuleCreate,
|
||||
AccessControlAction.AlertingProvisioningSetStatus,
|
||||
]);
|
||||
|
||||
const { user } = render(<RuleListActions />);
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useToggle } from 'react-use';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Button, Dropdown, Icon, LinkButton, Menu, Stack } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
import { AlertingPageWrapper } from '../components/AlertingPageWrapper';
|
||||
import { GrafanaRulesExporter } from '../components/export/GrafanaRulesExporter';
|
||||
@@ -11,7 +13,6 @@ import { useListViewMode } from '../components/rules/Filter/RulesViewModeSelecto
|
||||
import { AIAlertRuleButtonComponent } from '../enterprise-components/AI/AIGenAlertRuleButton/addAIAlertRuleButton';
|
||||
import { AlertingAction, useAlertingAbility } from '../hooks/useAbilities';
|
||||
import { useRulesFilter } from '../hooks/useFilteredRules';
|
||||
import { isAdmin } from '../utils/misc';
|
||||
|
||||
import { FilterView } from './FilterView';
|
||||
import { GroupedView } from './GroupedView';
|
||||
@@ -44,7 +45,11 @@ export function RuleListActions() {
|
||||
const canExportRules = exportRulesSupported && exportRulesAllowed;
|
||||
|
||||
const canCreateRules = canCreateGrafanaRules || canCreateCloudRules;
|
||||
const canImportRulesToGMA = isAdmin() && config.featureToggles.alertingMigrationUI;
|
||||
// Align import UI permission with convert endpoint requirements: rule create + provisioning set status
|
||||
const canImportRulesToGMA =
|
||||
config.featureToggles.alertingMigrationUI &&
|
||||
contextSrv.hasPermission(AccessControlAction.AlertingRuleCreate) &&
|
||||
contextSrv.hasPermission(AccessControlAction.AlertingProvisioningSetStatus);
|
||||
|
||||
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
|
||||
|
||||
|
||||
@@ -326,7 +326,7 @@ describe('<EditDataSource>', () => {
|
||||
|
||||
it('should pass a context prop to the rendered UI extension component', () => {
|
||||
const message = "I'm a UI extension component!";
|
||||
const component = jest.fn().mockReturnValue(<div>{message}</div>);
|
||||
const Component = jest.fn().mockReturnValue(<div>{message}</div>);
|
||||
|
||||
setPluginComponentsHook(
|
||||
jest.fn().mockReturnValue({
|
||||
@@ -337,7 +337,7 @@ describe('<EditDataSource>', () => {
|
||||
pluginId: 'grafana-pdc-app',
|
||||
title: 'Example component',
|
||||
description: 'Example description',
|
||||
component,
|
||||
component: Component,
|
||||
},
|
||||
'1'
|
||||
),
|
||||
@@ -353,9 +353,9 @@ describe('<EditDataSource>', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(component).toHaveBeenCalled();
|
||||
expect(Component).toHaveBeenCalled();
|
||||
|
||||
const props = component.mock.calls[0][0];
|
||||
const props = Component.mock.calls[0][0];
|
||||
|
||||
expect(props.context).toBeDefined();
|
||||
expect(props.context.dataSource).toBeDefined();
|
||||
@@ -368,7 +368,7 @@ describe('<EditDataSource>', () => {
|
||||
|
||||
it('should be possible to update the `jsonData` first and `secureJsonData` directly afterwards from the extension component', () => {
|
||||
const message = "I'm a UI extension component!";
|
||||
const component = ({ context }: { context: PluginExtensionDataSourceConfigContext }) => {
|
||||
const Component = ({ context }: { context: PluginExtensionDataSourceConfigContext }) => {
|
||||
useEffect(() => {
|
||||
context.setJsonData({ test: 'test' } as unknown as DataSourceJsonData);
|
||||
context.setSecureJsonData({ test: 'test' });
|
||||
@@ -387,7 +387,7 @@ describe('<EditDataSource>', () => {
|
||||
pluginId: 'grafana-pdc-app',
|
||||
title: 'Example component',
|
||||
description: 'Example description',
|
||||
component: component as unknown as React.ComponentType<{}>,
|
||||
component: Component as unknown as React.ComponentType<{}>,
|
||||
},
|
||||
'1'
|
||||
),
|
||||
@@ -413,7 +413,7 @@ describe('<EditDataSource>', () => {
|
||||
|
||||
it('should be possible to update the `secureJsonData` first and `jsonData` directly afterwards from the extension component', () => {
|
||||
const message = "I'm a UI extension component!";
|
||||
const component = ({ context }: { context: PluginExtensionDataSourceConfigContext }) => {
|
||||
const Component = ({ context }: { context: PluginExtensionDataSourceConfigContext }) => {
|
||||
useEffect(() => {
|
||||
context.setSecureJsonData({ test: 'test' });
|
||||
context.setJsonData({ test: 'test' } as unknown as DataSourceJsonData);
|
||||
@@ -432,7 +432,7 @@ describe('<EditDataSource>', () => {
|
||||
pluginId: 'grafana-pdc-app',
|
||||
title: 'Example component',
|
||||
description: 'Example description',
|
||||
component: component as unknown as React.ComponentType<{}>,
|
||||
component: Component as unknown as React.ComponentType<{}>,
|
||||
},
|
||||
'1'
|
||||
),
|
||||
|
||||
@@ -129,6 +129,11 @@ export enum AccessControlAction {
|
||||
AlertingProvisioningReadSecrets = 'alert.provisioning.secrets:read',
|
||||
AlertingProvisioningRead = 'alert.provisioning:read',
|
||||
AlertingProvisioningWrite = 'alert.provisioning:write',
|
||||
AlertingRulesProvisioningRead = 'alert.rules.provisioning:read',
|
||||
AlertingRulesProvisioningWrite = 'alert.rules.provisioning:write',
|
||||
AlertingNotificationsProvisioningRead = 'alert.notifications.provisioning:read',
|
||||
AlertingNotificationsProvisioningWrite = 'alert.notifications.provisioning:write',
|
||||
AlertingProvisioningSetStatus = 'alert.provisioning.provenance:write',
|
||||
|
||||
// Alerting receivers actions
|
||||
AlertingReceiversPermissionsRead = 'receivers.permissions:read',
|
||||
|
||||
Reference in New Issue
Block a user