Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync
This commit is contained in:
commit
78afd65998
52 files changed
+1299
-277
No files matched your search
+1
-1
@@ -948,7 +948,7 @@ session_duration = "15m"
|
||||
|
||||
# Set the plugins that will receive AWS settings for each request (via plugin context)
|
||||
# By default this will include all Grafana Labs owned AWS plugins, or those that make use of AWS settings (ElasticSearch, Prometheus).
|
||||
forward_settings_to_plugins = cloudwatch, grafana-athena-datasource, grafana-redshift-datasource, grafana-x-ray-datasource, grafana-timestream-datasource, grafana-iot-sitewise-datasource, grafana-iot-twinmaker-app, grafana-opensearch-datasource, aws-datasource-provisioner, elasticsearch, prometheus
|
||||
forward_settings_to_plugins = cloudwatch, grafana-athena-datasource, grafana-redshift-datasource, grafana-x-ray-datasource, grafana-timestream-datasource, grafana-iot-sitewise-datasource, grafana-iot-twinmaker-app, grafana-opensearch-datasource, aws-datasource-provisioner, elasticsearch, prometheus, grafana-amazonprometheus-datasource, grafana-aurora-datasource
|
||||
|
||||
#################################### Azure ###############################
|
||||
[azure]
|
||||
|
||||
@@ -66,8 +66,17 @@ The metrics query editor helps you select metrics, group and aggregate by labels
|
||||
1. _(Optional)_ Use the plus and minus icons in the filter and group-by sections to add and remove filters or group-by clauses.
|
||||
|
||||
Google Cloud Monitoring supports several metrics types, such as `GAUGE`, `DELTA,` and `CUMULATIVE`.
|
||||
Each supports different aggregation options, such as reducers and aligners.
|
||||
The metrics query editor lists available aggregation methods for a selected metric, and sets a default reducer and aligner when you select a metric.
|
||||
Each supports different aggregation options, such as reducers and aligners. Additionally, metrics have specific value types that can be either scalar or a distribution.
|
||||
|
||||
The metrics query editor lists available aggregation methods for a selected metric, and sets a default aggregation, reducer and aligner when you select a metric.
|
||||
|
||||
In the case that the metric value type is a distribution, the aggregation will be set by default to the mean. For scalar value types, there is no aggregation by default.
|
||||
|
||||
The various metrics are documented [here](https://cloud.google.com/monitoring/api/metrics_gcp) and further details on the kinds and types of metrics can be found [here](https://cloud.google.com/monitoring/api/v3/kinds-and-types).
|
||||
|
||||
{{% admonition type="note" %}}
|
||||
Distribution metrics are typically best visualized as either a heatmap or histogram. When visualizing in this way, aggregation is not necessary. However, for other visualization types, performance degradation may be observed when attempting to query distribution metrics that are not aggregated due to the number of potential buckets that can be returned. For more information on how to visualize distribution metrics refer to [this page](https://cloud.google.com/monitoring/charts/charting-distribution-metrics).
|
||||
{{% /admonition %}}
|
||||
|
||||
### Apply a filter
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ _Logs_ are structured records of events or messages generated by a system or app
|
||||
|
||||
The logs visualization displays these records from data sources that support logs, such as Elastic, Influx, and Loki. The logs visualization has colored indicators of log status, as well as collapsible log events that help you analyze the information generated.
|
||||
|
||||
{{< figure src="/static/img/docs/v64/logs-panel.png" max-width="1025px" alt="Logs panel" >}}
|
||||

|
||||
|
||||
{{< docs/play title="Logs Panel" url="https://play.grafana.org/d/6NmftOxZz/" >}}
|
||||
|
||||
@@ -46,7 +46,7 @@ The following video provides a walkthrough of creating a logs visualization. You
|
||||
|
||||
## Supported data formats
|
||||
|
||||
The logs visualization works best with log-type datasets such as queries from data sources like Loki, Elastic, and InlfuxDB.
|
||||
The logs visualization works best with log-type datasets such as queries from data sources like Loki, Elastic, and InfluxDB.
|
||||
|
||||
You can also build log-formatted data from other data sources as long as the first field is a time type followed by string, number, and time fields. The leading time field is used to sort and timestamp the logs and if the data contains other time-type fields, they’re included as elements of the logged record.
|
||||
|
||||
@@ -66,27 +66,33 @@ To limit the number of log lines rendered in the visualization, you can use the
|
||||
|
||||

|
||||
|
||||
## Panel options
|
||||
|
||||
{{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
## Log level
|
||||
|
||||
For logs where a **level** label is specified, we use the value of the label to determine the log level and update color accordingly. If the log doesn't have a level label specified, we try to find out if its content matches any of the supported expressions (see below for more information). The log level is always determined by the first match. In case Grafana is not able to determine a log level, it will be visualized with **unknown** log level. See [supported log levels and mappings of log level abbreviation and expressions](ref:log-levels).
|
||||
|
||||
## Log details
|
||||
## Configuration options
|
||||
|
||||
Each log row has an extendable area with its labels and detected fields, for more robust interaction. Each field or label has a stats icon to display ad-hoc statistics in relation to all displayed logs.
|
||||
{{< docs/shared lookup="visualizations/config-options-intro.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
### Display options
|
||||
### Panel options
|
||||
|
||||
{{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||
|
||||
### Logs options
|
||||
|
||||
Use these settings to refine your visualization:
|
||||
|
||||
- **Time -** Show or hide the time column. This is the timestamp associated with the log line as reported from the data source.
|
||||
- **Unique labels -** Show or hide the unique labels column, which shows only non-common labels.
|
||||
- **Common labels -** Show or hide the common labels.
|
||||
- **Wrap lines -** Toggle line wrapping.
|
||||
- **Prettify JSON -** Set this to `true` to pretty print all JSON logs. This setting does not affect logs in any format other than JSON.
|
||||
- **Enable log details -** Toggle option to see the log details view for each log row. The default setting is true.
|
||||
- **Deduplication -** Hides log messages that are duplicates of others shown according to your selected criteria. Choose from: **Exact** (ignoring ISO datetimes), **Numerical** (ignoring only those that differ by numbers such as IPs or latencies), or **Signatures** (removing successive lines with identical punctuation and white space).
|
||||
- **Order -** Display results in descending or ascending time order. The default is **Descending**, showing the newest logs first. Set to **Ascending** to show the oldest log lines first.
|
||||
<!-- prettier-ignore-start -->
|
||||
|
||||
| Option | Description |
|
||||
| ------ | ----------- |
|
||||
| Time | Show or hide the time column. This is the timestamp associated with the log line as reported from the data source. |
|
||||
| Unique labels | Show or hide the unique labels column, which shows only non-common labels. |
|
||||
| Common labels | Show or hide the common labels. |
|
||||
| Wrap lines | Turn line wrapping on or off. |
|
||||
| Prettify JSON | Toggle the switch on to pretty print all JSON logs. This setting does not affect logs in any format other than JSON. |
|
||||
| Enable log details | Toggle the switch on to see an extendable area with log details including labels and detected fields. Each field or label has a stats icon to display ad-hoc statistics in relation to all displayed logs. The default setting is on. |
|
||||
| Deduplication | Hide log messages that are duplicates of others shown, according to your selected criteria. Choose from: <ul><li>**Exact** - Ignoring ISO datetimes.</li><li>**Numerical** - Ignoring only those that differ by numbers such as IPs or latencies.</li><li>**Signatures** - Removing successive lines with identical punctuation and white space.</li></ul> |
|
||||
| Order | Set whether to show results **Newest first** or **Oldest first**. |
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
+167
-9
@@ -3,8 +3,8 @@ import { config } from '@grafana/runtime';
|
||||
import { SUGGESTIONS_LIMIT } from '../../../language_provider';
|
||||
import { FUNCTIONS } from '../../../promql';
|
||||
|
||||
import { getCompletions } from './completions';
|
||||
import { DataProvider, DataProviderParams } from './data_provider';
|
||||
import { filterMetricNames, getCompletions } from './completions';
|
||||
import { DataProvider, type DataProviderParams } from './data_provider';
|
||||
import type { Situation } from './situation';
|
||||
|
||||
const history: string[] = ['previous_metric_name_1', 'previous_metric_name_2', 'previous_metric_name_3'];
|
||||
@@ -41,6 +41,133 @@ afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('filterMetricNames', () => {
|
||||
const sampleMetrics = [
|
||||
'http_requests_total',
|
||||
'http_requests_failed',
|
||||
'node_cpu_seconds_total',
|
||||
'node_memory_usage_bytes',
|
||||
'very_long_metric_name_with_many_underscores_and_detailed_description',
|
||||
'metric_name_1_with_extra_terms_included',
|
||||
];
|
||||
|
||||
describe('empty input', () => {
|
||||
it('should return all metrics up to limit when input is empty', () => {
|
||||
const result = filterMetricNames({
|
||||
metricNames: sampleMetrics,
|
||||
inputText: '',
|
||||
limit: 3,
|
||||
});
|
||||
expect(result).toEqual(sampleMetrics.slice(0, 3));
|
||||
});
|
||||
|
||||
it('should return all metrics when input is whitespace', () => {
|
||||
const result = filterMetricNames({
|
||||
metricNames: sampleMetrics,
|
||||
inputText: ' ',
|
||||
limit: 3,
|
||||
});
|
||||
expect(result).toEqual(sampleMetrics.slice(0, 3));
|
||||
});
|
||||
});
|
||||
|
||||
describe('simple searches (≤ 4 terms)', () => {
|
||||
it('should match exact strings', () => {
|
||||
const result = filterMetricNames({
|
||||
metricNames: sampleMetrics,
|
||||
inputText: 'http_requests_total',
|
||||
limit: 10,
|
||||
});
|
||||
expect(result).toContainEqual('http_requests_total');
|
||||
});
|
||||
|
||||
it('should match with single character errors', () => {
|
||||
// substitution
|
||||
let result = filterMetricNames({
|
||||
metricNames: sampleMetrics,
|
||||
inputText: 'http_requezts_total', // 's' replaced with 'z'
|
||||
limit: 10,
|
||||
});
|
||||
expect(result).toContainEqual('http_requests_total');
|
||||
|
||||
// ransposition
|
||||
result = filterMetricNames({
|
||||
metricNames: sampleMetrics,
|
||||
inputText: 'http_reqeust_total', // 'ue' swapped
|
||||
limit: 10,
|
||||
});
|
||||
expect(result).toContainEqual('http_requests_total');
|
||||
|
||||
// deletion
|
||||
result = filterMetricNames({
|
||||
metricNames: sampleMetrics,
|
||||
inputText: 'http_reqests_total', // missing 'u'
|
||||
limit: 10,
|
||||
});
|
||||
expect(result).toContainEqual('http_requests_total');
|
||||
|
||||
// insertion
|
||||
result = filterMetricNames({
|
||||
metricNames: sampleMetrics,
|
||||
inputText: 'http_reqquests_total', // extra 'q'
|
||||
limit: 10,
|
||||
});
|
||||
expect(result).toContainEqual('http_requests_total');
|
||||
});
|
||||
|
||||
it('should match partial strings', () => {
|
||||
const result = filterMetricNames({
|
||||
metricNames: sampleMetrics,
|
||||
inputText: 'requests', // partial match
|
||||
limit: 10,
|
||||
});
|
||||
expect(result).toContainEqual('http_requests_total');
|
||||
expect(result).toContainEqual('http_requests_failed');
|
||||
});
|
||||
|
||||
it('should not match with multiple errors', () => {
|
||||
const result = filterMetricNames({
|
||||
metricNames: sampleMetrics,
|
||||
inputText: 'htp_reqests_total', // two errors: missing 't' and missing 'u'
|
||||
limit: 10,
|
||||
});
|
||||
expect(result).not.toContainEqual('http_requests_total');
|
||||
});
|
||||
});
|
||||
|
||||
describe('complex searches (> 4 terms)', () => {
|
||||
it('should use substring matching for each term', () => {
|
||||
const result = filterMetricNames({
|
||||
metricNames: sampleMetrics,
|
||||
inputText: 'metric name 1 with extra terms',
|
||||
limit: 10,
|
||||
});
|
||||
expect(result).toContainEqual('metric_name_1_with_extra_terms_included');
|
||||
});
|
||||
|
||||
it('should return empty array when no metrics match all terms', () => {
|
||||
const result = filterMetricNames({
|
||||
metricNames: sampleMetrics,
|
||||
inputText: 'metric name 1 with nonexistent terms',
|
||||
limit: 10,
|
||||
});
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should stop searching after limit is reached', () => {
|
||||
const manyMetrics = Array.from({ length: 10 }, (_, i) => `metric_name_${i}_with_terms`);
|
||||
|
||||
const result = filterMetricNames({
|
||||
metricNames: manyMetrics,
|
||||
inputText: 'metric name with terms other words', // > 4 terms
|
||||
limit: 3,
|
||||
});
|
||||
|
||||
expect(result.length).toBeLessThanOrEqual(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
type MetricNameSituation = Extract<Situation['type'], 'AT_ROOT' | 'EMPTY' | 'IN_FUNCTION'>;
|
||||
const metricNameCompletionSituations = ['AT_ROOT', 'IN_FUNCTION', 'EMPTY'] as MetricNameSituation[];
|
||||
|
||||
@@ -74,22 +201,22 @@ describe.each(metricNameCompletionSituations)('metric name completions in situat
|
||||
expect(completions?.length).toBeLessThanOrEqual(expectedCompletionsCount);
|
||||
});
|
||||
|
||||
it('should limit completions for metric names when the number of metric names is greater than the limit', async () => {
|
||||
it('should limit completions for metric names when the number exceeds the limit', async () => {
|
||||
const situation: Situation = {
|
||||
type: situationType,
|
||||
};
|
||||
const expectedCompletionsCount = getSuggestionCountForSituation(situationType, metrics.beyondLimit.length);
|
||||
jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValue(metrics.beyondLimit);
|
||||
|
||||
// No text input
|
||||
dataProvider.monacoSettings.setInputInRange('');
|
||||
// Complex query
|
||||
dataProvider.monacoSettings.setInputInRange('metric name one two three four five');
|
||||
let completions = await getCompletions(situation, dataProvider);
|
||||
expect(completions).toHaveLength(expectedCompletionsCount);
|
||||
expect(completions.length).toBeLessThanOrEqual(expectedCompletionsCount);
|
||||
|
||||
// With text input (use fuzzy search)
|
||||
dataProvider.monacoSettings.setInputInRange('name_1');
|
||||
// Simple query with fuzzy match
|
||||
dataProvider.monacoSettings.setInputInRange('metric_name_');
|
||||
completions = await getCompletions(situation, dataProvider);
|
||||
expect(completions?.length).toBeLessThanOrEqual(expectedCompletionsCount);
|
||||
expect(completions.length).toBeLessThanOrEqual(expectedCompletionsCount);
|
||||
});
|
||||
|
||||
it('should enable autocomplete suggestions update when the number of metric names is greater than the limit', async () => {
|
||||
@@ -115,4 +242,35 @@ describe.each(metricNameCompletionSituations)('metric name completions in situat
|
||||
await getCompletions(situation, dataProvider);
|
||||
expect(dataProvider.monacoSettings.suggestionsIncomplete).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle complex queries efficiently', async () => {
|
||||
const situation: Situation = {
|
||||
type: situationType,
|
||||
};
|
||||
|
||||
const testMetrics = ['metric_name_1', 'metric_name_2', 'metric_name_1_with_extra_terms', 'unrelated_metric'];
|
||||
jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValue(testMetrics);
|
||||
|
||||
// Test with a complex query (> 4 terms)
|
||||
dataProvider.monacoSettings.setInputInRange('metric name 1 with extra terms more');
|
||||
const completions = await getCompletions(situation, dataProvider);
|
||||
|
||||
const metricCompletions = completions.filter((c) => c.type === 'METRIC_NAME');
|
||||
expect(metricCompletions.some((c) => c.label === 'metric_name_1_with_extra_terms')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle multiple term queries efficiently', async () => {
|
||||
const situation: Situation = {
|
||||
type: situationType,
|
||||
};
|
||||
|
||||
jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValue(metrics.beyondLimit);
|
||||
|
||||
// Test with multiple terms
|
||||
dataProvider.monacoSettings.setInputInRange('metric name 1 2 3 4 5');
|
||||
const completions = await getCompletions(situation, dataProvider);
|
||||
|
||||
const expectedCompletionsCount = getSuggestionCountForSituation(situationType, metrics.beyondLimit.length);
|
||||
expect(completions.length).toBeLessThanOrEqual(expectedCompletionsCount);
|
||||
});
|
||||
});
|
||||
+30
-6
@@ -22,7 +22,31 @@ type Completion = {
|
||||
triggerOnInsert?: boolean;
|
||||
};
|
||||
|
||||
const metricNamesSearchClient = new UFuzzy({ intraMode: 1 });
|
||||
const metricNamesSearch = {
|
||||
// see https://github.com/leeoniya/uFuzzy?tab=readme-ov-file#how-it-works for details
|
||||
multiInsert: new UFuzzy({ intraMode: 0 }),
|
||||
singleError: new UFuzzy({ intraMode: 1 }),
|
||||
};
|
||||
|
||||
interface MetricFilterOptions {
|
||||
metricNames: string[];
|
||||
inputText: string;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export function filterMetricNames({ metricNames, inputText, limit }: MetricFilterOptions): string[] {
|
||||
if (!inputText?.trim()) {
|
||||
return metricNames.slice(0, limit);
|
||||
}
|
||||
|
||||
const terms = metricNamesSearch.multiInsert.split(inputText); // e.g. 'some_metric_name or-another' -> ['some', 'metric', 'name', 'or', 'another']
|
||||
const isComplexSearch = terms.length > 4;
|
||||
const fuzzyResults = isComplexSearch
|
||||
? metricNamesSearch.multiInsert.filter(metricNames, inputText) // for complex searches, prioritize performance by using MultiInsert fuzzy search
|
||||
: metricNamesSearch.singleError.filter(metricNames, inputText); // for simple searches, prioritize flexibility by using SingleError fuzzy search
|
||||
|
||||
return fuzzyResults ? fuzzyResults.slice(0, limit).map((idx) => metricNames[idx]) : [];
|
||||
}
|
||||
|
||||
// we order items like: history, functions, metrics
|
||||
function getAllMetricNamesCompletions(dataProvider: DataProvider): Completion[] {
|
||||
@@ -36,11 +60,11 @@ function getAllMetricNamesCompletions(dataProvider: DataProvider): Completion[]
|
||||
monacoSettings.enableAutocompleteSuggestionsUpdate();
|
||||
|
||||
if (monacoSettings.inputInRange) {
|
||||
metricNames =
|
||||
metricNamesSearchClient
|
||||
.filter(metricNames, monacoSettings.inputInRange)
|
||||
?.slice(0, dataProvider.metricNamesSuggestionLimit)
|
||||
.map((idx) => metricNames[idx]) ?? [];
|
||||
metricNames = filterMetricNames({
|
||||
metricNames,
|
||||
inputText: monacoSettings.inputInRange,
|
||||
limit: dataProvider.metricNamesSuggestionLimit,
|
||||
});
|
||||
} else {
|
||||
metricNames = metricNames.slice(0, dataProvider.metricNamesSuggestionLimit);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"emitDeclarationOnly": true,
|
||||
"isolatedModules": true,
|
||||
"allowJs": true,
|
||||
"rootDirs": ["."]
|
||||
"rootDirs": ["."],
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": ["dist/**/*"],
|
||||
"extends": "@grafana/tsconfig",
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import { useObservable } from 'react-use';
|
||||
|
||||
import { locationService as mainLocationService } from './LocationService';
|
||||
import { SidecarService_EXPERIMENTAL, sidecarServiceSingleton_EXPERIMENTAL } from './SidecarService_EXPERIMENTAL';
|
||||
|
||||
export const SidecarContext_EXPERIMENTAL = createContext<SidecarService_EXPERIMENTAL>(
|
||||
sidecarServiceSingleton_EXPERIMENTAL
|
||||
);
|
||||
|
||||
const HIDDEN_ROUTES = ['/login'];
|
||||
|
||||
/**
|
||||
* This is the main way to interact with the sidecar service inside a react context. It provides a wrapper around the
|
||||
* service props so that even though they are observables we just pass actual values to the components.
|
||||
@@ -24,17 +27,33 @@ export function useSidecar_EXPERIMENTAL() {
|
||||
const initialContext = useObservable(service.initialContextObservable, service.initialContext);
|
||||
const activePluginId = useObservable(service.activePluginIdObservable, service.activePluginId);
|
||||
const locationService = service.getLocationService();
|
||||
const forceHidden = HIDDEN_ROUTES.includes(mainLocationService.getLocation().pathname);
|
||||
|
||||
return {
|
||||
activePluginId,
|
||||
initialContext,
|
||||
activePluginId: forceHidden ? undefined : activePluginId,
|
||||
initialContext: forceHidden ? undefined : initialContext,
|
||||
locationService,
|
||||
// TODO: currently this allows anybody to open any app, in the future we should probably scope this to the
|
||||
// current app but that means we will need to incorporate this better into the plugin platform APIs which
|
||||
// we will do once the functionality is reasonably stable
|
||||
openApp: (pluginId: string, context?: unknown) => service.openApp(pluginId, context),
|
||||
openAppV2: (pluginId: string, path?: string) => service.openAppV2(pluginId, path),
|
||||
openApp: (pluginId: string, context?: unknown) => {
|
||||
if (forceHidden) {
|
||||
return;
|
||||
}
|
||||
return service.openApp(pluginId, context);
|
||||
},
|
||||
openAppV2: (pluginId: string, path?: string) => {
|
||||
if (forceHidden) {
|
||||
return;
|
||||
}
|
||||
return service.openAppV2(pluginId, path);
|
||||
},
|
||||
closeApp: () => service.closeApp(),
|
||||
isAppOpened: (pluginId: string) => service.isAppOpened(pluginId),
|
||||
isAppOpened: (pluginId: string) => {
|
||||
if (forceHidden) {
|
||||
return false;
|
||||
}
|
||||
return service.isAppOpened(pluginId);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import * as H from 'history';
|
||||
import { pick } from 'lodash';
|
||||
import { BehaviorSubject, map, Observable } from 'rxjs';
|
||||
|
||||
import { reportInteraction } from '../analytics/utils';
|
||||
import { config } from '../config';
|
||||
|
||||
import { HistoryWrapper, LocationService, locationService } from './LocationService';
|
||||
@@ -92,6 +93,8 @@ export class SidecarService_EXPERIMENTAL {
|
||||
}
|
||||
this._initialContext.next(context);
|
||||
this.memoryLocationService.push({ pathname: `/a/${pluginId}` });
|
||||
|
||||
reportInteraction('sidecar_service_open_app', { pluginId, version: 1 });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,6 +107,7 @@ export class SidecarService_EXPERIMENTAL {
|
||||
}
|
||||
|
||||
this.memoryLocationService.push({ pathname: `/a/${pluginId}${path || ''}` });
|
||||
reportInteraction('sidecar_service_open_app', { pluginId, version: 2 });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,6 +120,8 @@ export class SidecarService_EXPERIMENTAL {
|
||||
|
||||
this._initialContext.next(undefined);
|
||||
this.memoryLocationService.replace({ pathname: '/' });
|
||||
|
||||
reportInteraction('sidecar_service_close_app');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,7 +145,9 @@ export class SidecarService_EXPERIMENTAL {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!(this.activePluginId && (this.activePluginId === pluginId || getMainAppPluginId() === pluginId));
|
||||
const result = !!(this.activePluginId && (this.activePluginId === pluginId || getMainAppPluginId() === pluginId));
|
||||
reportInteraction('sidecar_service_is_app_opened', { pluginId, isOpened: result });
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,19 +31,16 @@ type UPlotChartState = {
|
||||
export class UPlotChart extends Component<PlotProps, UPlotChartState> {
|
||||
plotContainer = createRef<HTMLDivElement>();
|
||||
plotCanvasBBox = createRef<DOMRect>();
|
||||
plotInstance: uPlot | null = null;
|
||||
|
||||
constructor(props: PlotProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
plot: null,
|
||||
};
|
||||
}
|
||||
|
||||
reinitPlot() {
|
||||
let { width, height, plotRef } = this.props;
|
||||
|
||||
this.state.plot?.destroy();
|
||||
this.plotInstance?.destroy();
|
||||
|
||||
if (width === 0 && height === 0) {
|
||||
return;
|
||||
@@ -69,7 +66,7 @@ export class UPlotChart extends Component<PlotProps, UPlotChartState> {
|
||||
plotRef(plot);
|
||||
}
|
||||
|
||||
this.setState({ plot });
|
||||
this.plotInstance = plot;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -77,21 +74,19 @@ export class UPlotChart extends Component<PlotProps, UPlotChartState> {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.state.plot?.destroy();
|
||||
this.plotInstance?.destroy();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: PlotProps) {
|
||||
let { plot } = this.state;
|
||||
|
||||
if (!sameDims(prevProps, this.props)) {
|
||||
plot?.setSize({
|
||||
this.plotInstance?.setSize({
|
||||
width: Math.floor(this.props.width),
|
||||
height: Math.floor(this.props.height),
|
||||
});
|
||||
} else if (!sameConfig(prevProps, this.props)) {
|
||||
this.reinitPlot();
|
||||
} else if (!sameData(prevProps, this.props)) {
|
||||
plot?.setData(this.props.data as AlignedData);
|
||||
this.plotInstance?.setData(this.props.data as AlignedData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -838,14 +838,14 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
|
||||
if dashboardService == nil {
|
||||
dashboardService, err = service.ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions,
|
||||
ac, folderSvc, fStore, nil,
|
||||
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
dashboardProvisioningService, err := service.ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions,
|
||||
ac, folderSvc, fStore, nil,
|
||||
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -478,7 +478,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
|
||||
dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl(
|
||||
sc.cfg, dashStore, folderStore,
|
||||
features, folderPermissions, dashboardPermissions, ac,
|
||||
folderServiceWithFlagOn, fStore, nil,
|
||||
folderServiceWithFlagOn, fStore, nil, zanzana.NewNoopClient(),
|
||||
)
|
||||
require.NoError(b, err)
|
||||
|
||||
|
||||
@@ -9,10 +9,6 @@ import (
|
||||
type DB struct {
|
||||
}
|
||||
|
||||
func (db *DB) TablesList(rawSQL string) ([]string, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (db *DB) RunCommands(commands []string) (string, error) {
|
||||
return "", errors.New("not implemented")
|
||||
}
|
||||
|
||||
@@ -20,10 +20,10 @@ var logger = log.New("sql_expr")
|
||||
|
||||
// TablesList returns a list of tables for the sql statement
|
||||
func TablesList(rawSQL string) ([]string, error) {
|
||||
duckDB := NewInMemoryDB()
|
||||
db := NewInMemoryDB()
|
||||
rawSQL = strings.Replace(rawSQL, "'", "''", -1)
|
||||
cmd := fmt.Sprintf("SELECT json_serialize_sql('%s')", rawSQL)
|
||||
ret, err := duckDB.RunCommands([]string{cmd})
|
||||
ret, err := db.RunCommands([]string{cmd})
|
||||
if err != nil {
|
||||
logger.Error("error serializing sql", "error", err.Error(), "sql", rawSQL, "cmd", cmd)
|
||||
return nil, fmt.Errorf("error serializing sql: %s", err.Error())
|
||||
|
||||
@@ -769,6 +769,242 @@ func (*WriteResponse) Descriptor() ([]byte, []int) {
|
||||
return file_extention_proto_rawDescGZIP(), []int{12}
|
||||
}
|
||||
|
||||
type BatchCheckRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Subject string `protobuf:"bytes,1,opt,name=subject,proto3" json:"subject,omitempty"`
|
||||
Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
Items []*BatchCheckItem `protobuf:"bytes,3,rep,name=items,proto3" json:"items,omitempty"`
|
||||
}
|
||||
|
||||
func (x *BatchCheckRequest) Reset() {
|
||||
*x = BatchCheckRequest{}
|
||||
mi := &file_extention_proto_msgTypes[13]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *BatchCheckRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*BatchCheckRequest) ProtoMessage() {}
|
||||
|
||||
func (x *BatchCheckRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_extention_proto_msgTypes[13]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use BatchCheckRequest.ProtoReflect.Descriptor instead.
|
||||
func (*BatchCheckRequest) Descriptor() ([]byte, []int) {
|
||||
return file_extention_proto_rawDescGZIP(), []int{13}
|
||||
}
|
||||
|
||||
func (x *BatchCheckRequest) GetSubject() string {
|
||||
if x != nil {
|
||||
return x.Subject
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *BatchCheckRequest) GetNamespace() string {
|
||||
if x != nil {
|
||||
return x.Namespace
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *BatchCheckRequest) GetItems() []*BatchCheckItem {
|
||||
if x != nil {
|
||||
return x.Items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type BatchCheckItem struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Verb string `protobuf:"bytes,1,opt,name=verb,proto3" json:"verb,omitempty"`
|
||||
Group string `protobuf:"bytes,2,opt,name=group,proto3" json:"group,omitempty"`
|
||||
Resource string `protobuf:"bytes,3,opt,name=resource,proto3" json:"resource,omitempty"`
|
||||
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Subresource string `protobuf:"bytes,5,opt,name=subresource,proto3" json:"subresource,omitempty"`
|
||||
Folder string `protobuf:"bytes,6,opt,name=folder,proto3" json:"folder,omitempty"`
|
||||
}
|
||||
|
||||
func (x *BatchCheckItem) Reset() {
|
||||
*x = BatchCheckItem{}
|
||||
mi := &file_extention_proto_msgTypes[14]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *BatchCheckItem) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*BatchCheckItem) ProtoMessage() {}
|
||||
|
||||
func (x *BatchCheckItem) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_extention_proto_msgTypes[14]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use BatchCheckItem.ProtoReflect.Descriptor instead.
|
||||
func (*BatchCheckItem) Descriptor() ([]byte, []int) {
|
||||
return file_extention_proto_rawDescGZIP(), []int{14}
|
||||
}
|
||||
|
||||
func (x *BatchCheckItem) GetVerb() string {
|
||||
if x != nil {
|
||||
return x.Verb
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *BatchCheckItem) GetGroup() string {
|
||||
if x != nil {
|
||||
return x.Group
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *BatchCheckItem) GetResource() string {
|
||||
if x != nil {
|
||||
return x.Resource
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *BatchCheckItem) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *BatchCheckItem) GetSubresource() string {
|
||||
if x != nil {
|
||||
return x.Subresource
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *BatchCheckItem) GetFolder() string {
|
||||
if x != nil {
|
||||
return x.Folder
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type BatchCheckResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Groups map[string]*BatchCheckGroupResource `protobuf:"bytes,1,rep,name=groups,proto3" json:"groups,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
}
|
||||
|
||||
func (x *BatchCheckResponse) Reset() {
|
||||
*x = BatchCheckResponse{}
|
||||
mi := &file_extention_proto_msgTypes[15]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *BatchCheckResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*BatchCheckResponse) ProtoMessage() {}
|
||||
|
||||
func (x *BatchCheckResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_extention_proto_msgTypes[15]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use BatchCheckResponse.ProtoReflect.Descriptor instead.
|
||||
func (*BatchCheckResponse) Descriptor() ([]byte, []int) {
|
||||
return file_extention_proto_rawDescGZIP(), []int{15}
|
||||
}
|
||||
|
||||
func (x *BatchCheckResponse) GetGroups() map[string]*BatchCheckGroupResource {
|
||||
if x != nil {
|
||||
return x.Groups
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type BatchCheckGroupResource struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Items map[string]bool `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
|
||||
}
|
||||
|
||||
func (x *BatchCheckGroupResource) Reset() {
|
||||
*x = BatchCheckGroupResource{}
|
||||
mi := &file_extention_proto_msgTypes[16]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *BatchCheckGroupResource) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*BatchCheckGroupResource) ProtoMessage() {}
|
||||
|
||||
func (x *BatchCheckGroupResource) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_extention_proto_msgTypes[16]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use BatchCheckGroupResource.ProtoReflect.Descriptor instead.
|
||||
func (*BatchCheckGroupResource) Descriptor() ([]byte, []int) {
|
||||
return file_extention_proto_rawDescGZIP(), []int{16}
|
||||
}
|
||||
|
||||
func (x *BatchCheckGroupResource) GetItems() map[string]bool {
|
||||
if x != nil {
|
||||
return x.Items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_extention_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_extention_proto_rawDesc = []byte{
|
||||
@@ -874,27 +1110,74 @@ var file_extention_proto_rawDesc = []byte{
|
||||
0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73,
|
||||
0x22, 0x0f, 0x0a, 0x0d, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x32, 0xfb, 0x01, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x45, 0x78, 0x74, 0x65, 0x6e,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x49, 0x0a, 0x04, 0x4c,
|
||||
0x69, 0x73, 0x74, 0x12, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65,
|
||||
0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74,
|
||||
0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65,
|
||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x1f,
|
||||
0x65, 0x22, 0x85, 0x01, 0x0a, 0x11, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65,
|
||||
0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63,
|
||||
0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12,
|
||||
0x38, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22,
|
||||
0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||
0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x12, 0x4c, 0x0a, 0x05, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74,
|
||||
0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x74,
|
||||
0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0xa4, 0x01, 0x0a, 0x0e, 0x42, 0x61,
|
||||
0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04,
|
||||
0x76, 0x65, 0x72, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x76, 0x65, 0x72, 0x62,
|
||||
0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
|
||||
0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
|
||||
0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x75, 0x62, 0x72, 0x65, 0x73,
|
||||
0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62,
|
||||
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64,
|
||||
0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72,
|
||||
0x22, 0xc8, 0x01, 0x0a, 0x12, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70,
|
||||
0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e,
|
||||
0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74,
|
||||
0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e,
|
||||
0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x67, 0x72, 0x6f,
|
||||
0x75, 0x70, 0x73, 0x1a, 0x66, 0x0a, 0x0b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74,
|
||||
0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x03, 0x6b, 0x65, 0x79, 0x12, 0x41, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65,
|
||||
0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68,
|
||||
0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
||||
0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xa1, 0x01, 0x0a, 0x17,
|
||||
0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52,
|
||||
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73,
|
||||
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65,
|
||||
0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63,
|
||||
0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75,
|
||||
0x72, 0x63, 0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05,
|
||||
0x69, 0x74, 0x65, 0x6d, 0x73, 0x1a, 0x38, 0x0a, 0x0a, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x45, 0x6e,
|
||||
0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x32,
|
||||
0xd8, 0x02, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x49, 0x0a, 0x04, 0x4c, 0x69, 0x73,
|
||||
0x74, 0x12, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||
0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x0a, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65,
|
||||
0x63, 0x6b, 0x12, 0x25, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65,
|
||||
0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68,
|
||||
0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42,
|
||||
0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x12, 0x49, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68,
|
||||
0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52,
|
||||
0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x75, 0x74,
|
||||
0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e,
|
||||
0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61,
|
||||
0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76,
|
||||
0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42,
|
||||
0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72,
|
||||
0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b,
|
||||
0x67, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x7a,
|
||||
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x33,
|
||||
0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05,
|
||||
0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78,
|
||||
0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e,
|
||||
0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69,
|
||||
0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69,
|
||||
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61,
|
||||
0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72,
|
||||
0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2f, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -909,7 +1192,7 @@ func file_extention_proto_rawDescGZIP() []byte {
|
||||
return file_extention_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
|
||||
var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 19)
|
||||
var file_extention_proto_goTypes = []any{
|
||||
(*ListRequest)(nil), // 0: authz.extention.v1.ListRequest
|
||||
(*ListResponse)(nil), // 1: authz.extention.v1.ListResponse
|
||||
@@ -924,33 +1207,45 @@ var file_extention_proto_goTypes = []any{
|
||||
(*WriteRequestDeletes)(nil), // 10: authz.extention.v1.WriteRequestDeletes
|
||||
(*WriteRequest)(nil), // 11: authz.extention.v1.WriteRequest
|
||||
(*WriteResponse)(nil), // 12: authz.extention.v1.WriteResponse
|
||||
(*timestamppb.Timestamp)(nil), // 13: google.protobuf.Timestamp
|
||||
(*structpb.Struct)(nil), // 14: google.protobuf.Struct
|
||||
(*wrapperspb.Int32Value)(nil), // 15: google.protobuf.Int32Value
|
||||
(*BatchCheckRequest)(nil), // 13: authz.extention.v1.BatchCheckRequest
|
||||
(*BatchCheckItem)(nil), // 14: authz.extention.v1.BatchCheckItem
|
||||
(*BatchCheckResponse)(nil), // 15: authz.extention.v1.BatchCheckResponse
|
||||
(*BatchCheckGroupResource)(nil), // 16: authz.extention.v1.BatchCheckGroupResource
|
||||
nil, // 17: authz.extention.v1.BatchCheckResponse.GroupsEntry
|
||||
nil, // 18: authz.extention.v1.BatchCheckGroupResource.ItemsEntry
|
||||
(*timestamppb.Timestamp)(nil), // 19: google.protobuf.Timestamp
|
||||
(*structpb.Struct)(nil), // 20: google.protobuf.Struct
|
||||
(*wrapperspb.Int32Value)(nil), // 21: google.protobuf.Int32Value
|
||||
}
|
||||
var file_extention_proto_depIdxs = []int32{
|
||||
5, // 0: authz.extention.v1.TupleKey.condition:type_name -> authz.extention.v1.RelationshipCondition
|
||||
2, // 1: authz.extention.v1.Tuple.key:type_name -> authz.extention.v1.TupleKey
|
||||
13, // 2: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp
|
||||
14, // 3: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct
|
||||
19, // 2: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp
|
||||
20, // 3: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct
|
||||
7, // 4: authz.extention.v1.ReadRequest.tuple_key:type_name -> authz.extention.v1.ReadRequestTupleKey
|
||||
15, // 5: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value
|
||||
21, // 5: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value
|
||||
3, // 6: authz.extention.v1.ReadResponse.tuples:type_name -> authz.extention.v1.Tuple
|
||||
2, // 7: authz.extention.v1.WriteRequestWrites.tuple_keys:type_name -> authz.extention.v1.TupleKey
|
||||
4, // 8: authz.extention.v1.WriteRequestDeletes.tuple_keys:type_name -> authz.extention.v1.TupleKeyWithoutCondition
|
||||
9, // 9: authz.extention.v1.WriteRequest.writes:type_name -> authz.extention.v1.WriteRequestWrites
|
||||
10, // 10: authz.extention.v1.WriteRequest.deletes:type_name -> authz.extention.v1.WriteRequestDeletes
|
||||
0, // 11: authz.extention.v1.AuthzExtentionService.List:input_type -> authz.extention.v1.ListRequest
|
||||
6, // 12: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest
|
||||
11, // 13: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest
|
||||
1, // 14: authz.extention.v1.AuthzExtentionService.List:output_type -> authz.extention.v1.ListResponse
|
||||
8, // 15: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse
|
||||
12, // 16: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse
|
||||
14, // [14:17] is the sub-list for method output_type
|
||||
11, // [11:14] is the sub-list for method input_type
|
||||
11, // [11:11] is the sub-list for extension type_name
|
||||
11, // [11:11] is the sub-list for extension extendee
|
||||
0, // [0:11] is the sub-list for field type_name
|
||||
14, // 11: authz.extention.v1.BatchCheckRequest.items:type_name -> authz.extention.v1.BatchCheckItem
|
||||
17, // 12: authz.extention.v1.BatchCheckResponse.groups:type_name -> authz.extention.v1.BatchCheckResponse.GroupsEntry
|
||||
18, // 13: authz.extention.v1.BatchCheckGroupResource.items:type_name -> authz.extention.v1.BatchCheckGroupResource.ItemsEntry
|
||||
16, // 14: authz.extention.v1.BatchCheckResponse.GroupsEntry.value:type_name -> authz.extention.v1.BatchCheckGroupResource
|
||||
0, // 15: authz.extention.v1.AuthzExtentionService.List:input_type -> authz.extention.v1.ListRequest
|
||||
13, // 16: authz.extention.v1.AuthzExtentionService.BatchCheck:input_type -> authz.extention.v1.BatchCheckRequest
|
||||
6, // 17: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest
|
||||
11, // 18: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest
|
||||
1, // 19: authz.extention.v1.AuthzExtentionService.List:output_type -> authz.extention.v1.ListResponse
|
||||
15, // 20: authz.extention.v1.AuthzExtentionService.BatchCheck:output_type -> authz.extention.v1.BatchCheckResponse
|
||||
8, // 21: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse
|
||||
12, // 22: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse
|
||||
19, // [19:23] is the sub-list for method output_type
|
||||
15, // [15:19] is the sub-list for method input_type
|
||||
15, // [15:15] is the sub-list for extension type_name
|
||||
15, // [15:15] is the sub-list for extension extendee
|
||||
0, // [0:15] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_extention_proto_init() }
|
||||
@@ -964,7 +1259,7 @@ func file_extention_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_extention_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 13,
|
||||
NumMessages: 19,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -10,6 +10,8 @@ import "google/protobuf/wrappers.proto";
|
||||
|
||||
service AuthzExtentionService {
|
||||
rpc List(ListRequest) returns (ListResponse);
|
||||
rpc BatchCheck(BatchCheckRequest) returns (BatchCheckResponse);
|
||||
|
||||
rpc Read(ReadRequest) returns (ReadResponse);
|
||||
rpc Write(WriteRequest) returns (WriteResponse);
|
||||
}
|
||||
@@ -86,3 +88,26 @@ message WriteRequest {
|
||||
}
|
||||
|
||||
message WriteResponse {}
|
||||
|
||||
message BatchCheckRequest {
|
||||
string subject = 1;
|
||||
string namespace = 2;
|
||||
repeated BatchCheckItem items = 3;
|
||||
}
|
||||
|
||||
message BatchCheckItem {
|
||||
string verb = 1;
|
||||
string group = 2;
|
||||
string resource = 3;
|
||||
string name = 4;
|
||||
string subresource = 5;
|
||||
string folder = 6;
|
||||
}
|
||||
|
||||
message BatchCheckResponse {
|
||||
map<string, BatchCheckGroupResource> groups = 1;
|
||||
}
|
||||
|
||||
message BatchCheckGroupResource {
|
||||
map<string, bool> items = 1;
|
||||
}
|
||||
@@ -19,9 +19,10 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion8
|
||||
|
||||
const (
|
||||
AuthzExtentionService_List_FullMethodName = "/authz.extention.v1.AuthzExtentionService/List"
|
||||
AuthzExtentionService_Read_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Read"
|
||||
AuthzExtentionService_Write_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Write"
|
||||
AuthzExtentionService_List_FullMethodName = "/authz.extention.v1.AuthzExtentionService/List"
|
||||
AuthzExtentionService_BatchCheck_FullMethodName = "/authz.extention.v1.AuthzExtentionService/BatchCheck"
|
||||
AuthzExtentionService_Read_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Read"
|
||||
AuthzExtentionService_Write_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Write"
|
||||
)
|
||||
|
||||
// AuthzExtentionServiceClient is the client API for AuthzExtentionService service.
|
||||
@@ -29,6 +30,7 @@ const (
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type AuthzExtentionServiceClient interface {
|
||||
List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error)
|
||||
BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error)
|
||||
Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error)
|
||||
Write(ctx context.Context, in *WriteRequest, opts ...grpc.CallOption) (*WriteResponse, error)
|
||||
}
|
||||
@@ -51,6 +53,16 @@ func (c *authzExtentionServiceClient) List(ctx context.Context, in *ListRequest,
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *authzExtentionServiceClient) BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(BatchCheckResponse)
|
||||
err := c.cc.Invoke(ctx, AuthzExtentionService_BatchCheck_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *authzExtentionServiceClient) Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ReadResponse)
|
||||
@@ -76,6 +88,7 @@ func (c *authzExtentionServiceClient) Write(ctx context.Context, in *WriteReques
|
||||
// for forward compatibility
|
||||
type AuthzExtentionServiceServer interface {
|
||||
List(context.Context, *ListRequest) (*ListResponse, error)
|
||||
BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error)
|
||||
Read(context.Context, *ReadRequest) (*ReadResponse, error)
|
||||
Write(context.Context, *WriteRequest) (*WriteResponse, error)
|
||||
}
|
||||
@@ -87,6 +100,9 @@ type UnimplementedAuthzExtentionServiceServer struct {
|
||||
func (UnimplementedAuthzExtentionServiceServer) List(context.Context, *ListRequest) (*ListResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
|
||||
}
|
||||
func (UnimplementedAuthzExtentionServiceServer) BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchCheck not implemented")
|
||||
}
|
||||
func (UnimplementedAuthzExtentionServiceServer) Read(context.Context, *ReadRequest) (*ReadResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Read not implemented")
|
||||
}
|
||||
@@ -123,6 +139,24 @@ func _AuthzExtentionService_List_Handler(srv interface{}, ctx context.Context, d
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AuthzExtentionService_BatchCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(BatchCheckRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AuthzExtentionServiceServer).BatchCheck(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AuthzExtentionService_BatchCheck_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthzExtentionServiceServer).BatchCheck(ctx, req.(*BatchCheckRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AuthzExtentionService_Read_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ReadRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -170,6 +204,10 @@ var AuthzExtentionService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "List",
|
||||
Handler: _AuthzExtentionService_List_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "BatchCheck",
|
||||
Handler: _AuthzExtentionService_BatchCheck_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Read",
|
||||
Handler: _AuthzExtentionService_Read_Handler,
|
||||
|
||||
@@ -16,6 +16,7 @@ type Client interface {
|
||||
List(ctx context.Context, id claims.AuthInfo, req authz.ListRequest) (*authzextv1.ListResponse, error)
|
||||
Read(ctx context.Context, req *authzextv1.ReadRequest) (*authzextv1.ReadResponse, error)
|
||||
Write(ctx context.Context, req *authzextv1.WriteRequest) error
|
||||
BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error)
|
||||
}
|
||||
|
||||
func NewNoopClient() *client.NoopClient {
|
||||
|
||||
@@ -169,3 +169,10 @@ func (c *Client) Write(ctx context.Context, req *authzextv1.WriteRequest) error
|
||||
_, err := c.authzext.Write(ctx, req)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
|
||||
ctx, span := tracer.Start(ctx, "authz.zanzana.client.Check")
|
||||
defer span.End()
|
||||
|
||||
return c.authzext.BatchCheck(ctx, req)
|
||||
}
|
||||
@@ -36,3 +36,7 @@ func (nc NoopClient) Read(ctx context.Context, req *authzextv1.ReadRequest) (*au
|
||||
func (nc NoopClient) Write(ctx context.Context, req *authzextv1.WriteRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nc NoopClient) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
)
|
||||
|
||||
func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
|
||||
ctx, span := tracer.Start(ctx, "authzServer.BatchCheck")
|
||||
defer span.End()
|
||||
|
||||
batchRes := &authzextv1.BatchCheckResponse{
|
||||
Groups: make(map[string]*authzextv1.BatchCheckGroupResource),
|
||||
}
|
||||
|
||||
subject := r.GetSubject()
|
||||
|
||||
for _, item := range r.Items {
|
||||
groupPrefix := common.FormatGroupResource(item.GetGroup(), item.GetResource())
|
||||
allowed, err := s.batchCheckItem(ctx, subject, r.Namespace, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, ok := batchRes.Groups[groupPrefix]; !ok {
|
||||
batchRes.Groups[groupPrefix] = &authzextv1.BatchCheckGroupResource{
|
||||
Items: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
batchRes.Groups[groupPrefix].Items[item.GetName()] = allowed
|
||||
}
|
||||
|
||||
return batchRes, nil
|
||||
}
|
||||
|
||||
func (s *Server) batchCheckItem(ctx context.Context, subject string, namespace string, item *authzextv1.BatchCheckItem) (bool, error) {
|
||||
req := &authzv1.CheckRequest{
|
||||
Namespace: namespace,
|
||||
Subject: subject,
|
||||
Verb: item.GetVerb(),
|
||||
Group: item.GetGroup(),
|
||||
Resource: item.GetResource(),
|
||||
Name: item.GetName(),
|
||||
Folder: item.GetFolder(),
|
||||
Subresource: item.GetSubresource(),
|
||||
}
|
||||
|
||||
var res *authzv1.CheckResponse
|
||||
var err error
|
||||
if info, ok := common.GetTypeInfo(item.GetGroup(), item.GetResource()); ok {
|
||||
res, err = s.checkTyped(ctx, req, info)
|
||||
} else {
|
||||
res, err = s.checkGeneric(ctx, req)
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return res.Allowed, nil
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
)
|
||||
|
||||
func newBatch(subject, group, resource string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest {
|
||||
for i, item := range items {
|
||||
items[i] = &authzextv1.BatchCheckItem{
|
||||
Verb: utils.VerbGet,
|
||||
Group: group,
|
||||
Resource: resource,
|
||||
Name: item.GetName(),
|
||||
Folder: item.GetFolder(),
|
||||
}
|
||||
}
|
||||
|
||||
return &authzextv1.BatchCheckRequest{
|
||||
Namespace: "default",
|
||||
Subject: subject,
|
||||
Items: items,
|
||||
}
|
||||
}
|
||||
|
||||
func testBatchCheck(t *testing.T, server *Server) {
|
||||
t.Run("user:1 should only be able to read resource:dashboards.grafana.app/dashboards/1", func(t *testing.T) {
|
||||
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
|
||||
res, err := server.BatchCheck(context.Background(), newBatch("user:1", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupPrefix].Items, 2)
|
||||
|
||||
assert.True(t, res.Groups[groupPrefix].Items["1"])
|
||||
assert.False(t, res.Groups[groupPrefix].Items["2"])
|
||||
})
|
||||
|
||||
t.Run("user:2 should be able to read resource:dashboards.grafana.app/dashboards/{1,2} through namespace", func(t *testing.T) {
|
||||
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
|
||||
res, err := server.BatchCheck(context.Background(), newBatch("user:2", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, res.Groups[groupPrefix].Items, 2)
|
||||
})
|
||||
|
||||
t.Run("user:3 should be able to read resource:dashboards.grafana.app/dashboards/1 with set relation", func(t *testing.T) {
|
||||
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
|
||||
res, err := server.BatchCheck(context.Background(), newBatch("user:3", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupPrefix].Items, 2)
|
||||
|
||||
assert.True(t, res.Groups[groupPrefix].Items["1"])
|
||||
assert.False(t, res.Groups[groupPrefix].Items["2"])
|
||||
})
|
||||
|
||||
t.Run("user:4 should be able to read all dashboards.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) {
|
||||
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
|
||||
res, err := server.BatchCheck(context.Background(), newBatch("user:4", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "3"},
|
||||
{Name: "3", Folder: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupPrefix].Items, 3)
|
||||
|
||||
assert.True(t, res.Groups[groupPrefix].Items["1"])
|
||||
assert.True(t, res.Groups[groupPrefix].Items["2"])
|
||||
assert.False(t, res.Groups[groupPrefix].Items["3"])
|
||||
})
|
||||
|
||||
t.Run("user:5 should be able to read resource:dashboards.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) {
|
||||
groupPrefix := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
|
||||
res, err := server.BatchCheck(context.Background(), newBatch("user:5", dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupPrefix].Items, 2)
|
||||
|
||||
assert.True(t, res.Groups[groupPrefix].Items["1"])
|
||||
assert.False(t, res.Groups[groupPrefix].Items["2"])
|
||||
})
|
||||
|
||||
t.Run("user:6 should be able to read folder 1", func(t *testing.T) {
|
||||
groupPrefix := zanzana.FormatGroupResource(folderGroup, folderResource)
|
||||
res, err := server.BatchCheck(context.Background(), newBatch("user:6", folderGroup, folderResource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "1"},
|
||||
{Name: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupPrefix].Items, 2)
|
||||
|
||||
assert.True(t, res.Groups[groupPrefix].Items["1"])
|
||||
assert.False(t, res.Groups[groupPrefix].Items["2"])
|
||||
})
|
||||
|
||||
t.Run("user:7 should be able to read folder {1,2} through namespace access", func(t *testing.T) {
|
||||
groupPrefix := zanzana.FormatGroupResource(folderGroup, folderResource)
|
||||
res, err := server.BatchCheck(context.Background(), newBatch("user:7", folderGroup, folderResource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "1"},
|
||||
{Name: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Groups[groupPrefix].Items, 2)
|
||||
})
|
||||
}
|
||||
@@ -20,6 +20,31 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C
|
||||
return s.checkGeneric(ctx, r)
|
||||
}
|
||||
|
||||
// checkNamespace checks if subject has access through namespace
|
||||
func (s *Server) checkNamespace(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.CheckResponse, error) {
|
||||
storeInf, err := s.getStoreInfo(ctx, r.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
relation := common.VerbMapping[r.GetVerb()]
|
||||
|
||||
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
|
||||
StoreId: storeInf.Id,
|
||||
AuthorizationModelId: storeInf.AuthorizationModelId,
|
||||
TupleKey: &openfgav1.CheckRequestTupleKey{
|
||||
User: r.GetSubject(),
|
||||
Relation: relation,
|
||||
Object: common.NewNamespaceResourceIdent(r.GetGroup(), r.GetResource()),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &authzv1.CheckResponse{Allowed: res.GetAllowed()}, nil
|
||||
}
|
||||
|
||||
func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info common.TypeInfo) (*authzv1.CheckResponse, error) {
|
||||
storeInf, err := s.getStoreInfo(ctx, r.Namespace)
|
||||
if err != nil {
|
||||
@@ -47,20 +72,12 @@ func (s *Server) checkTyped(ctx context.Context, r *authzv1.CheckRequest, info c
|
||||
}
|
||||
|
||||
// 2. check if subject has access through namespace
|
||||
res, err = s.openfga.Check(ctx, &openfgav1.CheckRequest{
|
||||
StoreId: storeInf.Id,
|
||||
AuthorizationModelId: storeInf.AuthorizationModelId,
|
||||
TupleKey: &openfgav1.CheckRequestTupleKey{
|
||||
User: r.GetSubject(),
|
||||
Relation: relation,
|
||||
Object: common.NewNamespaceResourceIdent(r.GetGroup(), r.GetResource()),
|
||||
},
|
||||
})
|
||||
nsRes, err := s.checkNamespace(ctx, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &authzv1.CheckResponse{Allowed: res.GetAllowed()}, nil
|
||||
return &authzv1.CheckResponse{Allowed: nsRes.GetAllowed()}, nil
|
||||
}
|
||||
|
||||
func (s *Server) checkGeneric(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.CheckResponse, error) {
|
||||
@@ -96,21 +113,12 @@ func (s *Server) checkGeneric(ctx context.Context, r *authzv1.CheckRequest) (*au
|
||||
}
|
||||
|
||||
// 2. check if subject has access through namespace
|
||||
res, err = s.openfga.Check(ctx, &openfgav1.CheckRequest{
|
||||
StoreId: storeInf.Id,
|
||||
AuthorizationModelId: storeInf.AuthorizationModelId,
|
||||
TupleKey: &openfgav1.CheckRequestTupleKey{
|
||||
User: r.GetSubject(),
|
||||
Relation: relation,
|
||||
Object: common.NewNamespaceResourceIdent(r.GetGroup(), r.GetResource()),
|
||||
},
|
||||
})
|
||||
|
||||
nsRes, err := s.checkNamespace(ctx, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.GetAllowed() {
|
||||
if nsRes.GetAllowed() {
|
||||
return &authzv1.CheckResponse{Allowed: true}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,10 @@ func TestIntegrationServer(t *testing.T) {
|
||||
t.Run("test list", func(t *testing.T) {
|
||||
testList(t, srv)
|
||||
})
|
||||
|
||||
t.Run("test batch check", func(t *testing.T) {
|
||||
testBatchCheck(t, srv)
|
||||
})
|
||||
}
|
||||
|
||||
func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {
|
||||
|
||||
@@ -83,6 +83,8 @@ var (
|
||||
ToOpenFGATuples = common.ToOpenFGATuples
|
||||
ToOpenFGATupleKey = common.ToOpenFGATupleKey
|
||||
ToOpenFGATupleKeyWithoutCondition = common.ToOpenFGATupleKeyWithoutCondition
|
||||
|
||||
FormatGroupResource = common.FormatGroupResource
|
||||
)
|
||||
|
||||
// NewTupleEntry constructs new openfga entry type:name[#relation].
|
||||
@@ -166,6 +168,14 @@ func TranslateToCheckRequest(namespace, action, kind, folder, name string) (*aut
|
||||
return req, true
|
||||
}
|
||||
|
||||
func TranslateToGroupResource(kind string) string {
|
||||
translation, ok := resourceTranslations[kind]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return common.FormatGroupResource(translation.group, translation.resource)
|
||||
}
|
||||
|
||||
func TranslateBasicRole(name string) string {
|
||||
return basicRolesTranslations[name]
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
@@ -59,6 +60,7 @@ type DashboardServiceImpl struct {
|
||||
folderPermissions accesscontrol.FolderPermissionsService
|
||||
dashboardPermissions accesscontrol.DashboardPermissionsService
|
||||
ac accesscontrol.AccessControl
|
||||
zclient zanzana.Client
|
||||
metrics *dashboardsMetrics
|
||||
}
|
||||
|
||||
@@ -67,7 +69,7 @@ func ProvideDashboardServiceImpl(
|
||||
cfg *setting.Cfg, dashboardStore dashboards.Store, folderStore folder.FolderStore,
|
||||
features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService,
|
||||
dashboardPermissionsService accesscontrol.DashboardPermissionsService, ac accesscontrol.AccessControl,
|
||||
folderSvc folder.Service, fStore folder.Store, r prometheus.Registerer,
|
||||
folderSvc folder.Service, fStore folder.Store, r prometheus.Registerer, zclient zanzana.Client,
|
||||
) (*DashboardServiceImpl, error) {
|
||||
dashSvc := &DashboardServiceImpl{
|
||||
cfg: cfg,
|
||||
@@ -77,6 +79,7 @@ func ProvideDashboardServiceImpl(
|
||||
folderPermissions: folderPermissionsService,
|
||||
dashboardPermissions: dashboardPermissionsService,
|
||||
ac: ac,
|
||||
zclient: zclient,
|
||||
folderStore: folderStore,
|
||||
folderService: folderSvc,
|
||||
metrics: newDashboardsMetrics(r),
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -883,6 +884,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc
|
||||
foldertest.NewFakeService(),
|
||||
folder.NewFakeStore(),
|
||||
nil,
|
||||
zanzana.NewNoopClient(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
guardian.InitAccessControlGuardian(cfg, ac, dashboardService)
|
||||
@@ -949,6 +951,7 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt
|
||||
foldertest.NewFakeService(),
|
||||
folder.NewFakeStore(),
|
||||
nil,
|
||||
zanzana.NewNoopClient(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
res, err := service.SaveDashboard(context.Background(), &dto, false)
|
||||
@@ -974,6 +977,7 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto
|
||||
foldertest.NewFakeService(),
|
||||
folder.NewFakeStore(),
|
||||
nil,
|
||||
zanzana.NewNoopClient(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
_, err = service.SaveDashboard(context.Background(), &dto, false)
|
||||
@@ -1018,6 +1022,7 @@ func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string
|
||||
foldertest.NewFakeService(),
|
||||
folder.NewFakeStore(),
|
||||
nil,
|
||||
zanzana.NewNoopClient(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
res, err := service.SaveDashboard(context.Background(), &dto, false)
|
||||
@@ -1069,6 +1074,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da
|
||||
foldertest.NewFakeService(),
|
||||
folder.NewFakeStore(),
|
||||
nil,
|
||||
zanzana.NewNoopClient(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
res, err := service.SaveDashboard(context.Background(), &dto, false)
|
||||
|
||||
@@ -6,9 +6,15 @@ import (
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultQueryLimit = 1000
|
||||
)
|
||||
|
||||
type searchResult struct {
|
||||
runner string
|
||||
result []dashboards.DashboardSearchProjection
|
||||
@@ -79,7 +85,113 @@ func (dr *DashboardServiceImpl) findDashboardsZanzanaCompare(ctx context.Context
|
||||
return first.result, first.err
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) findDashboardsZanzana(_ context.Context, _ dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
|
||||
// FIXME: Implement using the new schema
|
||||
return []dashboards.DashboardSearchProjection{}, nil
|
||||
func (dr *DashboardServiceImpl) findDashboardsZanzana(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
|
||||
return dr.findDashboardsZanzanaCheck(ctx, query)
|
||||
}
|
||||
|
||||
// findDashboardsZanzanaCheck implements "Search, then check" strategy. It first performs search query, then filters out results
|
||||
// by checking access to each item.
|
||||
func (dr *DashboardServiceImpl) findDashboardsZanzanaCheck(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
|
||||
ctx, span := tracer.Start(ctx, "dashboards.service.findDashboardsZanzanaCheck")
|
||||
defer span.End()
|
||||
|
||||
result := make([]dashboards.DashboardSearchProjection, 0, query.Limit)
|
||||
|
||||
query.SkipAccessControlFilter = true
|
||||
// Remember initial query limit
|
||||
limit := query.Limit
|
||||
// Set limit to default to prevent pagination issues
|
||||
query.Limit = defaultQueryLimit
|
||||
if query.Page == 0 {
|
||||
query.Page = 1
|
||||
}
|
||||
|
||||
for len(result) < int(limit) {
|
||||
findRes, err := dr.dashboardStore.FindDashboards(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
remains := limit - int64(len(result))
|
||||
res, err := dr.checkDashboardsBatch(ctx, query, findRes, remains)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result = append(result, res...)
|
||||
query.Page++
|
||||
|
||||
// Stop when last page reached
|
||||
if len(findRes) < defaultQueryLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) checkDashboardsBatch(ctx context.Context, query dashboards.FindPersistedDashboardsQuery, searchRes []dashboards.DashboardSearchProjection, remains int64) ([]dashboards.DashboardSearchProjection, error) {
|
||||
ctx, span := tracer.Start(ctx, "dashboards.service.checkDashboardsBatch")
|
||||
defer span.End()
|
||||
|
||||
if len(searchRes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
batchReqItems := make([]*authzextv1.BatchCheckItem, 0, len(searchRes))
|
||||
|
||||
for _, d := range searchRes {
|
||||
// FIXME: support different access levels
|
||||
kind := zanzana.KindDashboards
|
||||
action := dashboards.ActionDashboardsRead
|
||||
if d.IsFolder {
|
||||
kind = zanzana.KindFolders
|
||||
action = dashboards.ActionFoldersRead
|
||||
}
|
||||
|
||||
checkReq, ok := zanzana.TranslateToCheckRequest("", action, kind, d.FolderUID, d.UID)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
batchReqItems = append(batchReqItems, &authzextv1.BatchCheckItem{
|
||||
Verb: checkReq.Verb,
|
||||
Group: checkReq.Group,
|
||||
Resource: checkReq.Resource,
|
||||
Name: checkReq.Name,
|
||||
Folder: checkReq.Folder,
|
||||
Subresource: checkReq.Subresource,
|
||||
})
|
||||
}
|
||||
|
||||
batchReq := authzextv1.BatchCheckRequest{
|
||||
Namespace: query.SignedInUser.GetNamespace(),
|
||||
Subject: query.SignedInUser.GetUID(),
|
||||
Items: batchReqItems,
|
||||
}
|
||||
|
||||
res, err := dr.zclient.BatchCheck(ctx, &batchReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]dashboards.DashboardSearchProjection, 0)
|
||||
for _, d := range searchRes {
|
||||
if len(result) >= int(remains) {
|
||||
break
|
||||
}
|
||||
|
||||
kind := zanzana.KindDashboards
|
||||
if d.IsFolder {
|
||||
kind = zanzana.KindFolders
|
||||
}
|
||||
groupResource := zanzana.TranslateToGroupResource(kind)
|
||||
if group, ok := res.Groups[groupResource]; ok {
|
||||
if allowed := group.Items[d.UID]; allowed {
|
||||
result = append(result, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashdb "github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
dashsvc "github.com/grafana/grafana/pkg/services/dashboards/service"
|
||||
@@ -99,7 +100,7 @@ func TestValidateDashboardExists(t *testing.T) {
|
||||
secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
|
||||
dashboardStore, err := dashdb.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotatest.New(false, nil))
|
||||
require.NoError(t, err)
|
||||
dashSvc, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashboardStore, folderimpl.ProvideDashboardFolderStore(sqlStore), nil, nil, nil, acmock.New(), foldertest.NewFakeService(), folder.NewFakeStore(), nil)
|
||||
dashSvc, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashboardStore, folderimpl.ProvideDashboardFolderStore(sqlStore), nil, nil, nil, acmock.New(), foldertest.NewFakeService(), folder.NewFakeStore(), nil, zanzana.NewNoopClient())
|
||||
require.NoError(t, err)
|
||||
s := ProvideService(dsStore, secretsService, dashSvc)
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -490,7 +490,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
||||
CanEditValue: true,
|
||||
})
|
||||
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil)
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil, zanzana.NewNoopClient())
|
||||
require.NoError(t, err)
|
||||
|
||||
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, dashSrv, ac, b)
|
||||
@@ -573,7 +573,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
||||
})
|
||||
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOff,
|
||||
folderPermissions, dashboardPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil)
|
||||
folderPermissions, dashboardPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, zanzana.NewNoopClient())
|
||||
require.NoError(t, err)
|
||||
|
||||
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac, b)
|
||||
@@ -719,7 +719,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
||||
tc.service.dashboardStore = dashStore
|
||||
tc.service.store = nestedFolderStore
|
||||
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service, tc.service.store, nil)
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service, tc.service.store, nil, zanzana.NewNoopClient())
|
||||
require.NoError(t, err)
|
||||
alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, dashSrv, ac, b)
|
||||
require.NoError(t, err)
|
||||
@@ -1504,6 +1504,7 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
|
||||
serviceWithFlagOn,
|
||||
nestedFolderStore,
|
||||
nil,
|
||||
zanzana.NewNoopClient(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -310,6 +310,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash
|
||||
foldertest.NewFakeService(),
|
||||
folder.NewFakeStore(),
|
||||
nil,
|
||||
zanzana.NewNoopClient(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
dashboard, err := service.SaveDashboard(context.Background(), dashItem, true)
|
||||
@@ -396,7 +397,7 @@ func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scena
|
||||
cfg, dashboardStore, folderStore,
|
||||
features, folderPermissions, dashboardPermissions, ac,
|
||||
foldertest.NewFakeService(), folder.NewFakeStore(),
|
||||
nil,
|
||||
nil, zanzana.NewNoopClient(),
|
||||
)
|
||||
require.NoError(t, svcErr)
|
||||
guardian.InitAccessControlGuardian(cfg, ac, dashboardService)
|
||||
@@ -458,7 +459,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
|
||||
cfg, dashboardStore, folderStore,
|
||||
features, folderPermissions, dashboardPermissions, ac,
|
||||
foldertest.NewFakeService(), folder.NewFakeStore(),
|
||||
nil,
|
||||
nil, zanzana.NewNoopClient(),
|
||||
)
|
||||
require.NoError(t, dashSvcErr)
|
||||
guardian.InitAccessControlGuardian(cfg, ac, dashService)
|
||||
|
||||
@@ -7,6 +7,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
@@ -19,6 +22,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol/testutil"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service"
|
||||
@@ -38,8 +42,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const userInDbName = "user_in_db"
|
||||
@@ -734,7 +736,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash
|
||||
cfg, dashboardStore, folderStore,
|
||||
featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), dashPermissionService, ac,
|
||||
foldertest.NewFakeService(), folder.NewFakeStore(),
|
||||
nil,
|
||||
nil, zanzana.NewNoopClient(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
dashboard, err := service.SaveDashboard(context.Background(), dashItem, true)
|
||||
@@ -831,7 +833,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
|
||||
cfg, dashStore, folderStore,
|
||||
features, acmock.NewMockedPermissionsService(), dashPermissionService, ac,
|
||||
foldertest.NewFakeService(), folder.NewFakeStore(),
|
||||
nil,
|
||||
nil, zanzana.NewNoopClient(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
guardian.InitAccessControlGuardian(cfg, ac, dashService)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service"
|
||||
@@ -61,7 +62,7 @@ func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.Dashboa
|
||||
cfg, dashboardStore, fs,
|
||||
features, folderPermissions, dashboardPermissions, ac,
|
||||
foldertest.NewFakeService(), folder.NewFakeStore(),
|
||||
nil,
|
||||
nil, zanzana.NewNoopClient(),
|
||||
)
|
||||
require.NoError(tb, err)
|
||||
|
||||
|
||||
@@ -10,12 +10,13 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/service"
|
||||
@@ -324,7 +326,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T)
|
||||
dashService, err := service.ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStoreService, folderStore,
|
||||
featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), dashPermissionService, ac,
|
||||
foldertest.NewFakeService(), folder.NewFakeStore(), nil,
|
||||
foldertest.NewFakeService(), folder.NewFakeStore(), nil, zanzana.NewNoopClient(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -184,13 +184,13 @@ func addAnnotationMig(mg *Migrator) {
|
||||
Postgres("ALTER TABLE annotation ALTER COLUMN tags TYPE VARCHAR(4096);").
|
||||
Mysql("ALTER TABLE annotation MODIFY tags VARCHAR(4096);"))
|
||||
|
||||
mg.AddMigration("Increase prev_state column to length 40", NewRawSQLMigration("").
|
||||
Postgres("ALTER TABLE annotation ALTER COLUMN prev_state TYPE VARCHAR(40);").
|
||||
Mysql("ALTER TABLE annotation MODIFY prev_state VARCHAR(40);"))
|
||||
mg.AddMigration("Increase prev_state column to length 40 not null", NewRawSQLMigration("").
|
||||
Postgres("ALTER TABLE annotation ALTER COLUMN prev_state TYPE VARCHAR(40);"). // Does not modify nullability.
|
||||
Mysql("ALTER TABLE annotation MODIFY prev_state VARCHAR(40) NOT NULL;"))
|
||||
|
||||
mg.AddMigration("Increase new_state column to length 40", NewRawSQLMigration("").
|
||||
Postgres("ALTER TABLE annotation ALTER COLUMN new_state TYPE VARCHAR(40);").
|
||||
Mysql("ALTER TABLE annotation MODIFY new_state VARCHAR(40);"))
|
||||
mg.AddMigration("Increase new_state column to length 40 not null", NewRawSQLMigration("").
|
||||
Postgres("ALTER TABLE annotation ALTER COLUMN new_state TYPE VARCHAR(40);"). // Does not modify nullability.
|
||||
Mysql("ALTER TABLE annotation MODIFY new_state VARCHAR(40) NOT NULL;"))
|
||||
}
|
||||
|
||||
type AddMakeRegionSingleRowMigration struct {
|
||||
|
||||
@@ -77,10 +77,17 @@ func (e *elasticsearchDataQuery) execute() (*backend.QueryDataResponse, error) {
|
||||
e.logger.Info("Prepared request", "queriesLength", len(queries), "duration", time.Since(start), "stage", es.StagePrepareRequest)
|
||||
res, err := e.client.ExecuteMultisearch(req)
|
||||
if err != nil {
|
||||
// We are returning error containing the source that was added through errorsource.Middleware
|
||||
if backend.IsDownstreamHTTPError(err) {
|
||||
err = errorsource.DownstreamError(err, false)
|
||||
}
|
||||
return errorsource.AddErrorToResponse(e.dataQueries[0].RefID, response, err), nil
|
||||
}
|
||||
|
||||
if res.Status >= 400 {
|
||||
errWithSource := errorsource.SourceError(backend.ErrorSourceFromHTTPStatus(res.Status), fmt.Errorf("unexpected status code: %d", res.Status), false)
|
||||
return errorsource.AddErrorToResponse(e.dataQueries[0].RefID, response, errWithSource), nil
|
||||
}
|
||||
|
||||
return parseResponse(e.ctx, res.Responses, queries, e.client.GetConfiguredFields(), e.keepLabelsInResponse, e.logger)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
exp "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource"
|
||||
exphttpclient "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource/httpclient"
|
||||
|
||||
es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
|
||||
)
|
||||
@@ -90,10 +89,7 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins
|
||||
httpCliOpts.SigV4.Service = "es"
|
||||
}
|
||||
|
||||
// set the default middlewars from the httpClientProvider
|
||||
httpCliOpts.Middlewares = httpClientProvider.Opts.Middlewares
|
||||
// enable experimental http client to support errors with source
|
||||
httpCli, err := exphttpclient.New(httpCliOpts)
|
||||
httpCli, err := httpClientProvider.New(httpCliOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -43,7 +44,8 @@ func TestErrorAvgMissingField(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// FIXME: we should return the received error message
|
||||
require.Len(t, result.response.Responses, 0)
|
||||
require.Equal(t, "unexpected status code: 400", result.response.Responses["A"].Error.Error())
|
||||
require.Equal(t, backend.ErrorSourceDownstream, result.response.Responses["A"].ErrorSource)
|
||||
}
|
||||
|
||||
func TestErrorAvgMissingFieldNoDetailedErrors(t *testing.T) {
|
||||
@@ -71,7 +73,7 @@ func TestErrorAvgMissingFieldNoDetailedErrors(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// FIXME: we should return the received error message
|
||||
require.Len(t, result.response.Responses, 0)
|
||||
require.Equal(t, "unexpected status code: 400", result.response.Responses["A"].Error.Error())
|
||||
}
|
||||
|
||||
func TestErrorTooManyDateHistogramBuckets(t *testing.T) {
|
||||
|
||||
@@ -44,7 +44,15 @@ export interface LoadDashboardOptions {
|
||||
uid: string;
|
||||
route: DashboardRoutes;
|
||||
urlFolderUid?: string;
|
||||
queryParams?: UrlQueryMap;
|
||||
params?: {
|
||||
version: number;
|
||||
scopes: string[];
|
||||
timeRange: {
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
variables: UrlQueryMap;
|
||||
};
|
||||
}
|
||||
|
||||
export class DashboardScenePageStateManager extends StateManagerBase<DashboardScenePageState> {
|
||||
@@ -59,11 +67,11 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
|
||||
uid,
|
||||
route,
|
||||
urlFolderUid,
|
||||
queryParams,
|
||||
params,
|
||||
}: LoadDashboardOptions): Promise<DashboardDTO | null> {
|
||||
const cacheKey = route === DashboardRoutes.Home ? HOME_DASHBOARD_CACHE_KEY : uid;
|
||||
|
||||
if (!queryParams) {
|
||||
if (!params) {
|
||||
const cachedDashboard = this.getDashboardFromCache(cacheKey);
|
||||
|
||||
if (cachedDashboard) {
|
||||
@@ -97,6 +105,15 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
|
||||
return await dashboardLoaderSrv.loadDashboard('public', '', uid);
|
||||
}
|
||||
default:
|
||||
const queryParams = params
|
||||
? {
|
||||
version: params.version,
|
||||
scopes: params.scopes,
|
||||
from: params.timeRange.from,
|
||||
to: params.timeRange.to,
|
||||
...params.variables,
|
||||
}
|
||||
: undefined;
|
||||
rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid, queryParams);
|
||||
|
||||
if (route === DashboardRoutes.Embedded) {
|
||||
@@ -188,17 +205,26 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
|
||||
}
|
||||
}
|
||||
|
||||
public async reloadDashboard(queryParams?: LoadDashboardOptions['queryParams'] | undefined) {
|
||||
if (!this.state.options) {
|
||||
public async reloadDashboard(params: LoadDashboardOptions['params']) {
|
||||
const stateOptions = this.state.options;
|
||||
|
||||
if (!stateOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
...this.state.options,
|
||||
queryParams,
|
||||
...stateOptions,
|
||||
params,
|
||||
};
|
||||
|
||||
if (isEqual(options, this.state.options)) {
|
||||
// We shouldn't check all params since:
|
||||
// - version doesn't impact the new dashboard, and it's there for increased compatibility
|
||||
// - time range is almost always different for relative time ranges and absolute time ranges do not trigger subsequent reloads
|
||||
// - other params don't affect the dashboard content
|
||||
if (
|
||||
isEqual(options.params?.variables, stateOptions.params?.variables) &&
|
||||
isEqual(options.params?.scopes, stateOptions.params?.scopes)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { isEqual } from 'lodash';
|
||||
import { debounce, isEqual } from 'lodash';
|
||||
|
||||
import { UrlQueryMap } from '@grafana/data';
|
||||
import { sceneGraph, SceneObjectBase, SceneObjectState, VariableDependencyConfig } from '@grafana/scenes';
|
||||
import { getClosestScopesFacade, ScopesFacade } from 'app/features/scopes';
|
||||
import { getClosestScopesFacade } from 'app/features/scopes';
|
||||
|
||||
import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager';
|
||||
|
||||
@@ -13,27 +13,25 @@ export interface DashboardReloadBehaviorState extends SceneObjectState {
|
||||
}
|
||||
|
||||
export class DashboardReloadBehavior extends SceneObjectBase<DashboardReloadBehaviorState> {
|
||||
private _scopesFacade: ScopesFacade | null = null;
|
||||
|
||||
constructor(state: DashboardReloadBehaviorState) {
|
||||
const shouldReload = state.reloadOnParamsChange && state.uid;
|
||||
|
||||
super(state);
|
||||
|
||||
this.reloadDashboard = this.reloadDashboard.bind(this);
|
||||
// Sometimes the reload is triggered multiple subsequent times
|
||||
// Debouncing it prevents double/triple reloads
|
||||
this.reloadDashboard = debounce(this.reloadDashboard).bind(this);
|
||||
|
||||
if (shouldReload) {
|
||||
this.addActivationHandler(() => {
|
||||
this._scopesFacade = getClosestScopesFacade(this);
|
||||
getClosestScopesFacade(this)?.setState({
|
||||
handler: this.reloadDashboard,
|
||||
});
|
||||
|
||||
this._variableDependency = new VariableDependencyConfig(this, {
|
||||
onAnyVariableChanged: this.reloadDashboard,
|
||||
});
|
||||
|
||||
this._scopesFacade?.setState({
|
||||
handler: this.reloadDashboard,
|
||||
});
|
||||
|
||||
this._subs.add(
|
||||
sceneGraph.getTimeRange(this).subscribeToState((newState, prevState) => {
|
||||
if (!isEqual(newState.value, prevState.value)) {
|
||||
@@ -41,6 +39,8 @@ export class DashboardReloadBehavior extends SceneObjectBase<DashboardReloadBeha
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.reloadDashboard();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -59,21 +59,25 @@ export class DashboardReloadBehavior extends SceneObjectBase<DashboardReloadBeha
|
||||
if (!this.isEditing() && !this.isWaitingForVariables()) {
|
||||
const timeRange = sceneGraph.getTimeRange(this);
|
||||
|
||||
let params: UrlQueryMap = {
|
||||
version: this.state.version,
|
||||
scopes: this._scopesFacade?.value.map((scope) => scope.metadata.name),
|
||||
...timeRange.urlSync?.getUrlState(),
|
||||
};
|
||||
|
||||
params = sceneGraph.getVariables(this).state.variables.reduce<UrlQueryMap>(
|
||||
(acc, variable) => ({
|
||||
...acc,
|
||||
...variable.urlSync?.getUrlState(),
|
||||
}),
|
||||
params
|
||||
);
|
||||
|
||||
getDashboardScenePageStateManager().reloadDashboard(params);
|
||||
// This is wrapped in setTimeout in order to allow variables and scopes to be set in the URL before actually reloading the dashboard
|
||||
setTimeout(() => {
|
||||
getDashboardScenePageStateManager().reloadDashboard({
|
||||
version: this.state.version!,
|
||||
scopes: getClosestScopesFacade(this)?.value.map((scope) => scope.metadata.name) ?? [],
|
||||
// We're not using the getUrlState from timeRange since it makes more sense to pass the absolute timestamps as opposed to relative time
|
||||
timeRange: {
|
||||
from: timeRange.state.value.from.toISOString(),
|
||||
to: timeRange.state.value.to.toISOString(),
|
||||
},
|
||||
variables: sceneGraph.getVariables(this).state.variables.reduce<UrlQueryMap>(
|
||||
(acc, variable) => ({
|
||||
...acc,
|
||||
...variable.urlSync?.getUrlState(),
|
||||
}),
|
||||
{}
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
import { setDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
|
||||
import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager';
|
||||
|
||||
import { enterEditMode, updateMyVar, updateScopes, updateTimeRange } from './utils/actions';
|
||||
import { clearMocks, enterEditMode, updateMyVar, updateScopes, updateTimeRange } from './utils/actions';
|
||||
import { expectDashboardReload, expectNotDashboardReload } from './utils/assertions';
|
||||
import { getDatasource, getInstanceSettings, getMock } from './utils/mocks';
|
||||
import { renderDashboard, resetScenes } from './utils/render';
|
||||
@@ -15,84 +16,66 @@ jest.mock('@grafana/runtime', () => ({
|
||||
usePluginLinks: jest.fn().mockReturnValue({ links: [] }),
|
||||
}));
|
||||
|
||||
const runTest = async (
|
||||
reloadDashboardsOnParamsChange: boolean,
|
||||
reloadOnParamsChange: boolean,
|
||||
withUid: boolean,
|
||||
editMode: boolean
|
||||
) => {
|
||||
config.featureToggles.reloadDashboardsOnParamsChange = reloadDashboardsOnParamsChange;
|
||||
setDashboardAPI(undefined);
|
||||
const uid = 'dash-1';
|
||||
const dashboardScene = renderDashboard({ uid: withUid ? uid : undefined }, { reloadOnParamsChange });
|
||||
|
||||
if (editMode) {
|
||||
await enterEditMode(dashboardScene);
|
||||
}
|
||||
|
||||
const shouldReload = reloadDashboardsOnParamsChange && reloadOnParamsChange && withUid && !editMode;
|
||||
|
||||
await updateTimeRange(dashboardScene);
|
||||
if (!shouldReload) {
|
||||
expectNotDashboardReload();
|
||||
} else {
|
||||
expectDashboardReload();
|
||||
}
|
||||
|
||||
await updateMyVar(dashboardScene, '2');
|
||||
if (!shouldReload) {
|
||||
expectNotDashboardReload();
|
||||
} else {
|
||||
expectDashboardReload();
|
||||
}
|
||||
|
||||
await updateScopes(['grafana']);
|
||||
if (!shouldReload) {
|
||||
expectNotDashboardReload();
|
||||
} else {
|
||||
expectDashboardReload();
|
||||
}
|
||||
};
|
||||
|
||||
describe('Dashboard reload', () => {
|
||||
beforeAll(() => {
|
||||
config.featureToggles.scopeFilters = true;
|
||||
config.featureToggles.groupByVariable = true;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
setDashboardAPI(undefined);
|
||||
await resetScenes();
|
||||
});
|
||||
it.each([
|
||||
[false, false, false, false],
|
||||
[false, false, true, false],
|
||||
[false, true, false, false],
|
||||
[false, true, true, false],
|
||||
[true, false, false, false],
|
||||
[true, false, true, false],
|
||||
[true, true, false, true],
|
||||
[true, true, true, true],
|
||||
[true, true, false, false],
|
||||
[true, true, true, false],
|
||||
])(
|
||||
`reloadDashboardsOnParamsChange: %s, reloadOnParamsChange: %s, withUid: %s, editMode: %s`,
|
||||
async (reloadDashboardsOnParamsChange, reloadOnParamsChange, withUid, editMode) => {
|
||||
config.featureToggles.reloadDashboardsOnParamsChange = reloadDashboardsOnParamsChange;
|
||||
setDashboardAPI(undefined);
|
||||
|
||||
describe('reloadDashboardsOnParamsChange off', () => {
|
||||
describe('reloadOnParamsChange off', () => {
|
||||
it('with UID - no reload', () => runTest(false, false, true, false));
|
||||
it('without UID - no reload', () => runTest(false, false, false, false));
|
||||
});
|
||||
const dashboardScene = renderDashboard({ uid: withUid ? 'dash-1' : undefined }, { reloadOnParamsChange });
|
||||
|
||||
describe('reloadOnParamsChange on', () => {
|
||||
it('with UID - no reload', () => runTest(false, true, true, false));
|
||||
it('without UID - no reload', () => runTest(false, true, false, false));
|
||||
});
|
||||
});
|
||||
if (editMode) {
|
||||
await enterEditMode(dashboardScene);
|
||||
}
|
||||
|
||||
describe('reloadDashboardsOnParamsChange on', () => {
|
||||
describe('reloadOnParamsChange off', () => {
|
||||
it('with UID - no reload', () => runTest(true, false, true, false));
|
||||
it('without UID - no reload', () => runTest(true, false, false, false));
|
||||
});
|
||||
const shouldReload = reloadDashboardsOnParamsChange && reloadOnParamsChange && withUid && !editMode;
|
||||
|
||||
describe('reloadOnParamsChange on', () => {
|
||||
describe('edit mode on', () => {
|
||||
it('with UID - no reload', () => runTest(true, true, true, true));
|
||||
it('without UID - no reload', () => runTest(true, true, false, true));
|
||||
});
|
||||
await updateTimeRange(dashboardScene);
|
||||
await jest.advanceTimersToNextTimerAsync();
|
||||
if (!shouldReload) {
|
||||
expectNotDashboardReload();
|
||||
} else {
|
||||
expectDashboardReload();
|
||||
}
|
||||
|
||||
describe('edit mode off', () => {
|
||||
it('with UID - reload', () => runTest(true, true, true, false));
|
||||
it('without UID - no reload', () => runTest(true, true, false, false));
|
||||
});
|
||||
});
|
||||
});
|
||||
await updateMyVar(dashboardScene, '2');
|
||||
await jest.advanceTimersToNextTimerAsync();
|
||||
if (!shouldReload) {
|
||||
expectNotDashboardReload();
|
||||
} else {
|
||||
expectDashboardReload();
|
||||
}
|
||||
|
||||
await updateScopes(['grafana']);
|
||||
await jest.advanceTimersToNextTimerAsync();
|
||||
if (!shouldReload) {
|
||||
expectNotDashboardReload();
|
||||
} else {
|
||||
expectDashboardReload();
|
||||
}
|
||||
|
||||
getDashboardScenePageStateManager().clearDashboardCache();
|
||||
getDashboardScenePageStateManager().clearSceneCache();
|
||||
setDashboardAPI(undefined);
|
||||
await resetScenes();
|
||||
clearMocks();
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -164,11 +164,9 @@ export class MetricActionBar extends SceneObjectBase<MetricActionBarState> {
|
||||
public openExploreLink = async () => {
|
||||
reportExploreMetrics('selected_metric_action_clicked', { action: 'open_in_explore' });
|
||||
this.getLinkToExplore().then((link) => {
|
||||
// We need to ensure we prefix with the appSubUrl for environments that don't host grafana at the root.
|
||||
const url = `${config.appSubUrl}${link}`;
|
||||
// We use window.open instead of a Link or <a> because we want to compute the explore link when clicking,
|
||||
// if we precompute it we have to keep track of a lot of dependencies
|
||||
window.open(url, '_blank');
|
||||
window.open(link, '_blank');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -22,7 +22,13 @@ export const Aggregation = (props: Props) => {
|
||||
const selected = useSelectedFromOptions(aggOptions, props);
|
||||
|
||||
return (
|
||||
<EditorField label="Group by function" data-testid="cloud-monitoring-aggregation">
|
||||
<EditorField
|
||||
label="Group by function"
|
||||
data-testid="cloud-monitoring-aggregation"
|
||||
tooltip={
|
||||
'Aggregation function used on the metric data. Defaults to none for scalar data and mean for distribution data. Not applying an aggregation to distribution data may lead to performance issues.'
|
||||
}
|
||||
>
|
||||
<Select
|
||||
width="auto"
|
||||
onChange={({ value }) => props.onChange(value!)}
|
||||
|
||||
+56
-3
@@ -9,7 +9,7 @@ import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource';
|
||||
import { createMockMetricDescriptor } from '../__mocks__/cloudMonitoringMetricDescriptor';
|
||||
import { createMockTimeSeriesList } from '../__mocks__/cloudMonitoringQuery';
|
||||
import { PreprocessorType, MetricKind } from '../types/query';
|
||||
import { PreprocessorType, MetricKind, ValueTypes } from '../types/query';
|
||||
|
||||
import { defaultTimeSeriesList } from './MetricQueryEditor';
|
||||
import { VisualMetricQueryEditor } from './VisualMetricQueryEditor';
|
||||
@@ -98,7 +98,11 @@ describe('VisualMetricQueryEditor', () => {
|
||||
replace = (target?: string) => target || '';
|
||||
const onChange = jest.fn();
|
||||
const query = createMockTimeSeriesList();
|
||||
const mockMetricDescriptor = createMockMetricDescriptor({ displayName: 'metricName_test', type: 'test_type' });
|
||||
const mockMetricDescriptor = createMockMetricDescriptor({
|
||||
displayName: 'metricName_test',
|
||||
type: 'test_type',
|
||||
valueType: ValueTypes.DOUBLE,
|
||||
});
|
||||
const datasource = createMockDatasource({
|
||||
getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor(), mockMetricDescriptor]),
|
||||
filterMetricsByType: jest.fn().mockResolvedValue([createMockMetricDescriptor(), mockMetricDescriptor]),
|
||||
@@ -129,7 +133,56 @@ describe('VisualMetricQueryEditor', () => {
|
||||
await select(metricName, 'metricName_test', { container: document.body });
|
||||
});
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ filters: ['metric.type', '=', mockMetricDescriptor.type] })
|
||||
expect.objectContaining({
|
||||
filters: ['metric.type', '=', mockMetricDescriptor.type],
|
||||
crossSeriesReducer: 'REDUCE_NONE',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('can select a metric name with DISTRIBUTION valueType', async () => {
|
||||
replace = (target?: string) => target || '';
|
||||
const onChange = jest.fn();
|
||||
const query = createMockTimeSeriesList();
|
||||
const mockMetricDescriptor = createMockMetricDescriptor({
|
||||
displayName: 'metricName_test',
|
||||
type: 'test_type',
|
||||
valueType: ValueTypes.DISTRIBUTION,
|
||||
});
|
||||
const datasource = createMockDatasource({
|
||||
getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor(), mockMetricDescriptor]),
|
||||
filterMetricsByType: jest.fn().mockResolvedValue([createMockMetricDescriptor(), mockMetricDescriptor]),
|
||||
getLabels: jest.fn().mockResolvedValue([]),
|
||||
});
|
||||
const range = getDefaultTimeRange();
|
||||
|
||||
render(
|
||||
<VisualMetricQueryEditor
|
||||
{...defaultProps}
|
||||
onChange={onChange}
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
range={range}
|
||||
/>
|
||||
);
|
||||
|
||||
const service = await screen.findByLabelText('Service');
|
||||
openMenu(service);
|
||||
await act(async () => {
|
||||
await select(service, 'Srv', { container: document.body });
|
||||
});
|
||||
const metricName = await screen.findByLabelText('Metric name');
|
||||
openMenu(metricName);
|
||||
await userEvent.type(metricName, 'test');
|
||||
await waitFor(() => expect(document.body).toHaveTextContent('metricName_test'));
|
||||
await act(async () => {
|
||||
await select(metricName, 'metricName_test', { container: document.body });
|
||||
});
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
filters: ['metric.type', '=', mockMetricDescriptor.type],
|
||||
crossSeriesReducer: 'REDUCE_MEAN',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -209,6 +209,11 @@ export function Editor({
|
||||
// On metric name change reset query to defaults except project name and filters
|
||||
Object.assign(query, {
|
||||
...defaultTimeSeriesList(datasource),
|
||||
// If the metric value type is DISTRIBUTION use REDUCE_MEAN in order to avoid
|
||||
// returning data frames with a large number of frames (as we return a frame per bucket).
|
||||
// DISTRIBUTION metrics only typically make sense with an aggregation performed against them or
|
||||
// when filtered to a specific label value.
|
||||
crossSeriesReducer: valueType === ValueTypes.DISTRIBUTION ? 'REDUCE_MEAN' : 'REDUCE_NONE',
|
||||
projectName: query.projectName,
|
||||
filters: query.filters,
|
||||
});
|
||||
|
||||
@@ -106,7 +106,8 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
||||
{
|
||||
projectName: this.templateSrv.replace(projectName),
|
||||
groupBys: this.interpolateGroupBys(aggregation?.groupBys || [], {}),
|
||||
crossSeriesReducer: aggregation?.crossSeriesReducer ?? 'REDUCE_NONE',
|
||||
// Use REDUCE_NONE to retrieve all available labels for the metric
|
||||
crossSeriesReducer: 'REDUCE_NONE',
|
||||
view: 'HEADERS',
|
||||
},
|
||||
this.templateSrv.replace(metricType)
|
||||
|
||||
@@ -43,12 +43,12 @@ composableKinds: PanelCfg: {
|
||||
// Unit for the secondary stat to override what ever is set in the data frame.
|
||||
secondaryStatUnit?: string
|
||||
}
|
||||
ZoomMode: "cooperative" | "greedy" @cuetsy(kind="enum")
|
||||
ZoomMode: "cooperative" | "greedy" @cuetsy(kind="enum")
|
||||
Options: {
|
||||
nodes?: NodeOptions
|
||||
edges?: EdgeOptions
|
||||
// How to handle zoom/scroll events in the node graph
|
||||
zoomMode?: ZoomMode
|
||||
// How to handle zoom/scroll events in the node graph
|
||||
zoomMode?: ZoomMode
|
||||
} @cuetsy(kind="interface")
|
||||
}
|
||||
}]
|
||||
|
||||
@@ -45,7 +45,7 @@ composableKinds: PanelCfg: {
|
||||
|
||||
show?: XYShowMode & (*"points" | _)
|
||||
|
||||
pointSize?: {
|
||||
pointSize?: {
|
||||
fixed?: int32 & >=0
|
||||
min?: int32 & >=0
|
||||
max?: int32 & >=0
|
||||
@@ -62,12 +62,12 @@ composableKinds: PanelCfg: {
|
||||
} @cuetsy(kind="interface",TSVeneer="type")
|
||||
|
||||
XYSeriesConfig: {
|
||||
name?: { fixed?: string }
|
||||
frame?: { matcher: #MatcherConfig }
|
||||
x?: { matcher: #MatcherConfig }
|
||||
y?: { matcher: #MatcherConfig }
|
||||
color?: { matcher: #MatcherConfig }
|
||||
size?: { matcher: #MatcherConfig }
|
||||
name?: {fixed?: string}
|
||||
frame?: {matcher: #MatcherConfig}
|
||||
x?: {matcher: #MatcherConfig}
|
||||
y?: {matcher: #MatcherConfig}
|
||||
color?: {matcher: #MatcherConfig}
|
||||
size?: {matcher: #MatcherConfig}
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
Options: {
|
||||
|
||||
@@ -280,6 +280,10 @@
|
||||
"success": ""
|
||||
}
|
||||
},
|
||||
"rule-list": {
|
||||
"configure-datasource": "",
|
||||
"new-alert-rule": ""
|
||||
},
|
||||
"rule-state": {
|
||||
"creating": "",
|
||||
"deleting": "",
|
||||
|
||||
@@ -280,6 +280,10 @@
|
||||
"success": ""
|
||||
}
|
||||
},
|
||||
"rule-list": {
|
||||
"configure-datasource": "",
|
||||
"new-alert-rule": ""
|
||||
},
|
||||
"rule-state": {
|
||||
"creating": "",
|
||||
"deleting": "",
|
||||
|
||||
@@ -280,6 +280,10 @@
|
||||
"success": ""
|
||||
}
|
||||
},
|
||||
"rule-list": {
|
||||
"configure-datasource": "",
|
||||
"new-alert-rule": ""
|
||||
},
|
||||
"rule-state": {
|
||||
"creating": "",
|
||||
"deleting": "",
|
||||
|
||||
@@ -280,6 +280,10 @@
|
||||
"success": ""
|
||||
}
|
||||
},
|
||||
"rule-list": {
|
||||
"configure-datasource": "",
|
||||
"new-alert-rule": ""
|
||||
},
|
||||
"rule-state": {
|
||||
"creating": "",
|
||||
"deleting": "",
|
||||
|
||||
@@ -276,6 +276,10 @@
|
||||
"success": ""
|
||||
}
|
||||
},
|
||||
"rule-list": {
|
||||
"configure-datasource": "",
|
||||
"new-alert-rule": ""
|
||||
},
|
||||
"rule-state": {
|
||||
"creating": "",
|
||||
"deleting": "",
|
||||
|
||||
Reference in new issue
Block a user