Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9becd8e634 | ||
|
|
88746cf96c | ||
|
|
e116254f32 | ||
|
|
27c464c833 | ||
|
|
e1efcae8f3 | ||
|
|
1f32a31c2b | ||
|
|
65cee8e6b4 | ||
|
|
6da8c5d1bb | ||
|
|
87f09ffa2d | ||
|
|
e0b76f7916 | ||
|
|
87b32fe05c | ||
|
|
649fabb2e6 |
Generated
+1
-1
@@ -13,7 +13,7 @@ import (
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaReceiver = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", NewReceiver(), &ReceiverList{}, resource.WithKind("Receiver"),
|
||||
resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{resource.SelectableField{
|
||||
resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{
|
||||
FieldSelector: "spec.title",
|
||||
FieldValueFunc: func(o resource.Object) (string, error) {
|
||||
cast, ok := o.(*Receiver)
|
||||
|
||||
@@ -790,8 +790,6 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -794,8 +794,6 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -301,8 +301,6 @@ var _ resource.ListObject = &DashboardList{}
|
||||
|
||||
// Copy methods for all subresource types
|
||||
|
||||
|
||||
|
||||
// DeepCopy creates a full deep copy of DashboardStatus
|
||||
func (s *DashboardStatus) DeepCopy() *DashboardStatus {
|
||||
cpy := &DashboardStatus{}
|
||||
|
||||
@@ -301,8 +301,6 @@ var _ resource.ListObject = &DashboardList{}
|
||||
|
||||
// Copy methods for all subresource types
|
||||
|
||||
|
||||
|
||||
// DeepCopy creates a full deep copy of DashboardStatus
|
||||
func (s *DashboardStatus) DeepCopy() *DashboardStatus {
|
||||
cpy := &DashboardStatus{}
|
||||
|
||||
@@ -794,8 +794,6 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -1411,8 +1411,6 @@ type DashboardVariableOption struct {
|
||||
Text DashboardStringOrArrayOfString `json:"text"`
|
||||
// Value of the option
|
||||
Value DashboardStringOrArrayOfString `json:"value"`
|
||||
// Additional properties for multi-props variables
|
||||
Properties map[string]string `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// NewDashboardVariableOption creates a new DashboardVariableOption object.
|
||||
|
||||
@@ -798,8 +798,6 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -1414,8 +1414,6 @@ type DashboardVariableOption struct {
|
||||
Text DashboardStringOrArrayOfString `json:"text"`
|
||||
// Value of the option
|
||||
Value DashboardStringOrArrayOfString `json:"value"`
|
||||
// Additional properties for multi-props variables
|
||||
Properties map[string]string `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// NewDashboardVariableOption creates a new DashboardVariableOption object.
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
@@ -18,8 +18,6 @@ import (
|
||||
v1beta1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
||||
)
|
||||
|
||||
var ()
|
||||
|
||||
var appManifestData = app.ManifestData{
|
||||
AppName: "folder",
|
||||
Group: "folder.grafana.app",
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
CustomVariableSupport,
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
QueryEditorProps,
|
||||
Field,
|
||||
DataFrame,
|
||||
MetricFindValue,
|
||||
} from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { EditorMode, EditorRows, EditorRow, EditorField } from '@grafana/plugin-ui';
|
||||
import { Combobox, ComboboxOption } from '@grafana/ui';
|
||||
|
||||
import { SqlQueryEditorLazy } from './components/QueryEditorLazy';
|
||||
import { SqlDatasource } from './datasource/SqlDatasource';
|
||||
import { applyQueryDefaults } from './defaults';
|
||||
import { QueryFormat, type SQLQuery, type SQLOptions, type SQLQueryMeta } from './types';
|
||||
|
||||
type SQLVariableQuery = { query: string } & SQLQuery;
|
||||
|
||||
const refId = 'SQLVariableQueryEditor-VariableQuery';
|
||||
|
||||
export class SQLVariableSupport extends CustomVariableSupport<SqlDatasource, SQLQuery> {
|
||||
constructor(readonly datasource: SqlDatasource) {
|
||||
super();
|
||||
}
|
||||
editor = SQLVariablesQueryEditor;
|
||||
query(request: DataQueryRequest<SQLQuery>): Observable<DataQueryResponse> {
|
||||
if (request.targets.length < 1) {
|
||||
throw new Error('no variable query found');
|
||||
}
|
||||
const updatedQuery = migrateVariableQuery(request.targets[0]);
|
||||
return this.datasource.query({ ...request, targets: [updatedQuery] }).pipe(
|
||||
map((d: DataQueryResponse) => {
|
||||
const frames = d.data || [];
|
||||
const metricFindValues = convertDataFramesToMetricFindValues(frames, updatedQuery.meta);
|
||||
return { data: metricFindValues };
|
||||
})
|
||||
);
|
||||
}
|
||||
getDefaultQuery(): Partial<SQLQuery> {
|
||||
return applyQueryDefaults({ refId, editorMode: EditorMode.Builder, format: QueryFormat.Table });
|
||||
}
|
||||
}
|
||||
|
||||
type SQLVariableQueryEditorProps = QueryEditorProps<SqlDatasource, SQLQuery, SQLOptions>;
|
||||
|
||||
const SQLVariablesQueryEditor = (props: SQLVariableQueryEditorProps) => {
|
||||
const query = migrateVariableQuery(props.query);
|
||||
return (
|
||||
<>
|
||||
<SqlQueryEditorLazy {...props} query={query} />
|
||||
<FieldMapping {...props} query={query} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FieldMapping = (props: SQLVariableQueryEditorProps) => {
|
||||
const { query, datasource, onChange } = props;
|
||||
const [choices, setChoices] = useState<ComboboxOption[]>([]);
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
// eslint-disable-next-line
|
||||
const subscription = datasource.query({ targets: [query] } as DataQueryRequest<SQLQuery>).subscribe({
|
||||
next: (response) => {
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
const fieldNames = (response.data[0] || { fields: [] }).fields.map((f: Field) => f.name);
|
||||
setChoices(fieldNames.map((f: Field) => ({ value: f, label: f })));
|
||||
},
|
||||
error: () => {
|
||||
if (isActive) {
|
||||
setChoices([]);
|
||||
}
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
isActive = false;
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [datasource, query]);
|
||||
const onMetaPropChange = <Key extends keyof SQLQueryMeta, Value extends SQLQueryMeta[Key]>(
|
||||
key: Key,
|
||||
value: Value,
|
||||
meta = query.meta || {}
|
||||
) => {
|
||||
onChange({ ...query, meta: { ...meta, [key]: value } });
|
||||
};
|
||||
return (
|
||||
<EditorRows>
|
||||
<EditorRow>
|
||||
<EditorField label={t('grafana-sql.components.query-meta.variables.valueField', 'Value Field')}>
|
||||
<Combobox
|
||||
isClearable
|
||||
value={query.meta?.valueField}
|
||||
onChange={(e) => onMetaPropChange('valueField', e?.value)}
|
||||
width={40}
|
||||
options={choices}
|
||||
/>
|
||||
</EditorField>
|
||||
<EditorField label={t('grafana-sql.components.query-meta.variables.textField', 'Text Field')}>
|
||||
<Combobox
|
||||
isClearable
|
||||
value={query.meta?.textField}
|
||||
onChange={(e) => onMetaPropChange('textField', e?.value)}
|
||||
width={40}
|
||||
options={choices}
|
||||
/>
|
||||
</EditorField>
|
||||
</EditorRow>
|
||||
</EditorRows>
|
||||
);
|
||||
};
|
||||
|
||||
const migrateVariableQuery = (rawQuery: string | SQLQuery): SQLVariableQuery => {
|
||||
if (typeof rawQuery !== 'string') {
|
||||
return {
|
||||
...rawQuery,
|
||||
refId: rawQuery.refId || refId,
|
||||
query: rawQuery.rawSql || '',
|
||||
};
|
||||
}
|
||||
return {
|
||||
...applyQueryDefaults({
|
||||
refId,
|
||||
rawSql: rawQuery,
|
||||
editorMode: rawQuery ? EditorMode.Code : EditorMode.Builder,
|
||||
}),
|
||||
query: rawQuery,
|
||||
};
|
||||
};
|
||||
|
||||
const convertDataFramesToMetricFindValues = (frames: DataFrame[], meta?: SQLQueryMeta): MetricFindValue[] => {
|
||||
if (!frames.length) {
|
||||
throw new Error('no results found');
|
||||
}
|
||||
|
||||
const frame = frames[0];
|
||||
|
||||
const fields = frame.fields;
|
||||
|
||||
if (fields.length < 1) {
|
||||
throw new Error('no fields found in the response');
|
||||
}
|
||||
|
||||
let textField = fields.find((f) => f.name === '__text');
|
||||
let valueField = fields.find((f) => f.name === '__value');
|
||||
if (meta?.textField) {
|
||||
textField = fields.find((f) => f.name === meta.textField);
|
||||
}
|
||||
if (meta?.valueField) {
|
||||
valueField = fields.find((f) => f.name === meta.valueField);
|
||||
}
|
||||
const resolvedTextField = textField || valueField || fields[0];
|
||||
const resolvedValueField = valueField || textField || fields[0];
|
||||
|
||||
const results: MetricFindValue[] = [];
|
||||
const rowCount = frame.length;
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const text = String(resolvedTextField.values[i] ?? '');
|
||||
const value = String(resolvedValueField.values[i] ?? '');
|
||||
const properties: Record<string, string> = {};
|
||||
for (const field of fields) {
|
||||
properties[field.name] = String(field.values[i] ?? '');
|
||||
}
|
||||
results.push({ text, value, properties });
|
||||
}
|
||||
return results;
|
||||
};
|
||||
@@ -21,7 +21,6 @@ export { TLSSecretsConfig } from './components/configuration/TLSSecretsConfig';
|
||||
export { useMigrateDatabaseFields } from './components/configuration/useMigrateDatabaseFields';
|
||||
export { SqlQueryEditorLazy } from './components/QueryEditorLazy';
|
||||
export type { QueryHeaderProps } from './components/QueryHeader';
|
||||
export { SQLVariableSupport } from './SQLVariableSupport';
|
||||
export { createSelectClause, haveColumns } from './utils/sql.utils';
|
||||
export { applyQueryDefaults } from './defaults';
|
||||
export { makeVariable } from './utils/testHelpers';
|
||||
|
||||
@@ -69,12 +69,6 @@
|
||||
"placeholder-select-format": "Select format",
|
||||
"run-query": "Run query"
|
||||
},
|
||||
"query-meta": {
|
||||
"variables": {
|
||||
"textField": "Text Field",
|
||||
"valueField": "Value Field"
|
||||
}
|
||||
},
|
||||
"query-toolbox": {
|
||||
"content-hit-ctrlcmdreturn-to-run-query": "Hit CTRL/CMD+Return to run query",
|
||||
"tooltip-collapse": "Collapse editor",
|
||||
|
||||
@@ -50,8 +50,6 @@ export enum QueryFormat {
|
||||
Table = 'table',
|
||||
}
|
||||
|
||||
export type SQLQueryMeta = { valueField?: string; textField?: string };
|
||||
|
||||
export interface SQLQuery extends DataQuery {
|
||||
alias?: string;
|
||||
format?: QueryFormat;
|
||||
@@ -61,7 +59,6 @@ export interface SQLQuery extends DataQuery {
|
||||
sql?: SQLExpression;
|
||||
editorMode?: EditorMode;
|
||||
rawQuery?: boolean;
|
||||
meta?: SQLQueryMeta;
|
||||
}
|
||||
|
||||
export interface NameValue {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { HttpResponse, http } from 'msw';
|
||||
|
||||
import { mockTeamsMap } from '../../../../fixtures/teams';
|
||||
|
||||
const getDisplayMapping = () =>
|
||||
http.get<{ namespace: string }>('/apis/iam.grafana.app/v0alpha1/namespaces/:namespace/display', ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
@@ -26,4 +28,76 @@ const getDisplayMapping = () =>
|
||||
});
|
||||
});
|
||||
|
||||
export default [getDisplayMapping()];
|
||||
const listExternalGroupMappings = () =>
|
||||
http.get<{ namespace: string }>('/apis/iam.grafana.app/v0alpha1/namespaces/:namespace/externalgroupmappings', () => {
|
||||
const items = [];
|
||||
for (const [teamName, data] of mockTeamsMap.entries()) {
|
||||
for (const group of data.groups) {
|
||||
items.push({
|
||||
apiVersion: 'iam.grafana.app/v0alpha1',
|
||||
kind: 'ExternalGroupMapping',
|
||||
metadata: {
|
||||
name: `mapping-${teamName}-${group.groupId}`,
|
||||
creationTimestamp: new Date().toISOString(),
|
||||
},
|
||||
spec: {
|
||||
externalGroupId: group.groupId,
|
||||
teamRef: {
|
||||
name: teamName,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return HttpResponse.json({ items });
|
||||
});
|
||||
|
||||
const createExternalGroupMapping = () =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
http.post<{ namespace: string }, any>(
|
||||
'/apis/iam.grafana.app/v0alpha1/namespaces/:namespace/externalgroupmappings',
|
||||
async ({ request }) => {
|
||||
const body = await request.json();
|
||||
const teamName = body.spec.teamRef.name;
|
||||
const groupId = body.spec.externalGroupId;
|
||||
|
||||
const teamData = mockTeamsMap.get(teamName);
|
||||
if (teamData) {
|
||||
teamData.groups.push({ groupId });
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
...body,
|
||||
metadata: {
|
||||
name: `mapping-${teamName}-${groupId}`,
|
||||
creationTimestamp: new Date().toISOString(),
|
||||
...body.metadata,
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const deleteExternalGroupMapping = () =>
|
||||
http.delete<{ namespace: string; name: string }>(
|
||||
'/apis/iam.grafana.app/v0alpha1/namespaces/:namespace/externalgroupmappings/:name',
|
||||
({ params }) => {
|
||||
const { name } = params;
|
||||
|
||||
for (const [teamName, data] of mockTeamsMap.entries()) {
|
||||
const groupIndex = data.groups.findIndex((g) => `mapping-${teamName}-${g.groupId}` === name);
|
||||
if (groupIndex !== -1) {
|
||||
data.groups.splice(groupIndex, 1);
|
||||
return HttpResponse.json({ status: 'Success' });
|
||||
}
|
||||
}
|
||||
|
||||
return HttpResponse.json({ status: 'Failure', message: 'Not found' }, { status: 404 });
|
||||
}
|
||||
);
|
||||
|
||||
export default [
|
||||
getDisplayMapping(),
|
||||
listExternalGroupMappings(),
|
||||
createExternalGroupMapping(),
|
||||
deleteExternalGroupMapping(),
|
||||
];
|
||||
|
||||
@@ -47,7 +47,7 @@ export const getFormFieldsForSilence = (silence: Silence): SilenceFormFields =>
|
||||
startsAt: interval.start.toISOString(),
|
||||
endsAt: interval.end.toISOString(),
|
||||
comment: silence.comment,
|
||||
createdBy: silence.createdBy,
|
||||
createdBy: isExpired ? contextSrv.user.name : silence.createdBy,
|
||||
duration: intervalToAbbreviatedDurationString(interval),
|
||||
isRegex: false,
|
||||
matchers: silence.matchers?.map(matcherToMatcherField) || [],
|
||||
|
||||
@@ -284,7 +284,6 @@ function variableValueOptionsToVariableOptions(varState: MultiValueVariable['sta
|
||||
value: String(o.value),
|
||||
text: o.label,
|
||||
selected: Array.isArray(varState.value) ? varState.value.includes(o.value) : varState.value === o.value,
|
||||
...(o.properties && { properties: o.properties }),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -80,18 +80,18 @@ const buildLabelPath = (label: string) => {
|
||||
return label.includes('.') || label.trim().includes(' ') ? `["${label}"]` : `.${label}`;
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> => {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
};
|
||||
|
||||
const getVariableValueProperties = (variable: TypedVariableModel): string[] => {
|
||||
function collectFieldPaths(option: Record<string, unknown>, currentPath: string): string[] {
|
||||
if (!('valuesFormat' in variable) || variable.valuesFormat !== 'json') {
|
||||
return [];
|
||||
}
|
||||
|
||||
function collectFieldPaths(option: Record<string, string>, currentPath: string) {
|
||||
let paths: string[] = [];
|
||||
for (const field in option) {
|
||||
if (option.hasOwnProperty(field)) {
|
||||
const newPath = `${currentPath}.${field}`;
|
||||
const value = option[field];
|
||||
if (isRecord(value)) {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
paths = [...paths, ...collectFieldPaths(value, newPath)];
|
||||
}
|
||||
paths.push(newPath);
|
||||
@@ -100,23 +100,11 @@ const getVariableValueProperties = (variable: TypedVariableModel): string[] => {
|
||||
return paths;
|
||||
}
|
||||
|
||||
if ('valuesFormat' in variable && variable.valuesFormat === 'json') {
|
||||
try {
|
||||
return collectFieldPaths(JSON.parse(variable.query)[0], variable.name);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
return collectFieldPaths(JSON.parse(variable.query)[0], variable.name);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ('options' in variable && Array.isArray(variable.options) && variable.options.length > 0) {
|
||||
for (const opt of variable.options) {
|
||||
if ('properties' in opt && isRecord(opt.properties) && Object.keys(opt.properties).length > 0) {
|
||||
return collectFieldPaths(opt.properties, variable.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { render, screen, waitFor } from 'test/test-utils';
|
||||
|
||||
import { setBackendSrv } from '@grafana/runtime';
|
||||
import { setBackendSrv, config } from '@grafana/runtime';
|
||||
import { setupMockServer } from '@grafana/test-utils/server';
|
||||
import { MOCK_TEAMS, MOCK_TEAM_GROUPS } from '@grafana/test-utils/unstable';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
@@ -25,9 +25,8 @@ describe('TeamGroupSync', () => {
|
||||
expect(await screen.findAllByRole('row')).toHaveLength(MOCK_TEAM_GROUPS.length + 1); // items plus table header
|
||||
});
|
||||
|
||||
it('should call add group', async () => {
|
||||
it('should add group', async () => {
|
||||
const { user } = setup();
|
||||
// Wait for the groups to load so the "Add group" button appears
|
||||
await screen.findAllByRole('row');
|
||||
|
||||
await user.click(screen.getAllByRole('button', { name: /add group/i })[0]);
|
||||
@@ -43,10 +42,54 @@ describe('TeamGroupSync', () => {
|
||||
const { user } = setup();
|
||||
const groupToRemove = MOCK_TEAM_GROUPS[0].groupId;
|
||||
|
||||
// Wait for group to be rendered
|
||||
await screen.findByRole('row', { name: new RegExp(groupToRemove, 'i') });
|
||||
|
||||
// Remove group
|
||||
await user.click(screen.getByRole('button', { name: `Remove group ${groupToRemove}` }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('row', { name: new RegExp(groupToRemove, 'i') })).not.toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TeamGroupSync with kubernetesExternalGroupMapping enabled', () => {
|
||||
const originalFeatureToggles = { ...config.featureToggles };
|
||||
const originalNamespace = config.namespace;
|
||||
|
||||
beforeAll(() => {
|
||||
config.featureToggles.kubernetesExternalGroupMapping = true;
|
||||
config.namespace = 'default';
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
config.featureToggles = originalFeatureToggles;
|
||||
config.namespace = originalNamespace;
|
||||
});
|
||||
|
||||
it('should render groups table', async () => {
|
||||
setup();
|
||||
expect(await screen.findAllByRole('row')).toHaveLength(MOCK_TEAM_GROUPS.length + 1); // items plus table header
|
||||
});
|
||||
|
||||
it('should add group', async () => {
|
||||
const { user } = setup();
|
||||
await screen.findAllByRole('row');
|
||||
|
||||
await user.click(screen.getAllByRole('button', { name: /add group/i })[0]);
|
||||
expect(screen.getByRole('textbox', { name: /add external group/i })).toBeVisible();
|
||||
|
||||
await user.type(screen.getByRole('textbox', { name: /add external group/i }), 'new-group');
|
||||
await user.click(screen.getAllByRole('button', { name: /add group/i })[1]);
|
||||
|
||||
expect(await screen.findByRole('row', { name: /new-group/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should remove group', async () => {
|
||||
const { user } = setup();
|
||||
const groupToRemove = MOCK_TEAM_GROUPS[0].groupId;
|
||||
|
||||
await screen.findByRole('row', { name: new RegExp(groupToRemove, 'i') });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: `Remove group ${groupToRemove}` }));
|
||||
|
||||
await waitFor(() =>
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { FormEventHandler, useState } from 'react';
|
||||
|
||||
import {
|
||||
TeamGroupDto,
|
||||
useAddTeamGroupApiMutation,
|
||||
useGetTeamGroupsApiQuery,
|
||||
useRemoveTeamGroupApiQueryMutation,
|
||||
} from '@grafana/api-clients/rtkq/legacy';
|
||||
import { TeamGroupDto } from '@grafana/api-clients/rtkq/legacy';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Input, Tooltip, Icon, Button, useTheme2, InlineField, InlineFieldRow, useStyles2 } from '@grafana/ui';
|
||||
import { SlideDown } from 'app/core/components/Animations/SlideDown';
|
||||
@@ -15,6 +10,8 @@ import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import { UpgradeBox, UpgradeContent, UpgradeContentProps } from 'app/core/components/Upgrade/UpgradeBox';
|
||||
import { highlightTrial } from 'app/features/admin/utils';
|
||||
|
||||
import { useAddExternalGroupMapping, useGetExternalGroupMappings, useRemoveExternalGroupMapping } from './hooks';
|
||||
|
||||
interface Props {
|
||||
isReadOnly: boolean;
|
||||
teamUid: string;
|
||||
@@ -27,9 +24,9 @@ export const TeamGroupSync = ({ isReadOnly, teamUid }: Props) => {
|
||||
const [newGroupId, setNewGroupId] = useState('');
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const { data: groups = [] } = useGetTeamGroupsApiQuery({ teamId: teamUid });
|
||||
const [addTeamGroup] = useAddTeamGroupApiMutation();
|
||||
const [removeTeamGroup] = useRemoveTeamGroupApiQueryMutation();
|
||||
const { data: groups = [] } = useGetExternalGroupMappings({ teamId: teamUid });
|
||||
const [addTeamGroup] = useAddExternalGroupMapping();
|
||||
const [removeTeamGroup] = useRemoveExternalGroupMapping();
|
||||
|
||||
const onToggleAdding = () => {
|
||||
setIsAddBoxVisible(!isAddBoxVisible);
|
||||
@@ -46,11 +43,12 @@ export const TeamGroupSync = ({ isReadOnly, teamUid }: Props) => {
|
||||
setNewGroupId('');
|
||||
};
|
||||
|
||||
const onRemoveGroup = async (groupId: string | undefined) => {
|
||||
if (!groupId) {
|
||||
const onRemoveGroup = async (group: TeamGroupDto) => {
|
||||
if (!group.groupId) {
|
||||
return;
|
||||
}
|
||||
await removeTeamGroup({ teamId: teamUid, groupId });
|
||||
// group.uid is always defined here because it comes from the API
|
||||
await removeTeamGroup({ teamId: teamUid, groupId: group.groupId, uid: group.uid! });
|
||||
};
|
||||
|
||||
const isNewGroupValid = () => {
|
||||
@@ -65,7 +63,7 @@ export const TeamGroupSync = ({ isReadOnly, teamUid }: Props) => {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => onRemoveGroup(group.groupId)}
|
||||
onClick={() => onRemoveGroup(group)}
|
||||
disabled={isReadOnly}
|
||||
aria-label={t('teams.team-group-sync.aria-label-remove', 'Remove group {{groupName}}', {
|
||||
groupName: group.groupId,
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
useCreateExternalGroupMappingMutation,
|
||||
useListExternalGroupMappingQuery,
|
||||
useDeleteExternalGroupMappingMutation,
|
||||
} from '@grafana/api-clients/rtkq/iam/v0alpha1';
|
||||
import {
|
||||
useAddTeamGroupApiMutation,
|
||||
useGetTeamGroupsApiQuery,
|
||||
useRemoveTeamGroupApiQueryMutation,
|
||||
TeamGroupDto,
|
||||
} from '@grafana/api-clients/rtkq/legacy';
|
||||
import { config } from '@grafana/runtime';
|
||||
import {
|
||||
useSearchTeamsQuery as useLegacySearchTeamsQuery,
|
||||
useCreateTeamMutation,
|
||||
@@ -152,3 +164,71 @@ export const useCreateTeam = () => {
|
||||
|
||||
return [trigger, response] as const;
|
||||
};
|
||||
|
||||
export const useGetExternalGroupMappings = (args: { teamId: string }) => {
|
||||
const shouldUseAppPlatform = Boolean(config.featureToggles.kubernetesExternalGroupMapping);
|
||||
|
||||
const legacyResult = useGetTeamGroupsApiQuery(args, { skip: shouldUseAppPlatform });
|
||||
|
||||
const { data: newApiData, ...newApiRest } = useListExternalGroupMappingQuery({}, { skip: !shouldUseAppPlatform });
|
||||
|
||||
const groups: TeamGroupDto[] = useMemo(() => {
|
||||
// FIXME: Consider using the search API which has sorting support
|
||||
return (newApiData?.items || [])
|
||||
.filter((item) => item.spec.teamRef.name === args.teamId)
|
||||
.map((item) => ({
|
||||
groupId: item.spec.externalGroupId,
|
||||
uid: item.metadata.name,
|
||||
}));
|
||||
}, [newApiData, args.teamId]);
|
||||
|
||||
if (shouldUseAppPlatform) {
|
||||
return {
|
||||
...newApiRest,
|
||||
data: groups,
|
||||
};
|
||||
}
|
||||
return legacyResult;
|
||||
};
|
||||
|
||||
export const useAddExternalGroupMapping = () => {
|
||||
const legacyMutation = useAddTeamGroupApiMutation();
|
||||
|
||||
const [addNew, newResult] = useCreateExternalGroupMappingMutation();
|
||||
|
||||
const add = async (args: { teamId: string; teamGroupMapping: { groupId: string } }) => {
|
||||
return addNew({
|
||||
externalGroupMapping: {
|
||||
metadata: {
|
||||
generateName: 'external-group-mapping-',
|
||||
},
|
||||
spec: {
|
||||
externalGroupId: args.teamGroupMapping.groupId,
|
||||
teamRef: {
|
||||
name: args.teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (!config.featureToggles.kubernetesExternalGroupMapping) {
|
||||
return legacyMutation;
|
||||
}
|
||||
return [add, newResult] as const;
|
||||
};
|
||||
|
||||
export const useRemoveExternalGroupMapping = () => {
|
||||
const legacyMutation = useRemoveTeamGroupApiQueryMutation();
|
||||
|
||||
const [deleteMapping, deleteResult] = useDeleteExternalGroupMappingMutation();
|
||||
|
||||
const remove = async (args: { teamId: string; groupId: string; uid: string }) => {
|
||||
return deleteMapping({ name: args.uid });
|
||||
};
|
||||
|
||||
if (!config.featureToggles.kubernetesExternalGroupMapping) {
|
||||
return legacyMutation;
|
||||
}
|
||||
return [remove, deleteResult] as const;
|
||||
};
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
SQLQuery,
|
||||
SQLSelectableValue,
|
||||
SqlDatasource,
|
||||
SQLVariableSupport,
|
||||
formatSQL,
|
||||
} from '@grafana/sql';
|
||||
|
||||
@@ -26,7 +25,6 @@ export class PostgresDatasource extends SqlDatasource {
|
||||
|
||||
constructor(instanceSettings: DataSourceInstanceSettings<PostgresOptions>) {
|
||||
super(instanceSettings);
|
||||
this.variables = new SQLVariableSupport(this);
|
||||
}
|
||||
|
||||
getQueryModel(target?: SQLQuery, templateSrv?: TemplateSrv, scopedVars?: ScopedVars): PostgresQueryModel {
|
||||
|
||||
Reference in New Issue
Block a user