Compare commits
4 Commits
search-def
...
sriram/SQL
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40fd558587 | ||
|
|
c5bff2df50 | ||
|
|
c621dbc325 | ||
|
|
ecd3f0b490 |
@@ -13,7 +13,7 @@ import (
|
|||||||
// schema is unexported to prevent accidental overwrites
|
// schema is unexported to prevent accidental overwrites
|
||||||
var (
|
var (
|
||||||
schemaReceiver = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", NewReceiver(), &ReceiverList{}, resource.WithKind("Receiver"),
|
schemaReceiver = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", NewReceiver(), &ReceiverList{}, resource.WithKind("Receiver"),
|
||||||
resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{
|
resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{resource.SelectableField{
|
||||||
FieldSelector: "spec.title",
|
FieldSelector: "spec.title",
|
||||||
FieldValueFunc: func(o resource.Object) (string, error) {
|
FieldValueFunc: func(o resource.Object) (string, error) {
|
||||||
cast, ok := o.(*Receiver)
|
cast, ok := o.(*Receiver)
|
||||||
|
|||||||
@@ -790,6 +790,8 @@ VariableOption: {
|
|||||||
text: string | [...string]
|
text: string | [...string]
|
||||||
// Value of the option
|
// Value of the option
|
||||||
value: string | [...string]
|
value: string | [...string]
|
||||||
|
// Additional properties for multi-props variables
|
||||||
|
properties?: {[string]: string}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query variable specification
|
// Query variable specification
|
||||||
|
|||||||
@@ -794,6 +794,8 @@ VariableOption: {
|
|||||||
text: string | [...string]
|
text: string | [...string]
|
||||||
// Value of the option
|
// Value of the option
|
||||||
value: string | [...string]
|
value: string | [...string]
|
||||||
|
// Additional properties for multi-props variables
|
||||||
|
properties?: {[string]: string}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query variable specification
|
// Query variable specification
|
||||||
|
|||||||
@@ -301,6 +301,8 @@ var _ resource.ListObject = &DashboardList{}
|
|||||||
|
|
||||||
// Copy methods for all subresource types
|
// Copy methods for all subresource types
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// DeepCopy creates a full deep copy of DashboardStatus
|
// DeepCopy creates a full deep copy of DashboardStatus
|
||||||
func (s *DashboardStatus) DeepCopy() *DashboardStatus {
|
func (s *DashboardStatus) DeepCopy() *DashboardStatus {
|
||||||
cpy := &DashboardStatus{}
|
cpy := &DashboardStatus{}
|
||||||
|
|||||||
@@ -301,6 +301,8 @@ var _ resource.ListObject = &DashboardList{}
|
|||||||
|
|
||||||
// Copy methods for all subresource types
|
// Copy methods for all subresource types
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// DeepCopy creates a full deep copy of DashboardStatus
|
// DeepCopy creates a full deep copy of DashboardStatus
|
||||||
func (s *DashboardStatus) DeepCopy() *DashboardStatus {
|
func (s *DashboardStatus) DeepCopy() *DashboardStatus {
|
||||||
cpy := &DashboardStatus{}
|
cpy := &DashboardStatus{}
|
||||||
|
|||||||
@@ -794,6 +794,8 @@ VariableOption: {
|
|||||||
text: string | [...string]
|
text: string | [...string]
|
||||||
// Value of the option
|
// Value of the option
|
||||||
value: string | [...string]
|
value: string | [...string]
|
||||||
|
// Additional properties for multi-props variables
|
||||||
|
properties?: {[string]: string}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query variable specification
|
// Query variable specification
|
||||||
|
|||||||
@@ -1411,6 +1411,8 @@ type DashboardVariableOption struct {
|
|||||||
Text DashboardStringOrArrayOfString `json:"text"`
|
Text DashboardStringOrArrayOfString `json:"text"`
|
||||||
// Value of the option
|
// Value of the option
|
||||||
Value DashboardStringOrArrayOfString `json:"value"`
|
Value DashboardStringOrArrayOfString `json:"value"`
|
||||||
|
// Additional properties for multi-props variables
|
||||||
|
Properties map[string]string `json:"properties,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDashboardVariableOption creates a new DashboardVariableOption object.
|
// NewDashboardVariableOption creates a new DashboardVariableOption object.
|
||||||
|
|||||||
@@ -798,6 +798,8 @@ VariableOption: {
|
|||||||
text: string | [...string]
|
text: string | [...string]
|
||||||
// Value of the option
|
// Value of the option
|
||||||
value: string | [...string]
|
value: string | [...string]
|
||||||
|
// Additional properties for multi-props variables
|
||||||
|
properties?: {[string]: string}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query variable specification
|
// Query variable specification
|
||||||
|
|||||||
@@ -1414,6 +1414,8 @@ type DashboardVariableOption struct {
|
|||||||
Text DashboardStringOrArrayOfString `json:"text"`
|
Text DashboardStringOrArrayOfString `json:"text"`
|
||||||
// Value of the option
|
// Value of the option
|
||||||
Value DashboardStringOrArrayOfString `json:"value"`
|
Value DashboardStringOrArrayOfString `json:"value"`
|
||||||
|
// Additional properties for multi-props variables
|
||||||
|
Properties map[string]string `json:"properties,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDashboardVariableOption creates a new DashboardVariableOption object.
|
// NewDashboardVariableOption creates a new DashboardVariableOption object.
|
||||||
|
|||||||
4
apps/dashboard/pkg/apis/dashboard_manifest.go
generated
4
apps/dashboard/pkg/apis/dashboard_manifest.go
generated
File diff suppressed because one or more lines are too long
@@ -18,6 +18,8 @@ import (
|
|||||||
v1beta1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
v1beta1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ()
|
||||||
|
|
||||||
var appManifestData = app.ManifestData{
|
var appManifestData = app.ManifestData{
|
||||||
AppName: "folder",
|
AppName: "folder",
|
||||||
Group: "folder.grafana.app",
|
Group: "folder.grafana.app",
|
||||||
|
|||||||
@@ -135,12 +135,9 @@ You can use the **Span Limit** field in **Options** section of the TraceQL query
|
|||||||
This field sets the maximum number of spans to return for each span set.
|
This field sets the maximum number of spans to return for each span set.
|
||||||
By default, the maximum value that you can set for the **Span Limit** value (or the spss query) is 100.
|
By default, the maximum value that you can set for the **Span Limit** value (or the spss query) is 100.
|
||||||
In Tempo configuration, this value is controlled by the `max_spans_per_span_set` parameter and can be modified by your Tempo administrator.
|
In Tempo configuration, this value is controlled by the `max_spans_per_span_set` parameter and can be modified by your Tempo administrator.
|
||||||
|
Grafana Cloud users can contact Grafana Support to request a change.
|
||||||
Entering a value higher than the default results in an error.
|
Entering a value higher than the default results in an error.
|
||||||
|
|
||||||
{{< admonition type="note" >}}
|
|
||||||
Changing the value of `max_spans_per_span_set` isn't supported in Grafana Cloud.
|
|
||||||
{{< /admonition >}}
|
|
||||||
|
|
||||||
### Focus on traces or spans
|
### Focus on traces or spans
|
||||||
|
|
||||||
Under **Options**, you can choose to display the table as **Traces** or **Spans** focused.
|
Under **Options**, you can choose to display the table as **Traces** or **Spans** focused.
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -33,14 +33,12 @@ require (
|
|||||||
github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad
|
github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad
|
||||||
github.com/aws/aws-sdk-go v1.55.7 // @grafana/aws-datasources
|
github.com/aws/aws-sdk-go v1.55.7 // @grafana/aws-datasources
|
||||||
github.com/aws/aws-sdk-go-v2 v1.40.0 // @grafana/aws-datasources
|
github.com/aws/aws-sdk-go-v2 v1.40.0 // @grafana/aws-datasources
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // @grafana/grafana-operator-experience-squad
|
|
||||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 // @grafana/aws-datasources
|
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 // @grafana/aws-datasources
|
||||||
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 // @grafana/aws-datasources
|
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 // @grafana/aws-datasources
|
||||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 // @grafana/aws-datasources
|
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 // @grafana/aws-datasources
|
||||||
github.com/aws/aws-sdk-go-v2/service/oam v1.18.3 // @grafana/aws-datasources
|
github.com/aws/aws-sdk-go-v2/service/oam v1.18.3 // @grafana/aws-datasources
|
||||||
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 // @grafana/aws-datasources
|
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.26.6 // @grafana/aws-datasources
|
||||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 // @grafana/grafana-operator-experience-squad
|
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 // @grafana/grafana-operator-experience-squad
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // @grafana/grafana-operator-experience-squad
|
|
||||||
github.com/aws/smithy-go v1.23.2 // @grafana/aws-datasources
|
github.com/aws/smithy-go v1.23.2 // @grafana/aws-datasources
|
||||||
github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group
|
github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group
|
||||||
github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend
|
github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend
|
||||||
@@ -345,6 +343,7 @@ require (
|
|||||||
github.com/at-wat/mqtt-go v0.19.6 // indirect
|
github.com/at-wat/mqtt-go v0.19.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.31.17 // indirect
|
github.com/aws/aws-sdk-go-v2/config v1.31.17 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
|
||||||
@@ -359,6 +358,7 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 // indirect
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect
|
||||||
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 // indirect
|
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 // indirect
|
||||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
|
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
|
||||||
|
|||||||
@@ -165,17 +165,9 @@ describe('DateMath', () => {
|
|||||||
expect(date!.valueOf()).toEqual(dateTime([2014, 1, 3]).valueOf());
|
expect(date!.valueOf()).toEqual(dateTime([2014, 1, 3]).valueOf());
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it('should handle multiple math expressions', () => {
|
||||||
['-2d-6h', [2014, 1, 5], [2014, 1, 2, 18]],
|
const date = dateMath.parseDateMath('-2d-6h', dateTime([2014, 1, 5]));
|
||||||
['-30m-2d', [2014, 1, 5], [2014, 1, 2, 23, 30]],
|
expect(date!.valueOf()).toEqual(dateTime([2014, 1, 2, 18]).valueOf());
|
||||||
['-2d-1d', [2014, 1, 5], [2014, 1, 2]],
|
|
||||||
['-1h-30m', [2014, 1, 5, 12, 0], [2014, 1, 5, 10, 30]],
|
|
||||||
['-1d-1h-30m', [2014, 1, 5, 12, 0], [2014, 1, 4, 10, 30]],
|
|
||||||
['+1d-6h', [2014, 1, 5], [2014, 1, 5, 18]],
|
|
||||||
['-1w-1d', [2014, 1, 14], [2014, 1, 6]],
|
|
||||||
])('should handle multiple math expressions: %s', (expression, inputDate, expectedDate) => {
|
|
||||||
const date = dateMath.parseDateMath(expression, dateTime(inputDate));
|
|
||||||
expect(date!.valueOf()).toEqual(dateTime(expectedDate).valueOf());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when invalid expression', () => {
|
it('should return false when invalid expression', () => {
|
||||||
|
|||||||
174
packages/grafana-sql/src/SQLVariableSupport.tsx
Normal file
174
packages/grafana-sql/src/SQLVariableSupport.tsx
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CustomVariableSupport,
|
||||||
|
DataQueryRequest,
|
||||||
|
DataQueryResponse,
|
||||||
|
QueryEditorProps,
|
||||||
|
Field,
|
||||||
|
DataFrame,
|
||||||
|
MetricFindValue,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { t } from '@grafana/i18n';
|
||||||
|
import { EditorMode, EditorRows, EditorRow, EditorField } from '@grafana/plugin-ui';
|
||||||
|
import { Combobox, ComboboxOption } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { SqlQueryEditorLazy } from './components/QueryEditorLazy';
|
||||||
|
import { SqlDatasource } from './datasource/SqlDatasource';
|
||||||
|
import { applyQueryDefaults } from './defaults';
|
||||||
|
import { QueryFormat, type SQLQuery, type SQLOptions, type SQLQueryMeta } from './types';
|
||||||
|
|
||||||
|
type SQLVariableQuery = { query: string } & SQLQuery;
|
||||||
|
|
||||||
|
const refId = 'SQLVariableQueryEditor-VariableQuery';
|
||||||
|
|
||||||
|
export class SQLVariableSupport extends CustomVariableSupport<SqlDatasource, SQLQuery> {
|
||||||
|
constructor(readonly datasource: SqlDatasource) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
editor = SQLVariablesQueryEditor;
|
||||||
|
query(request: DataQueryRequest<SQLQuery>): Observable<DataQueryResponse> {
|
||||||
|
if (request.targets.length < 1) {
|
||||||
|
throw new Error('no variable query found');
|
||||||
|
}
|
||||||
|
const updatedQuery = migrateVariableQuery(request.targets[0]);
|
||||||
|
return this.datasource.query({ ...request, targets: [updatedQuery] }).pipe(
|
||||||
|
map((d: DataQueryResponse) => {
|
||||||
|
const frames = d.data || [];
|
||||||
|
const metricFindValues = convertDataFramesToMetricFindValues(frames, updatedQuery.meta);
|
||||||
|
return { data: metricFindValues };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
getDefaultQuery(): Partial<SQLQuery> {
|
||||||
|
return applyQueryDefaults({ refId, editorMode: EditorMode.Builder, format: QueryFormat.Table });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SQLVariableQueryEditorProps = QueryEditorProps<SqlDatasource, SQLQuery, SQLOptions>;
|
||||||
|
|
||||||
|
const SQLVariablesQueryEditor = (props: SQLVariableQueryEditorProps) => {
|
||||||
|
const query = migrateVariableQuery(props.query);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SqlQueryEditorLazy {...props} query={query} />
|
||||||
|
<FieldMapping {...props} query={query} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FieldMapping = (props: SQLVariableQueryEditorProps) => {
|
||||||
|
const { query, datasource, onChange } = props;
|
||||||
|
const [choices, setChoices] = useState<ComboboxOption[]>([]);
|
||||||
|
useEffect(() => {
|
||||||
|
let isActive = true;
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const subscription = datasource.query({ targets: [query] } as DataQueryRequest<SQLQuery>).subscribe({
|
||||||
|
next: (response) => {
|
||||||
|
if (!isActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fieldNames = (response.data[0] || { fields: [] }).fields.map((f: Field) => f.name);
|
||||||
|
setChoices(fieldNames.map((f: Field) => ({ value: f, label: f })));
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
if (isActive) {
|
||||||
|
setChoices([]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
isActive = false;
|
||||||
|
subscription.unsubscribe();
|
||||||
|
};
|
||||||
|
}, [datasource, query]);
|
||||||
|
const onMetaPropChange = <Key extends keyof SQLQueryMeta, Value extends SQLQueryMeta[Key]>(
|
||||||
|
key: Key,
|
||||||
|
value: Value,
|
||||||
|
meta = query.meta || {}
|
||||||
|
) => {
|
||||||
|
onChange({ ...query, meta: { ...meta, [key]: value } });
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<EditorRows>
|
||||||
|
<EditorRow>
|
||||||
|
<EditorField label={t('grafana-sql.components.query-meta.variables.valueField', 'Value Field')}>
|
||||||
|
<Combobox
|
||||||
|
isClearable
|
||||||
|
value={query.meta?.valueField}
|
||||||
|
onChange={(e) => onMetaPropChange('valueField', e?.value)}
|
||||||
|
width={40}
|
||||||
|
options={choices}
|
||||||
|
/>
|
||||||
|
</EditorField>
|
||||||
|
<EditorField label={t('grafana-sql.components.query-meta.variables.textField', 'Text Field')}>
|
||||||
|
<Combobox
|
||||||
|
isClearable
|
||||||
|
value={query.meta?.textField}
|
||||||
|
onChange={(e) => onMetaPropChange('textField', e?.value)}
|
||||||
|
width={40}
|
||||||
|
options={choices}
|
||||||
|
/>
|
||||||
|
</EditorField>
|
||||||
|
</EditorRow>
|
||||||
|
</EditorRows>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrateVariableQuery = (rawQuery: string | SQLQuery): SQLVariableQuery => {
|
||||||
|
if (typeof rawQuery !== 'string') {
|
||||||
|
return {
|
||||||
|
...rawQuery,
|
||||||
|
refId: rawQuery.refId || refId,
|
||||||
|
query: rawQuery.rawSql || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...applyQueryDefaults({
|
||||||
|
refId,
|
||||||
|
rawSql: rawQuery,
|
||||||
|
editorMode: rawQuery ? EditorMode.Code : EditorMode.Builder,
|
||||||
|
}),
|
||||||
|
query: rawQuery,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertDataFramesToMetricFindValues = (frames: DataFrame[], meta?: SQLQueryMeta): MetricFindValue[] => {
|
||||||
|
if (!frames.length) {
|
||||||
|
throw new Error('no results found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const frame = frames[0];
|
||||||
|
|
||||||
|
const fields = frame.fields;
|
||||||
|
|
||||||
|
if (fields.length < 1) {
|
||||||
|
throw new Error('no fields found in the response');
|
||||||
|
}
|
||||||
|
|
||||||
|
let textField = fields.find((f) => f.name === '__text');
|
||||||
|
let valueField = fields.find((f) => f.name === '__value');
|
||||||
|
if (meta?.textField) {
|
||||||
|
textField = fields.find((f) => f.name === meta.textField);
|
||||||
|
}
|
||||||
|
if (meta?.valueField) {
|
||||||
|
valueField = fields.find((f) => f.name === meta.valueField);
|
||||||
|
}
|
||||||
|
const resolvedTextField = textField || valueField || fields[0];
|
||||||
|
const resolvedValueField = valueField || textField || fields[0];
|
||||||
|
|
||||||
|
const results: MetricFindValue[] = [];
|
||||||
|
const rowCount = frame.length;
|
||||||
|
for (let i = 0; i < rowCount; i++) {
|
||||||
|
const text = String(resolvedTextField.values[i] ?? '');
|
||||||
|
const value = String(resolvedValueField.values[i] ?? '');
|
||||||
|
const properties: Record<string, string> = {};
|
||||||
|
for (const field of fields) {
|
||||||
|
properties[field.name] = String(field.values[i] ?? '');
|
||||||
|
}
|
||||||
|
results.push({ text, value, properties });
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
};
|
||||||
@@ -21,6 +21,7 @@ export { TLSSecretsConfig } from './components/configuration/TLSSecretsConfig';
|
|||||||
export { useMigrateDatabaseFields } from './components/configuration/useMigrateDatabaseFields';
|
export { useMigrateDatabaseFields } from './components/configuration/useMigrateDatabaseFields';
|
||||||
export { SqlQueryEditorLazy } from './components/QueryEditorLazy';
|
export { SqlQueryEditorLazy } from './components/QueryEditorLazy';
|
||||||
export type { QueryHeaderProps } from './components/QueryHeader';
|
export type { QueryHeaderProps } from './components/QueryHeader';
|
||||||
|
export { SQLVariableSupport } from './SQLVariableSupport';
|
||||||
export { createSelectClause, haveColumns } from './utils/sql.utils';
|
export { createSelectClause, haveColumns } from './utils/sql.utils';
|
||||||
export { applyQueryDefaults } from './defaults';
|
export { applyQueryDefaults } from './defaults';
|
||||||
export { makeVariable } from './utils/testHelpers';
|
export { makeVariable } from './utils/testHelpers';
|
||||||
|
|||||||
@@ -69,6 +69,12 @@
|
|||||||
"placeholder-select-format": "Select format",
|
"placeholder-select-format": "Select format",
|
||||||
"run-query": "Run query"
|
"run-query": "Run query"
|
||||||
},
|
},
|
||||||
|
"query-meta": {
|
||||||
|
"variables": {
|
||||||
|
"textField": "Text Field",
|
||||||
|
"valueField": "Value Field"
|
||||||
|
}
|
||||||
|
},
|
||||||
"query-toolbox": {
|
"query-toolbox": {
|
||||||
"content-hit-ctrlcmdreturn-to-run-query": "Hit CTRL/CMD+Return to run query",
|
"content-hit-ctrlcmdreturn-to-run-query": "Hit CTRL/CMD+Return to run query",
|
||||||
"tooltip-collapse": "Collapse editor",
|
"tooltip-collapse": "Collapse editor",
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ export enum QueryFormat {
|
|||||||
Table = 'table',
|
Table = 'table',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SQLQueryMeta = { valueField?: string; textField?: string };
|
||||||
|
|
||||||
export interface SQLQuery extends DataQuery {
|
export interface SQLQuery extends DataQuery {
|
||||||
alias?: string;
|
alias?: string;
|
||||||
format?: QueryFormat;
|
format?: QueryFormat;
|
||||||
@@ -59,6 +61,7 @@ export interface SQLQuery extends DataQuery {
|
|||||||
sql?: SQLExpression;
|
sql?: SQLExpression;
|
||||||
editorMode?: EditorMode;
|
editorMode?: EditorMode;
|
||||||
rawQuery?: boolean;
|
rawQuery?: boolean;
|
||||||
|
meta?: SQLQueryMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NameValue {
|
export interface NameValue {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useId, memo, HTMLAttributes, SVGProps } from 'react';
|
import { useId, memo, HTMLAttributes, ReactNode, SVGProps } from 'react';
|
||||||
|
|
||||||
import { FieldDisplay } from '@grafana/data';
|
import { FieldDisplay } from '@grafana/data';
|
||||||
|
|
||||||
import { RadialArcPathEndpointMarks } from './RadialArcPathEndpointMarks';
|
import { getBarEndcapColors, getGradientCss, getEndpointMarkerColors } from './colors';
|
||||||
import { getBarEndcapColors, getGradientCss } from './colors';
|
|
||||||
import { RadialShape, RadialGaugeDimensions, GradientStop } from './types';
|
import { RadialShape, RadialGaugeDimensions, GradientStop } from './types';
|
||||||
import { drawRadialArcPath, toRad } from './utils';
|
import { drawRadialArcPath, toRad } from './utils';
|
||||||
|
|
||||||
@@ -30,6 +29,11 @@ interface RadialArcPathPropsWithGradient extends RadialArcPathPropsBase {
|
|||||||
|
|
||||||
type RadialArcPathProps = RadialArcPathPropsWithColor | RadialArcPathPropsWithGradient;
|
type RadialArcPathProps = RadialArcPathPropsWithColor | RadialArcPathPropsWithGradient;
|
||||||
|
|
||||||
|
const ENDPOINT_MARKER_MIN_ANGLE = 10;
|
||||||
|
const DOT_OPACITY = 0.5;
|
||||||
|
const DOT_RADIUS_FACTOR = 0.4;
|
||||||
|
const MAX_DOT_RADIUS = 8;
|
||||||
|
|
||||||
export const RadialArcPath = memo(
|
export const RadialArcPath = memo(
|
||||||
({
|
({
|
||||||
arcLengthDeg,
|
arcLengthDeg,
|
||||||
@@ -64,25 +68,67 @@ export const RadialArcPath = memo(
|
|||||||
const xEnd = centerX + radius * Math.cos(endRadians);
|
const xEnd = centerX + radius * Math.cos(endRadians);
|
||||||
const yEnd = centerY + radius * Math.sin(endRadians);
|
const yEnd = centerY + radius * Math.sin(endRadians);
|
||||||
|
|
||||||
|
const dotRadius =
|
||||||
|
endpointMarker === 'point' ? Math.min((barWidth / 2) * DOT_RADIUS_FACTOR, MAX_DOT_RADIUS) : barWidth / 2;
|
||||||
|
|
||||||
const bgDivStyle: HTMLAttributes<HTMLDivElement>['style'] = { width: boxSize, height: vizHeight, marginLeft: boxX };
|
const bgDivStyle: HTMLAttributes<HTMLDivElement>['style'] = { width: boxSize, height: vizHeight, marginLeft: boxX };
|
||||||
|
|
||||||
const pathProps: SVGProps<SVGPathElement> = {};
|
const pathProps: SVGProps<SVGPathElement> = {};
|
||||||
|
let barEndcapColors: [string, string] | undefined;
|
||||||
|
let endpointMarks: ReactNode = null;
|
||||||
if (isGradient) {
|
if (isGradient) {
|
||||||
bgDivStyle.backgroundImage = getGradientCss(rest.gradient, shape);
|
bgDivStyle.backgroundImage = getGradientCss(rest.gradient, shape);
|
||||||
|
|
||||||
|
if (endpointMarker && (rest.gradient?.length ?? 0) > 0) {
|
||||||
|
switch (endpointMarker) {
|
||||||
|
case 'point':
|
||||||
|
const [pointColorStart, pointColorEnd] = getEndpointMarkerColors(
|
||||||
|
rest.gradient!,
|
||||||
|
fieldDisplay.display.percent
|
||||||
|
);
|
||||||
|
endpointMarks = (
|
||||||
|
<>
|
||||||
|
{arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE && (
|
||||||
|
<circle cx={xStart} cy={yStart} r={dotRadius} fill={pointColorStart} opacity={DOT_OPACITY} />
|
||||||
|
)}
|
||||||
|
<circle cx={xEnd} cy={yEnd} r={dotRadius} fill={pointColorEnd} opacity={DOT_OPACITY} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'glow':
|
||||||
|
const offsetAngle = toRad(ENDPOINT_MARKER_MIN_ANGLE);
|
||||||
|
const xStartMark = centerX + radius * Math.cos(endRadians + offsetAngle);
|
||||||
|
const yStartMark = centerY + radius * Math.sin(endRadians + offsetAngle);
|
||||||
|
endpointMarks =
|
||||||
|
arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE ? (
|
||||||
|
<path
|
||||||
|
d={['M', xStartMark, yStartMark, 'A', radius, radius, 0, 0, 1, xEnd, yEnd].join(' ')}
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={barWidth}
|
||||||
|
stroke={endpointMarkerGlowFilter}
|
||||||
|
strokeLinecap={roundedBars ? 'round' : 'butt'}
|
||||||
|
filter={glowFilter}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (barEndcaps) {
|
||||||
|
barEndcapColors = getBarEndcapColors(rest.gradient, fieldDisplay.display.percent);
|
||||||
|
}
|
||||||
|
|
||||||
pathProps.fill = 'none';
|
pathProps.fill = 'none';
|
||||||
pathProps.stroke = 'white';
|
pathProps.stroke = 'white';
|
||||||
} else {
|
} else {
|
||||||
bgDivStyle.backgroundColor = rest.color;
|
bgDivStyle.backgroundColor = rest.color;
|
||||||
|
|
||||||
pathProps.fill = 'none';
|
pathProps.fill = 'none';
|
||||||
pathProps.stroke = rest.color;
|
pathProps.stroke = rest.color;
|
||||||
}
|
}
|
||||||
|
|
||||||
let barEndcapColors: [string, string] | undefined;
|
|
||||||
if (barEndcaps) {
|
|
||||||
barEndcapColors = isGradient
|
|
||||||
? getBarEndcapColors(rest.gradient, fieldDisplay.display.percent)
|
|
||||||
: [rest.color, rest.color];
|
|
||||||
}
|
|
||||||
|
|
||||||
const pathEl = (
|
const pathEl = (
|
||||||
<path d={path} strokeWidth={barWidth} strokeLinecap={roundedBars ? 'round' : 'butt'} {...pathProps} />
|
<path d={path} strokeWidth={barWidth} strokeLinecap={roundedBars ? 'round' : 'butt'} {...pathProps} />
|
||||||
);
|
);
|
||||||
@@ -112,23 +158,7 @@ export const RadialArcPath = memo(
|
|||||||
)}
|
)}
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
{endpointMarker && (
|
{endpointMarks}
|
||||||
<RadialArcPathEndpointMarks
|
|
||||||
startAngle={angle}
|
|
||||||
arcLengthDeg={arcLengthDeg}
|
|
||||||
dimensions={dimensions}
|
|
||||||
endpointMarker={endpointMarker}
|
|
||||||
fieldDisplay={fieldDisplay}
|
|
||||||
xStart={xStart}
|
|
||||||
xEnd={xEnd}
|
|
||||||
yStart={yStart}
|
|
||||||
yEnd={yEnd}
|
|
||||||
roundedBars={roundedBars}
|
|
||||||
endpointMarkerGlowFilter={endpointMarkerGlowFilter}
|
|
||||||
glowFilter={glowFilter}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,143 +0,0 @@
|
|||||||
import { render, RenderResult } from '@testing-library/react';
|
|
||||||
|
|
||||||
import { FieldDisplay } from '@grafana/data';
|
|
||||||
|
|
||||||
import { RadialArcPathEndpointMarks, RadialArcPathEndpointMarksProps } from './RadialArcPathEndpointMarks';
|
|
||||||
import { RadialGaugeDimensions } from './types';
|
|
||||||
|
|
||||||
const ser = new XMLSerializer();
|
|
||||||
|
|
||||||
const expectHTML = (result: RenderResult, expected: string) => {
|
|
||||||
let actual = ser.serializeToString(result.asFragment()).replace(/xmlns=".*?" /g, '');
|
|
||||||
expect(actual).toEqual(expected.replace(/^\s*|\n/gm, ''));
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('RadialArcPathEndpointMarks', () => {
|
|
||||||
const defaultDimensions = Object.freeze({
|
|
||||||
centerX: 100,
|
|
||||||
centerY: 100,
|
|
||||||
radius: 80,
|
|
||||||
barWidth: 20,
|
|
||||||
vizWidth: 200,
|
|
||||||
vizHeight: 200,
|
|
||||||
margin: 10,
|
|
||||||
barIndex: 0,
|
|
||||||
thresholdsBarRadius: 0,
|
|
||||||
thresholdsBarWidth: 0,
|
|
||||||
thresholdsBarSpacing: 0,
|
|
||||||
scaleLabelsFontSize: 0,
|
|
||||||
scaleLabelsSpacing: 0,
|
|
||||||
scaleLabelsRadius: 0,
|
|
||||||
gaugeBottomY: 0,
|
|
||||||
}) satisfies RadialGaugeDimensions;
|
|
||||||
|
|
||||||
const defaultFieldDisplay = Object.freeze({
|
|
||||||
name: 'Test',
|
|
||||||
field: {},
|
|
||||||
display: { text: '50', numeric: 50, color: '#FF0000' },
|
|
||||||
hasLinks: false,
|
|
||||||
}) satisfies FieldDisplay;
|
|
||||||
|
|
||||||
const defaultProps = Object.freeze({
|
|
||||||
arcLengthDeg: 90,
|
|
||||||
dimensions: defaultDimensions,
|
|
||||||
fieldDisplay: defaultFieldDisplay,
|
|
||||||
startAngle: 0,
|
|
||||||
xStart: 100,
|
|
||||||
xEnd: 150,
|
|
||||||
yStart: 100,
|
|
||||||
yEnd: 50,
|
|
||||||
}) satisfies Omit<RadialArcPathEndpointMarksProps, 'color' | 'gradient' | 'endpointMarker'>;
|
|
||||||
|
|
||||||
it('renders the expected marks when endpointMarker is "point" w/ a static color', () => {
|
|
||||||
expectHTML(
|
|
||||||
render(
|
|
||||||
<svg role="img">
|
|
||||||
<RadialArcPathEndpointMarks {...defaultProps} endpointMarker="point" color="#FF0000" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
'<svg role=\"img\"><circle cx=\"100\" cy=\"100\" r=\"4\" fill=\"#111217\" opacity=\"0.5\"/><circle cx=\"150\" cy=\"50\" r=\"4\" fill=\"#111217\" opacity=\"0.5\"/></svg>'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders the expected marks when endpointMarker is "point" w/ a gradient color', () => {
|
|
||||||
expectHTML(
|
|
||||||
render(
|
|
||||||
<svg role="img">
|
|
||||||
<RadialArcPathEndpointMarks
|
|
||||||
{...defaultProps}
|
|
||||||
endpointMarker="point"
|
|
||||||
gradient={[
|
|
||||||
{ color: '#00FF00', percent: 0 },
|
|
||||||
{ color: '#0000FF', percent: 1 },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
'<svg role=\"img\"><circle cx=\"100\" cy=\"100\" r=\"4\" fill=\"#111217\" opacity=\"0.5\"/><circle cx=\"150\" cy=\"50\" r=\"4\" fill=\"#fbfbfb\" opacity=\"0.5\"/></svg>'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders the expected marks when endpointMarker is "glow" w/ a static color', () => {
|
|
||||||
expectHTML(
|
|
||||||
render(
|
|
||||||
<svg role="img">
|
|
||||||
<RadialArcPathEndpointMarks {...defaultProps} endpointMarker="glow" color="#FF0000" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
'<svg role=\"img\"><path d=\"M 113.89185421335443 21.215379759023364 A 80 80 0 0 1 150 50\" fill=\"none\" stroke-width=\"20\" stroke-linecap=\"butt\"/></svg>'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders the expected marks when endpointMarker is "glow" w/ a gradient color', () => {
|
|
||||||
expectHTML(
|
|
||||||
render(
|
|
||||||
<svg role="img">
|
|
||||||
<RadialArcPathEndpointMarks
|
|
||||||
{...defaultProps}
|
|
||||||
endpointMarker="glow"
|
|
||||||
gradient={[
|
|
||||||
{ color: '#00FF00', percent: 0 },
|
|
||||||
{ color: '#0000FF', percent: 1 },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
'<svg role=\"img\"><path d=\"M 113.89185421335443 21.215379759023364 A 80 80 0 0 1 150 50\" fill=\"none\" stroke-width=\"20\" stroke-linecap=\"butt\"/></svg>'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not render the start mark when arcLengthDeg is less than the minimum angle for "point" endpointMarker', () => {
|
|
||||||
expectHTML(
|
|
||||||
render(
|
|
||||||
<svg role="img">
|
|
||||||
<RadialArcPathEndpointMarks {...defaultProps} arcLengthDeg={5} endpointMarker="point" color="#FF0000" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
'<svg role=\"img\"><circle cx=\"150\" cy=\"50\" r=\"4\" fill=\"#111217\" opacity=\"0.5\"/></svg>'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not render anything when arcLengthDeg is less than the minimum angle for "glow" endpointMarker', () => {
|
|
||||||
expectHTML(
|
|
||||||
render(
|
|
||||||
<svg role="img">
|
|
||||||
<RadialArcPathEndpointMarks {...defaultProps} arcLengthDeg={5} endpointMarker="glow" color="#FF0000" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
'<svg role=\"img\"/>'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not render anything if endpointMarker is some other value', () => {
|
|
||||||
expectHTML(
|
|
||||||
render(
|
|
||||||
<svg role="img">
|
|
||||||
{/* @ts-ignore: confirming the component doesn't throw */}
|
|
||||||
<RadialArcPathEndpointMarks {...defaultProps} endpointMarker="foo" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
'<svg role=\"img\"/>'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import { FieldDisplay } from '@grafana/data';
|
|
||||||
|
|
||||||
import { getEndpointMarkerColors, getGuideDotColor } from './colors';
|
|
||||||
import { GradientStop, RadialGaugeDimensions } from './types';
|
|
||||||
import { toRad } from './utils';
|
|
||||||
|
|
||||||
interface RadialArcPathEndpointMarksPropsBase {
|
|
||||||
arcLengthDeg: number;
|
|
||||||
dimensions: RadialGaugeDimensions;
|
|
||||||
fieldDisplay: FieldDisplay;
|
|
||||||
endpointMarker: 'point' | 'glow';
|
|
||||||
roundedBars?: boolean;
|
|
||||||
startAngle: number;
|
|
||||||
glowFilter?: string;
|
|
||||||
endpointMarkerGlowFilter?: string;
|
|
||||||
xStart: number;
|
|
||||||
xEnd: number;
|
|
||||||
yStart: number;
|
|
||||||
yEnd: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RadialArcPathEndpointMarksPropsWithColor extends RadialArcPathEndpointMarksPropsBase {
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RadialArcPathEndpointMarksPropsWithGradient extends RadialArcPathEndpointMarksPropsBase {
|
|
||||||
gradient: GradientStop[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RadialArcPathEndpointMarksProps =
|
|
||||||
| RadialArcPathEndpointMarksPropsWithColor
|
|
||||||
| RadialArcPathEndpointMarksPropsWithGradient;
|
|
||||||
|
|
||||||
const ENDPOINT_MARKER_MIN_ANGLE = 10;
|
|
||||||
const DOT_OPACITY = 0.5;
|
|
||||||
const DOT_RADIUS_FACTOR = 0.4;
|
|
||||||
const MAX_DOT_RADIUS = 8;
|
|
||||||
|
|
||||||
export function RadialArcPathEndpointMarks({
|
|
||||||
startAngle: angle,
|
|
||||||
arcLengthDeg,
|
|
||||||
dimensions,
|
|
||||||
endpointMarker,
|
|
||||||
fieldDisplay,
|
|
||||||
xStart,
|
|
||||||
xEnd,
|
|
||||||
yStart,
|
|
||||||
yEnd,
|
|
||||||
roundedBars,
|
|
||||||
endpointMarkerGlowFilter,
|
|
||||||
glowFilter,
|
|
||||||
...rest
|
|
||||||
}: RadialArcPathEndpointMarksProps) {
|
|
||||||
const isGradient = 'gradient' in rest;
|
|
||||||
const { radius, centerX, centerY, barWidth } = dimensions;
|
|
||||||
const endRadians = toRad(angle + arcLengthDeg);
|
|
||||||
|
|
||||||
switch (endpointMarker) {
|
|
||||||
case 'point': {
|
|
||||||
const [pointColorStart, pointColorEnd] = isGradient
|
|
||||||
? getEndpointMarkerColors(rest.gradient, fieldDisplay.display.percent)
|
|
||||||
: [getGuideDotColor(rest.color), getGuideDotColor(rest.color)];
|
|
||||||
|
|
||||||
const dotRadius =
|
|
||||||
endpointMarker === 'point' ? Math.min((barWidth / 2) * DOT_RADIUS_FACTOR, MAX_DOT_RADIUS) : barWidth / 2;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{arcLengthDeg > ENDPOINT_MARKER_MIN_ANGLE && (
|
|
||||||
<circle cx={xStart} cy={yStart} r={dotRadius} fill={pointColorStart} opacity={DOT_OPACITY} />
|
|
||||||
)}
|
|
||||||
<circle cx={xEnd} cy={yEnd} r={dotRadius} fill={pointColorEnd} opacity={DOT_OPACITY} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case 'glow':
|
|
||||||
const offsetAngle = toRad(ENDPOINT_MARKER_MIN_ANGLE);
|
|
||||||
const xStartMark = centerX + radius * Math.cos(endRadians + offsetAngle);
|
|
||||||
const yStartMark = centerY + radius * Math.sin(endRadians + offsetAngle);
|
|
||||||
if (arcLengthDeg <= ENDPOINT_MARKER_MIN_ANGLE) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<path
|
|
||||||
d={['M', xStartMark, yStartMark, 'A', radius, radius, 0, 0, 1, xEnd, yEnd].join(' ')}
|
|
||||||
fill="none"
|
|
||||||
strokeWidth={barWidth}
|
|
||||||
stroke={endpointMarkerGlowFilter}
|
|
||||||
strokeLinecap={roundedBars ? 'round' : 'butt'}
|
|
||||||
filter={glowFilter}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -175,7 +175,7 @@ export function getGradientCss(gradientStops: GradientStop[], shape: RadialShape
|
|||||||
const GRAY_05 = '#111217';
|
const GRAY_05 = '#111217';
|
||||||
const GRAY_90 = '#fbfbfb';
|
const GRAY_90 = '#fbfbfb';
|
||||||
const CONTRAST_THRESHOLD_MAX = 4.5;
|
const CONTRAST_THRESHOLD_MAX = 4.5;
|
||||||
export const getGuideDotColor = (color: string): string => {
|
const getGuideDotColor = (color: string): string => {
|
||||||
const darkColor = GRAY_05;
|
const darkColor = GRAY_05;
|
||||||
const lightColor = GRAY_90;
|
const lightColor = GRAY_90;
|
||||||
return colorManipulator.getContrastRatio(darkColor, color) >= CONTRAST_THRESHOLD_MAX ? darkColor : lightColor;
|
return colorManipulator.getContrastRatio(darkColor, color) >= CONTRAST_THRESHOLD_MAX ? darkColor : lightColor;
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ func (hs *HTTPServer) DeleteDataSourceById(c *contextmodel.ReqContext) response.
|
|||||||
func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Response {
|
func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Response {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("GetDataSourceByUID"), time.Since(start).Seconds())
|
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "GetDataSourceByUID"), time.Since(start).Seconds())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
ds, err := hs.getRawDataSourceByUID(c.Req.Context(), web.Params(c.Req)[":uid"], c.GetOrgID())
|
ds, err := hs.getRawDataSourceByUID(c.Req.Context(), web.Params(c.Req)[":uid"], c.GetOrgID())
|
||||||
@@ -240,7 +240,7 @@ func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Re
|
|||||||
func (hs *HTTPServer) DeleteDataSourceByUID(c *contextmodel.ReqContext) response.Response {
|
func (hs *HTTPServer) DeleteDataSourceByUID(c *contextmodel.ReqContext) response.Response {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("DeleteDataSourceByUID"), time.Since(start).Seconds())
|
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "DeleteDataSourceByUID"), time.Since(start).Seconds())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
uid := web.Params(c.Req)[":uid"]
|
uid := web.Params(c.Req)[":uid"]
|
||||||
@@ -375,7 +375,7 @@ func validateJSONData(jsonData *simplejson.Json, cfg *setting.Cfg) error {
|
|||||||
func (hs *HTTPServer) AddDataSource(c *contextmodel.ReqContext) response.Response {
|
func (hs *HTTPServer) AddDataSource(c *contextmodel.ReqContext) response.Response {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("AddDataSource"), time.Since(start).Seconds())
|
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "AddDataSource"), time.Since(start).Seconds())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
cmd := datasources.AddDataSourceCommand{}
|
cmd := datasources.AddDataSourceCommand{}
|
||||||
@@ -497,7 +497,7 @@ func (hs *HTTPServer) UpdateDataSourceByID(c *contextmodel.ReqContext) response.
|
|||||||
func (hs *HTTPServer) UpdateDataSourceByUID(c *contextmodel.ReqContext) response.Response {
|
func (hs *HTTPServer) UpdateDataSourceByUID(c *contextmodel.ReqContext) response.Response {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("UpdateDataSourceByUID"), time.Since(start).Seconds())
|
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "UpdateDataSourceByUID"), time.Since(start).Seconds())
|
||||||
}()
|
}()
|
||||||
cmd := datasources.UpdateDataSourceCommand{}
|
cmd := datasources.UpdateDataSourceCommand{}
|
||||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func setupDsConfigHandlerMetrics() (prometheus.Registerer, *prometheus.Histogram
|
|||||||
Namespace: "grafana",
|
Namespace: "grafana",
|
||||||
Name: "ds_config_handler_requests_duration_seconds",
|
Name: "ds_config_handler_requests_duration_seconds",
|
||||||
Help: "Duration of requests handled by datasource configuration handlers",
|
Help: "Duration of requests handled by datasource configuration handlers",
|
||||||
}, []string{"handler"})
|
}, []string{"code_path", "handler"})
|
||||||
promRegister.MustRegister(dsConfigHandlerRequestsDuration)
|
promRegister.MustRegister(dsConfigHandlerRequestsDuration)
|
||||||
return promRegister, dsConfigHandlerRequestsDuration
|
return promRegister, dsConfigHandlerRequestsDuration
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -387,7 +387,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
|||||||
Namespace: "grafana",
|
Namespace: "grafana",
|
||||||
Name: "ds_config_handler_requests_duration_seconds",
|
Name: "ds_config_handler_requests_duration_seconds",
|
||||||
Help: "Duration of requests handled by datasource configuration handlers",
|
Help: "Duration of requests handled by datasource configuration handlers",
|
||||||
}, []string{"handler"}),
|
}, []string{"code_path", "handler"}),
|
||||||
}
|
}
|
||||||
|
|
||||||
promRegister.MustRegister(hs.htmlHandlerRequestsDuration)
|
promRegister.MustRegister(hs.htmlHandlerRequestsDuration)
|
||||||
|
|||||||
@@ -928,10 +928,9 @@ func getDatasourceProxiedRequest(t *testing.T, ctx *contextmodel.ReqContext, cfg
|
|||||||
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
||||||
features := featuremgmt.WithFeatures()
|
features := featuremgmt.WithFeatures()
|
||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
dsRetriever := datasourceservice.ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features),
|
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features),
|
||||||
&actest.FakePermissionsService{}, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{},
|
&actest.FakePermissionsService{}, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{},
|
||||||
plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()), dsRetriever)
|
plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, features)
|
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, features)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1051,11 +1050,9 @@ func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, secrets
|
|||||||
var routes []*plugins.Route
|
var routes []*plugins.Route
|
||||||
features := featuremgmt.WithFeatures()
|
features := featuremgmt.WithFeatures()
|
||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
var sqlStore db.DB = nil
|
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features),
|
||||||
dsRetriever := datasourceservice.ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := datasourceservice.ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features),
|
|
||||||
&actest.FakePermissionsService{}, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{},
|
&actest.FakePermissionsService{}, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{},
|
||||||
plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()), dsRetriever)
|
plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
proxy, err := NewDataSourceProxy(test.datasource, routes, ctx, "", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, features)
|
proxy, err := NewDataSourceProxy(test.datasource, routes, ctx, "", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, features)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1109,11 +1106,9 @@ func setupDSProxyTest(t *testing.T, ctx *contextmodel.ReqContext, ds *datasource
|
|||||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
secretsStore := secretskvs.NewSQLSecretsKVStore(dbtest.NewFakeDB(), secretsService, log.NewNopLogger())
|
secretsStore := secretskvs.NewSQLSecretsKVStore(dbtest.NewFakeDB(), secretsService, log.NewNopLogger())
|
||||||
features := featuremgmt.WithFeatures()
|
features := featuremgmt.WithFeatures()
|
||||||
var sqlStore db.DB = nil
|
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features),
|
||||||
dsRetriever := datasourceservice.ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := datasourceservice.ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features),
|
|
||||||
&actest.FakePermissionsService{}, quotatest.New(false, nil), &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{},
|
&actest.FakePermissionsService{}, quotatest.New(false, nil), &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{},
|
||||||
plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()), dsRetriever)
|
plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
tracer := tracing.InitializeTracerForTest()
|
tracer := tracing.InitializeTracerForTest()
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ import (
|
|||||||
_ "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
|
_ "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
|
||||||
_ "github.com/Azure/go-autorest/autorest"
|
_ "github.com/Azure/go-autorest/autorest"
|
||||||
_ "github.com/Azure/go-autorest/autorest/adal"
|
_ "github.com/Azure/go-autorest/autorest/adal"
|
||||||
_ "github.com/aws/aws-sdk-go-v2/credentials"
|
|
||||||
_ "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
|
|
||||||
_ "github.com/aws/aws-sdk-go-v2/service/sts"
|
|
||||||
_ "github.com/beevik/etree"
|
_ "github.com/beevik/etree"
|
||||||
_ "github.com/blugelabs/bluge"
|
_ "github.com/blugelabs/bluge"
|
||||||
_ "github.com/blugelabs/bluge_segment_api"
|
_ "github.com/blugelabs/bluge_segment_api"
|
||||||
@@ -49,6 +46,7 @@ import (
|
|||||||
_ "sigs.k8s.io/randfill"
|
_ "sigs.k8s.io/randfill"
|
||||||
_ "xorm.io/builder"
|
_ "xorm.io/builder"
|
||||||
|
|
||||||
|
_ "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
|
||||||
_ "github.com/grafana/authlib/authn"
|
_ "github.com/grafana/authlib/authn"
|
||||||
_ "github.com/grafana/authlib/authz"
|
_ "github.com/grafana/authlib/authz"
|
||||||
_ "github.com/grafana/authlib/cache"
|
_ "github.com/grafana/authlib/cache"
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ func (ots *TracingService) initSampler() (tracesdk.Sampler, error) {
|
|||||||
case "rateLimiting":
|
case "rateLimiting":
|
||||||
return newRateLimiter(ots.cfg.SamplerParam), nil
|
return newRateLimiter(ots.cfg.SamplerParam), nil
|
||||||
case "remote":
|
case "remote":
|
||||||
return jaegerremote.New(ots.cfg.ServiceName,
|
return jaegerremote.New("grafana",
|
||||||
jaegerremote.WithSamplingServerURL(ots.cfg.SamplerRemoteURL),
|
jaegerremote.WithSamplingServerURL(ots.cfg.SamplerRemoteURL),
|
||||||
jaegerremote.WithInitialSampler(tracesdk.TraceIDRatioBased(ots.cfg.SamplerParam)),
|
jaegerremote.WithInitialSampler(tracesdk.TraceIDRatioBased(ots.cfg.SamplerParam)),
|
||||||
), nil
|
), nil
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -315,12 +316,6 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort.Slice(parsedResults.Hits, func(i, j int) bool {
|
|
||||||
// // Just sorting by resource for now. The rest should be sorted by search score already
|
|
||||||
// return parsedResults.Hits[i].Resource > parsedResults.Hits[j].Resource
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
result, err := s.client.Search(ctx, searchRequest)
|
result, err := s.client.Search(ctx, searchRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errhttp.Write(ctx, err, w)
|
errhttp.Write(ctx, err, w)
|
||||||
@@ -337,6 +332,14 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(searchRequest.SortBy) == 0 {
|
||||||
|
// default sort by resource descending ( folders then dashboards ) then title
|
||||||
|
sort.Slice(parsedResults.Hits, func(i, j int) bool {
|
||||||
|
// Just sorting by resource for now. The rest should be sorted by search score already
|
||||||
|
return parsedResults.Hits[i].Resource > parsedResults.Hits[j].Resource
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
s.write(w, parsedResults)
|
s.write(w, parsedResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,18 +428,6 @@ func convertHttpSearchRequestToResourceSearchRequest(queryParams url.Values, use
|
|||||||
}
|
}
|
||||||
searchRequest.SortBy = append(searchRequest.SortBy, s)
|
searchRequest.SortBy = append(searchRequest.SortBy, s)
|
||||||
}
|
}
|
||||||
} else if searchRequest.Query == "" {
|
|
||||||
// When no query exists, return the results in a predictable order
|
|
||||||
searchRequest.SortBy = []*resourcepb.ResourceSearchRequest_Sort{
|
|
||||||
{
|
|
||||||
Field: resource.SEARCH_FIELD_GROUP_RESOURCE, // folders then dashboards
|
|
||||||
Desc: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Field: resource.SEARCH_FIELD_TITLE, // then title
|
|
||||||
Desc: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The facet term fields
|
// The facet term fields
|
||||||
|
|||||||
@@ -57,12 +57,6 @@ func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Objec
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
||||||
if s.dsConfigHandlerRequestsDuration != nil {
|
|
||||||
start := time.Now()
|
|
||||||
defer func() {
|
|
||||||
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("legacyStorage.List"), time.Since(start).Seconds())
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
return s.datasources.ListDataSources(ctx)
|
return s.datasources.ListDataSources(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +64,7 @@ func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.Ge
|
|||||||
if s.dsConfigHandlerRequestsDuration != nil {
|
if s.dsConfigHandlerRequestsDuration != nil {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("legacyStorage.Get"), time.Since(start).Seconds())
|
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Get"), time.Since(start).Seconds())
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +76,7 @@ func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, createVa
|
|||||||
if s.dsConfigHandlerRequestsDuration != nil {
|
if s.dsConfigHandlerRequestsDuration != nil {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("legacyStorage.Create"), time.Since(start).Seconds())
|
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds())
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +92,7 @@ func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.Up
|
|||||||
if s.dsConfigHandlerRequestsDuration != nil {
|
if s.dsConfigHandlerRequestsDuration != nil {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("legacyStorage.Update"), time.Since(start).Seconds())
|
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds())
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +135,7 @@ func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidatio
|
|||||||
if s.dsConfigHandlerRequestsDuration != nil {
|
if s.dsConfigHandlerRequestsDuration != nil {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("legacyStorage.Delete"), time.Since(start).Seconds())
|
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds())
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,13 +145,6 @@ func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidatio
|
|||||||
|
|
||||||
// DeleteCollection implements rest.CollectionDeleter.
|
// DeleteCollection implements rest.CollectionDeleter.
|
||||||
func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
|
func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
|
||||||
if s.dsConfigHandlerRequestsDuration != nil {
|
|
||||||
start := time.Now()
|
|
||||||
defer func() {
|
|
||||||
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("legacyStorage.DeleteCollection"), time.Since(start).Seconds())
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
dss, err := s.datasources.ListDataSources(ctx)
|
dss, err := s.datasources.ListDataSources(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||||
queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
|
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
||||||
@@ -70,10 +69,10 @@ func RegisterAPIService(
|
|||||||
|
|
||||||
dataSourceCRUDMetric := metricutil.NewHistogramVec(prometheus.HistogramOpts{
|
dataSourceCRUDMetric := metricutil.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
Namespace: "grafana",
|
Namespace: "grafana",
|
||||||
Name: "ds_config_handler_apis_requests_duration_seconds",
|
Name: "ds_config_handler_requests_duration_seconds",
|
||||||
Help: "Duration of requests handled by new k8s style APIs datasource configuration handlers",
|
Help: "Duration of requests handled by datasource configuration handlers",
|
||||||
}, []string{"handler"})
|
}, []string{"code_path", "handler"})
|
||||||
regErr := metrics.ProvideRegisterer().Register(dataSourceCRUDMetric)
|
regErr := reg.Register(dataSourceCRUDMetric)
|
||||||
if regErr != nil && !errors.As(regErr, &prometheus.AlreadyRegisteredError{}) {
|
if regErr != nil && !errors.As(regErr, &prometheus.AlreadyRegisteredError{}) {
|
||||||
return nil, regErr
|
return nil, regErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||||
"github.com/grafana/grafana/pkg/services/apiserver/appinstaller"
|
"github.com/grafana/grafana/pkg/services/apiserver/appinstaller"
|
||||||
grafanaauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
|
grafanaauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||||
)
|
)
|
||||||
@@ -37,14 +36,10 @@ func ProvideAppInstaller(
|
|||||||
pluginStore pluginstore.Store,
|
pluginStore pluginstore.Store,
|
||||||
pluginAssetsService *pluginassets.Service,
|
pluginAssetsService *pluginassets.Service,
|
||||||
accessControlService accesscontrol.Service, accessClient authlib.AccessClient,
|
accessControlService accesscontrol.Service, accessClient authlib.AccessClient,
|
||||||
features featuremgmt.FeatureToggles,
|
|
||||||
) (*AppInstaller, error) {
|
) (*AppInstaller, error) {
|
||||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
|
||||||
if features.IsEnabledGlobally(featuremgmt.FlagPluginStoreServiceLoading) {
|
|
||||||
if err := registerAccessControlRoles(accessControlService); err != nil {
|
if err := registerAccessControlRoles(accessControlService); err != nil {
|
||||||
return nil, fmt.Errorf("registering access control roles: %w", err)
|
return nil, fmt.Errorf("registering access control roles: %w", err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
localProvider := meta.NewLocalProvider(pluginStore, pluginAssetsService)
|
localProvider := meta.NewLocalProvider(pluginStore, pluginAssetsService)
|
||||||
metaProviderManager := meta.NewProviderManager(localProvider)
|
metaProviderManager := meta.NewProviderManager(localProvider)
|
||||||
|
|||||||
@@ -330,7 +330,6 @@ var wireBasicSet = wire.NewSet(
|
|||||||
dashsnapstore.ProvideStore,
|
dashsnapstore.ProvideStore,
|
||||||
wire.Bind(new(dashboardsnapshots.Service), new(*dashsnapsvc.ServiceImpl)),
|
wire.Bind(new(dashboardsnapshots.Service), new(*dashsnapsvc.ServiceImpl)),
|
||||||
dashsnapsvc.ProvideService,
|
dashsnapsvc.ProvideService,
|
||||||
datasourceservice.ProvideDataSourceRetriever,
|
|
||||||
datasourceservice.ProvideService,
|
datasourceservice.ProvideService,
|
||||||
wire.Bind(new(datasources.DataSourceService), new(*datasourceservice.Service)),
|
wire.Bind(new(datasources.DataSourceService), new(*datasourceservice.Service)),
|
||||||
datasourceservice.ProvideLegacyDataSourceLookup,
|
datasourceservice.ProvideLegacyDataSourceLookup,
|
||||||
|
|||||||
12
pkg/server/wire_gen.go
generated
12
pkg/server/wire_gen.go
generated
File diff suppressed because one or more lines are too long
@@ -3,6 +3,7 @@ package authorizer
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
k8suser "k8s.io/apiserver/pkg/authentication/user"
|
k8suser "k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
@@ -28,9 +29,9 @@ type GrafanaAuthorizer struct {
|
|||||||
// 4. We check authorizer that is configured speficially for an api.
|
// 4. We check authorizer that is configured speficially for an api.
|
||||||
// 5. As a last fallback we check Role, this will only happen if an api have not configured
|
// 5. As a last fallback we check Role, this will only happen if an api have not configured
|
||||||
// an authorizer or return authorizer.DecisionNoOpinion
|
// an authorizer or return authorizer.DecisionNoOpinion
|
||||||
func NewGrafanaBuiltInSTAuthorizer() *GrafanaAuthorizer {
|
func NewGrafanaBuiltInSTAuthorizer(cfg *setting.Cfg) *GrafanaAuthorizer {
|
||||||
authorizers := []authorizer.Authorizer{
|
authorizers := []authorizer.Authorizer{
|
||||||
NewImpersonationAuthorizer(),
|
newImpersonationAuthorizer(),
|
||||||
authorizerfactory.NewPrivilegedGroups(k8suser.SystemPrivilegedGroup),
|
authorizerfactory.NewPrivilegedGroups(k8suser.SystemPrivilegedGroup),
|
||||||
newNamespaceAuthorizer(),
|
newNamespaceAuthorizer(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
var _ authorizer.Authorizer = (*impersonationAuthorizer)(nil)
|
var _ authorizer.Authorizer = (*impersonationAuthorizer)(nil)
|
||||||
|
|
||||||
func NewImpersonationAuthorizer() *impersonationAuthorizer {
|
func newImpersonationAuthorizer() *impersonationAuthorizer {
|
||||||
return &impersonationAuthorizer{}
|
return &impersonationAuthorizer{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,19 @@ var PathRewriters = []filters.PathRewriter{
|
|||||||
|
|
||||||
func GetDefaultBuildHandlerChainFunc(builders []APIGroupBuilder, reg prometheus.Registerer) BuildHandlerChainFunc {
|
func GetDefaultBuildHandlerChainFunc(builders []APIGroupBuilder, reg prometheus.Registerer) BuildHandlerChainFunc {
|
||||||
return func(delegateHandler http.Handler, c *genericapiserver.Config) http.Handler {
|
return func(delegateHandler http.Handler, c *genericapiserver.Config) http.Handler {
|
||||||
handler := filters.WithTracingHTTPLoggingAttributes(delegateHandler)
|
requestHandler, err := GetCustomRoutesHandler(
|
||||||
|
delegateHandler,
|
||||||
|
c.LoopbackClientConfig,
|
||||||
|
builders,
|
||||||
|
reg,
|
||||||
|
c.MergedResourceConfig,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("could not build the request handler for specified API builders: %s", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Needs to run last in request chain to function as expected, hence we register it first.
|
||||||
|
handler := filters.WithTracingHTTPLoggingAttributes(requestHandler)
|
||||||
|
|
||||||
// filters.WithRequester needs to be after the K8s chain because it depends on the K8s user in context
|
// filters.WithRequester needs to be after the K8s chain because it depends on the K8s user in context
|
||||||
handler = filters.WithRequester(handler)
|
handler = filters.WithRequester(handler)
|
||||||
|
|||||||
@@ -3,306 +3,146 @@ package builder
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/emicklei/go-restful/v3"
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
||||||
|
restclient "k8s.io/client-go/rest"
|
||||||
klog "k8s.io/klog/v2"
|
klog "k8s.io/klog/v2"
|
||||||
"k8s.io/kube-openapi/pkg/spec3"
|
"k8s.io/kube-openapi/pkg/spec3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// convertHandlerToRouteFunction converts an http.HandlerFunc to a restful.RouteFunction
|
type requestHandler struct {
|
||||||
// It extracts path parameters from restful.Request and populates them in the request context
|
router *mux.Router
|
||||||
// so that mux.Vars can read them (for backward compatibility with handlers that use mux.Vars)
|
|
||||||
func convertHandlerToRouteFunction(handler http.HandlerFunc) restful.RouteFunction {
|
|
||||||
return func(req *restful.Request, resp *restful.Response) {
|
|
||||||
// Extract path parameters from restful.Request and populate mux.Vars
|
|
||||||
// This is needed for backward compatibility with handlers that use mux.Vars(r)
|
|
||||||
vars := make(map[string]string)
|
|
||||||
|
|
||||||
// Get all path parameters from the restful.Request
|
|
||||||
// The restful.Request has PathParameters() method that returns a map
|
|
||||||
pathParams := req.PathParameters()
|
|
||||||
for key, value := range pathParams {
|
|
||||||
vars[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the vars in the request context using mux.SetURLVars
|
|
||||||
// This makes mux.Vars(r) work correctly
|
|
||||||
if len(vars) > 0 {
|
|
||||||
req.Request = mux.SetURLVars(req.Request, vars)
|
|
||||||
}
|
|
||||||
|
|
||||||
handler(resp.ResponseWriter, req.Request)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AugmentWebServicesWithCustomRoutes adds custom routes from builders to existing WebServices
|
func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient.Config, builders []APIGroupBuilder, metricsRegistry prometheus.Registerer, apiResourceConfig *serverstorage.ResourceConfig) (http.Handler, error) {
|
||||||
// in the container.
|
useful := false // only true if any routes exist anywhere
|
||||||
func AugmentWebServicesWithCustomRoutes(
|
router := mux.NewRouter()
|
||||||
container *restful.Container,
|
|
||||||
builders []APIGroupBuilder,
|
|
||||||
metricsRegistry prometheus.Registerer,
|
|
||||||
apiResourceConfig *serverstorage.ResourceConfig,
|
|
||||||
) error {
|
|
||||||
if container == nil {
|
|
||||||
return fmt.Errorf("container cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
metrics := NewCustomRouteMetrics(metricsRegistry)
|
metrics := NewCustomRouteMetrics(metricsRegistry)
|
||||||
|
|
||||||
// Build a map of existing WebServices by root path
|
for _, builder := range builders {
|
||||||
existingWebServices := make(map[string]*restful.WebService)
|
provider, ok := builder.(APIGroupRouteProvider)
|
||||||
for _, ws := range container.RegisteredWebServices() {
|
|
||||||
existingWebServices[ws.RootPath()] = ws
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, b := range builders {
|
|
||||||
provider, ok := b.(APIGroupRouteProvider)
|
|
||||||
if !ok || provider == nil {
|
if !ok || provider == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, gv := range GetGroupVersions(b) {
|
for _, gv := range GetGroupVersions(builder) {
|
||||||
// Filter out disabled API groups
|
// filter out api groups that are disabled in APIEnablementOptions
|
||||||
gvr := gv.WithResource("")
|
gvr := gv.WithResource("")
|
||||||
if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gvr) {
|
if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gvr) {
|
||||||
klog.InfoS("Skipping custom routes for disabled group version", "gv", gv.String())
|
klog.InfoS("Skipping custom route handler for disabled group version", "gv", gv.String())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
routes := provider.GetAPIRoutes(gv)
|
routes := provider.GetAPIRoutes(gv)
|
||||||
if routes == nil {
|
if routes == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find or create WebService for this group version
|
prefix := "/apis/" + gv.String()
|
||||||
rootPath := "/apis/" + gv.String()
|
|
||||||
ws, exists := existingWebServices[rootPath]
|
|
||||||
if !exists {
|
|
||||||
// Create a new WebService if one doesn't exist
|
|
||||||
ws = new(restful.WebService)
|
|
||||||
ws.Path(rootPath)
|
|
||||||
container.Add(ws)
|
|
||||||
existingWebServices[rootPath] = ws
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add root handlers using OpenAPI specs
|
// Root handlers
|
||||||
|
var sub *mux.Router
|
||||||
for _, route := range routes.Root {
|
for _, route := range routes.Root {
|
||||||
|
if sub == nil {
|
||||||
|
sub = router.PathPrefix(prefix).Subrouter()
|
||||||
|
sub.MethodNotAllowedHandler = &methodNotAllowedHandler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
useful = true
|
||||||
|
methods, err := methodsFromSpec(route.Path, route.Spec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
instrumentedHandler := metrics.InstrumentHandler(
|
instrumentedHandler := metrics.InstrumentHandler(
|
||||||
gv.Group,
|
gv.Group,
|
||||||
gv.Version,
|
gv.Version,
|
||||||
route.Path,
|
route.Path, // Use path as resource identifier
|
||||||
route.Handler,
|
route.Handler,
|
||||||
)
|
)
|
||||||
routeFunction := convertHandlerToRouteFunction(instrumentedHandler)
|
|
||||||
|
|
||||||
// Use OpenAPI spec to configure routes properly
|
sub.HandleFunc("/"+route.Path, instrumentedHandler).
|
||||||
if err := addRouteFromSpec(ws, route.Path, route.Spec, routeFunction, false); err != nil {
|
Methods(methods...)
|
||||||
return fmt.Errorf("failed to add root route %s: %w", route.Path, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add namespace handlers using OpenAPI specs
|
// Namespace handlers
|
||||||
|
sub = nil
|
||||||
|
prefix += "/namespaces/{namespace}"
|
||||||
for _, route := range routes.Namespace {
|
for _, route := range routes.Namespace {
|
||||||
|
if sub == nil {
|
||||||
|
sub = router.PathPrefix(prefix).Subrouter()
|
||||||
|
sub.MethodNotAllowedHandler = &methodNotAllowedHandler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
useful = true
|
||||||
|
methods, err := methodsFromSpec(route.Path, route.Spec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
instrumentedHandler := metrics.InstrumentHandler(
|
instrumentedHandler := metrics.InstrumentHandler(
|
||||||
gv.Group,
|
gv.Group,
|
||||||
gv.Version,
|
gv.Version,
|
||||||
route.Path,
|
route.Path, // Use path as resource identifier
|
||||||
route.Handler,
|
route.Handler,
|
||||||
)
|
)
|
||||||
routeFunction := convertHandlerToRouteFunction(instrumentedHandler)
|
|
||||||
|
|
||||||
// Use OpenAPI spec to configure routes properly
|
sub.HandleFunc("/"+route.Path, instrumentedHandler).
|
||||||
if err := addRouteFromSpec(ws, route.Path, route.Spec, routeFunction, true); err != nil {
|
Methods(methods...)
|
||||||
return fmt.Errorf("failed to add namespace route %s: %w", route.Path, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if !useful {
|
||||||
|
return delegateHandler, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per Gorilla Mux issue here: https://github.com/gorilla/mux/issues/616#issuecomment-798807509
|
||||||
|
// default handler must come last
|
||||||
|
router.PathPrefix("/").Handler(delegateHandler)
|
||||||
|
|
||||||
|
return &requestHandler{
|
||||||
|
router: router,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// addRouteFromSpec adds routes to a WebService using OpenAPI specs
|
func (h *requestHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
func addRouteFromSpec(ws *restful.WebService, routePath string, pathProps *spec3.PathProps, handler restful.RouteFunction, isNamespaced bool) error {
|
h.router.ServeHTTP(w, req)
|
||||||
if pathProps == nil {
|
|
||||||
return fmt.Errorf("pathProps cannot be nil for route %s", routePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the full path (relative to WebService root)
|
|
||||||
var fullPath string
|
|
||||||
if isNamespaced {
|
|
||||||
fullPath = "/namespaces/{namespace}/" + routePath
|
|
||||||
} else {
|
|
||||||
fullPath = "/" + routePath
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add routes for each HTTP method defined in the OpenAPI spec
|
|
||||||
operations := map[string]*spec3.Operation{
|
|
||||||
"GET": pathProps.Get,
|
|
||||||
"POST": pathProps.Post,
|
|
||||||
"PUT": pathProps.Put,
|
|
||||||
"PATCH": pathProps.Patch,
|
|
||||||
"DELETE": pathProps.Delete,
|
|
||||||
}
|
|
||||||
|
|
||||||
for method, operation := range operations {
|
|
||||||
if operation == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create route builder for this method
|
|
||||||
var routeBuilder *restful.RouteBuilder
|
|
||||||
switch method {
|
|
||||||
case "GET":
|
|
||||||
routeBuilder = ws.GET(fullPath)
|
|
||||||
case "POST":
|
|
||||||
routeBuilder = ws.POST(fullPath)
|
|
||||||
case "PUT":
|
|
||||||
routeBuilder = ws.PUT(fullPath)
|
|
||||||
case "PATCH":
|
|
||||||
routeBuilder = ws.PATCH(fullPath)
|
|
||||||
case "DELETE":
|
|
||||||
routeBuilder = ws.DELETE(fullPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set operation ID from OpenAPI spec (with K8s verb prefix if needed)
|
|
||||||
operationID := operation.OperationId
|
|
||||||
if operationID == "" {
|
|
||||||
// Generate from path if not specified
|
|
||||||
operationID = generateOperationNameFromPath(routePath)
|
|
||||||
}
|
|
||||||
operationID = prefixRouteIDWithK8sVerbIfNotPresent(operationID, method)
|
|
||||||
routeBuilder = routeBuilder.Operation(operationID)
|
|
||||||
|
|
||||||
// Add description from OpenAPI spec
|
|
||||||
if operation.Description != "" {
|
|
||||||
routeBuilder = routeBuilder.Doc(operation.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if namespace parameter is already in the OpenAPI spec
|
|
||||||
hasNamespaceParam := false
|
|
||||||
if operation.Parameters != nil {
|
|
||||||
for _, param := range operation.Parameters {
|
|
||||||
if param.Name == "namespace" && param.In == "path" {
|
|
||||||
hasNamespaceParam = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add namespace parameter for namespaced routes if not already in spec
|
|
||||||
if isNamespaced && !hasNamespaceParam {
|
|
||||||
routeBuilder = routeBuilder.Param(restful.PathParameter("namespace", "object name and auth scope, such as for teams and projects"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add parameters from OpenAPI spec
|
|
||||||
if operation.Parameters != nil {
|
|
||||||
for _, param := range operation.Parameters {
|
|
||||||
switch param.In {
|
|
||||||
case "path":
|
|
||||||
routeBuilder = routeBuilder.Param(restful.PathParameter(param.Name, param.Description))
|
|
||||||
case "query":
|
|
||||||
routeBuilder = routeBuilder.Param(restful.QueryParameter(param.Name, param.Description))
|
|
||||||
case "header":
|
|
||||||
routeBuilder = routeBuilder.Param(restful.HeaderParameter(param.Name, param.Description))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: Request/response schemas are already defined in the OpenAPI spec from builders
|
|
||||||
// and will be added to the OpenAPI document via addBuilderRoutes in openapi.go.
|
|
||||||
// We don't duplicate that information here since restful uses the route metadata
|
|
||||||
// for OpenAPI generation, which is handled separately in this codebase.
|
|
||||||
|
|
||||||
// Register the route with handler
|
|
||||||
ws.Route(routeBuilder.To(handler))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func prefixRouteIDWithK8sVerbIfNotPresent(operationID string, method string) string {
|
func methodsFromSpec(slug string, props *spec3.PathProps) ([]string, error) {
|
||||||
for _, verb := range allowedK8sVerbs {
|
if props == nil {
|
||||||
if len(operationID) > len(verb) && operationID[:len(verb)] == verb {
|
return []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, nil
|
||||||
return operationID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
methods := make([]string, 0)
|
||||||
|
if props.Get != nil {
|
||||||
|
methods = append(methods, "GET")
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s%s", httpMethodToK8sVerb[strings.ToUpper(method)], operationID)
|
if props.Post != nil {
|
||||||
|
methods = append(methods, "POST")
|
||||||
|
}
|
||||||
|
if props.Put != nil {
|
||||||
|
methods = append(methods, "PUT")
|
||||||
|
}
|
||||||
|
if props.Patch != nil {
|
||||||
|
methods = append(methods, "PATCH")
|
||||||
|
}
|
||||||
|
if props.Delete != nil {
|
||||||
|
methods = append(methods, "DELETE")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(methods) == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid OpenAPI Spec for slug=%s without any methods in PathProps", slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
return methods, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var allowedK8sVerbs = []string{
|
type methodNotAllowedHandler struct{}
|
||||||
"get", "log", "read", "replace", "patch", "delete", "deletecollection", "watch", "connect", "proxy", "list", "create", "patch",
|
|
||||||
}
|
func (h *methodNotAllowedHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
w.WriteHeader(405) // method not allowed
|
||||||
var httpMethodToK8sVerb = map[string]string{
|
|
||||||
http.MethodGet: "get",
|
|
||||||
http.MethodPost: "create",
|
|
||||||
http.MethodPut: "replace",
|
|
||||||
http.MethodPatch: "patch",
|
|
||||||
http.MethodDelete: "delete",
|
|
||||||
http.MethodConnect: "connect",
|
|
||||||
http.MethodOptions: "connect", // No real equivalent to options and head
|
|
||||||
http.MethodHead: "connect",
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateOperationNameFromPath creates an operation name from a route path.
|
|
||||||
// The operation name is used by the OpenAPI generator and should be descriptive.
|
|
||||||
// It uses meaningful path segments to create readable yet unique operation names.
|
|
||||||
// Examples:
|
|
||||||
// - "/search" -> "Search"
|
|
||||||
// - "/snapshots/create" -> "SnapshotsCreate"
|
|
||||||
// - "ofrep/v1/evaluate/flags" -> "OfrepEvaluateFlags"
|
|
||||||
// - "ofrep/v1/evaluate/flags/{flagKey}" -> "OfrepEvaluateFlagsFlagKey"
|
|
||||||
func generateOperationNameFromPath(routePath string) string {
|
|
||||||
// Remove leading slash and split by path segments
|
|
||||||
parts := strings.Split(strings.TrimPrefix(routePath, "/"), "/")
|
|
||||||
|
|
||||||
// Filter to keep meaningful segments and path parameters
|
|
||||||
var nameParts []string
|
|
||||||
skipPrefixes := map[string]bool{
|
|
||||||
"namespaces": true,
|
|
||||||
"apis": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, part := range parts {
|
|
||||||
if part == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract parameter name from {paramName} format
|
|
||||||
if strings.HasPrefix(part, "{") && strings.HasSuffix(part, "}") {
|
|
||||||
paramName := part[1 : len(part)-1]
|
|
||||||
// Skip generic parameters like {namespace}, but keep specific ones like {flagKey}
|
|
||||||
if paramName != "namespace" && paramName != "name" {
|
|
||||||
nameParts = append(nameParts, strings.ToUpper(paramName[:1])+paramName[1:])
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip common prefixes
|
|
||||||
if skipPrefixes[strings.ToLower(part)] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip version segments like v1, v0alpha1, v2beta1, etc.
|
|
||||||
if strings.HasPrefix(strings.ToLower(part), "v") &&
|
|
||||||
(len(part) <= 3 || strings.Contains(strings.ToLower(part), "alpha") || strings.Contains(strings.ToLower(part), "beta")) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capitalize first letter and add to parts
|
|
||||||
if len(part) > 0 {
|
|
||||||
nameParts = append(nameParts, strings.ToUpper(part[:1])+part[1:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(nameParts) == 0 {
|
|
||||||
return "Route"
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(nameParts, "")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/apiserver/options"
|
"github.com/grafana/grafana/pkg/services/apiserver/options"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
@@ -40,6 +41,15 @@ func applyGrafanaConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles, o
|
|||||||
apiserverCfg := cfg.SectionWithEnvOverrides("grafana-apiserver")
|
apiserverCfg := cfg.SectionWithEnvOverrides("grafana-apiserver")
|
||||||
|
|
||||||
runtimeConfig := apiserverCfg.Key("runtime_config").String()
|
runtimeConfig := apiserverCfg.Key("runtime_config").String()
|
||||||
|
runtimeConfigSplit := strings.Split(runtimeConfig, ",")
|
||||||
|
|
||||||
|
// TODO: temporary fix to allow disabling local features service and still being able to use its authz handler
|
||||||
|
if !cfg.OpenFeature.APIEnabled {
|
||||||
|
runtimeConfigSplit = append(runtimeConfigSplit, "features.grafana.app/v0alpha1=false")
|
||||||
|
}
|
||||||
|
|
||||||
|
runtimeConfig = strings.Join(runtimeConfigSplit, ",")
|
||||||
|
|
||||||
if runtimeConfig != "" {
|
if runtimeConfig != "" {
|
||||||
if err := o.APIEnablementOptions.RuntimeConfig.Set(runtimeConfig); err != nil {
|
if err := o.APIEnablementOptions.RuntimeConfig.Set(runtimeConfig); err != nil {
|
||||||
return fmt.Errorf("failed to set runtime config: %w", err)
|
return fmt.Errorf("failed to set runtime config: %w", err)
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ func ProvideService(
|
|||||||
features: features,
|
features: features,
|
||||||
rr: rr,
|
rr: rr,
|
||||||
builders: []builder.APIGroupBuilder{},
|
builders: []builder.APIGroupBuilder{},
|
||||||
authorizer: authorizer.NewGrafanaBuiltInSTAuthorizer(),
|
authorizer: authorizer.NewGrafanaBuiltInSTAuthorizer(cfg),
|
||||||
tracing: tracing,
|
tracing: tracing,
|
||||||
db: db, // For Unified storage
|
db: db, // For Unified storage
|
||||||
metrics: reg,
|
metrics: reg,
|
||||||
@@ -443,19 +443,6 @@ func (s *service) start(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Augment existing WebServices with custom routes from builders
|
|
||||||
// This directly adds routes to existing WebServices using the OpenAPI specs from builders
|
|
||||||
if server.Handler != nil && server.Handler.GoRestfulContainer != nil {
|
|
||||||
if err := builder.AugmentWebServicesWithCustomRoutes(
|
|
||||||
server.Handler.GoRestfulContainer,
|
|
||||||
builders,
|
|
||||||
s.metrics,
|
|
||||||
serverConfig.MergedResourceConfig,
|
|
||||||
); err != nil {
|
|
||||||
return fmt.Errorf("failed to augment web services with custom routes: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// stash the options for later use
|
// stash the options for later use
|
||||||
s.options = o
|
s.options = o
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ type Service struct {
|
|||||||
pluginStore pluginstore.Store
|
pluginStore pluginstore.Store
|
||||||
pluginClient plugins.Client
|
pluginClient plugins.Client
|
||||||
basePluginContextProvider plugincontext.BasePluginContextProvider
|
basePluginContextProvider plugincontext.BasePluginContextProvider
|
||||||
retriever DataSourceRetriever
|
|
||||||
|
|
||||||
ptc proxyTransportCache
|
ptc proxyTransportCache
|
||||||
}
|
}
|
||||||
@@ -71,7 +70,6 @@ func ProvideService(
|
|||||||
features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, datasourcePermissionsService accesscontrol.DatasourcePermissionsService,
|
features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, datasourcePermissionsService accesscontrol.DatasourcePermissionsService,
|
||||||
quotaService quota.Service, pluginStore pluginstore.Store, pluginClient plugins.Client,
|
quotaService quota.Service, pluginStore pluginstore.Store, pluginClient plugins.Client,
|
||||||
basePluginContextProvider plugincontext.BasePluginContextProvider,
|
basePluginContextProvider plugincontext.BasePluginContextProvider,
|
||||||
retriever DataSourceRetriever,
|
|
||||||
) (*Service, error) {
|
) (*Service, error) {
|
||||||
dslogger := log.New("datasources")
|
dslogger := log.New("datasources")
|
||||||
store := &SqlStore{db: db, logger: dslogger, features: features}
|
store := &SqlStore{db: db, logger: dslogger, features: features}
|
||||||
@@ -91,7 +89,6 @@ func ProvideService(
|
|||||||
pluginStore: pluginStore,
|
pluginStore: pluginStore,
|
||||||
pluginClient: pluginClient,
|
pluginClient: pluginClient,
|
||||||
basePluginContextProvider: basePluginContextProvider,
|
basePluginContextProvider: basePluginContextProvider,
|
||||||
retriever: retriever,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ac.RegisterScopeAttributeResolver(NewNameScopeResolver(store))
|
ac.RegisterScopeAttributeResolver(NewNameScopeResolver(store))
|
||||||
@@ -178,11 +175,11 @@ func NewIDScopeResolver(db DataSourceRetriever) (string, accesscontrol.ScopeAttr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetDataSource(ctx context.Context, query *datasources.GetDataSourceQuery) (*datasources.DataSource, error) {
|
func (s *Service) GetDataSource(ctx context.Context, query *datasources.GetDataSourceQuery) (*datasources.DataSource, error) {
|
||||||
return s.retriever.GetDataSource(ctx, query)
|
return s.SQLStore.GetDataSource(ctx, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetDataSourceInNamespace(ctx context.Context, namespace, name, group string) (*datasources.DataSource, error) {
|
func (s *Service) GetDataSourceInNamespace(ctx context.Context, namespace, name, group string) (*datasources.DataSource, error) {
|
||||||
return s.retriever.GetDataSourceInNamespace(ctx, namespace, name, group)
|
return s.SQLStore.GetDataSourceInNamespace(ctx, namespace, name, group)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetDataSources(ctx context.Context, query *datasources.GetDataSourcesQuery) ([]*datasources.DataSource, error) {
|
func (s *Service) GetDataSources(ctx context.Context, query *datasources.GetDataSourcesQuery) ([]*datasources.DataSource, error) {
|
||||||
|
|||||||
@@ -832,9 +832,8 @@ func TestIntegrationService_DeleteDataSource(t *testing.T) {
|
|||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
permissionSvc := acmock.NewMockedPermissionsService()
|
permissionSvc := acmock.NewMockedPermissionsService()
|
||||||
permissionSvc.On("DeleteResourcePermissions", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
|
permissionSvc.On("DeleteResourcePermissions", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
|
||||||
features := featuremgmt.WithFeatures()
|
|
||||||
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
|
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, &setting.Cfg{}, featuremgmt.WithFeatures(), acmock.New(), permissionSvc, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil)
|
||||||
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, &setting.Cfg{}, features, acmock.New(), permissionSvc, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
cmd := &datasources.DeleteDataSourceCommand{
|
cmd := &datasources.DeleteDataSourceCommand{
|
||||||
@@ -858,9 +857,7 @@ func TestIntegrationService_DeleteDataSource(t *testing.T) {
|
|||||||
permissionSvc.On("DeleteResourcePermissions", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
|
permissionSvc.On("DeleteResourcePermissions", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
|
||||||
cfg := &setting.Cfg{}
|
cfg := &setting.Cfg{}
|
||||||
enableRBACManagedPermissions(t, cfg)
|
enableRBACManagedPermissions(t, cfg)
|
||||||
features := featuremgmt.WithFeatures()
|
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), permissionSvc, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil)
|
||||||
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), permissionSvc, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// First add the datasource
|
// First add the datasource
|
||||||
@@ -1127,9 +1124,7 @@ func TestIntegrationService_GetHttpTransport(t *testing.T) {
|
|||||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
features := featuremgmt.WithFeatures()
|
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil)
|
||||||
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
rt1, err := dsService.GetHTTPTransport(context.Background(), &ds, provider)
|
rt1, err := dsService.GetHTTPTransport(context.Background(), &ds, provider)
|
||||||
@@ -1166,9 +1161,7 @@ func TestIntegrationService_GetHttpTransport(t *testing.T) {
|
|||||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
features := featuremgmt.WithFeatures()
|
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil)
|
||||||
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ds := datasources.DataSource{
|
ds := datasources.DataSource{
|
||||||
@@ -1219,9 +1212,7 @@ func TestIntegrationService_GetHttpTransport(t *testing.T) {
|
|||||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
features := featuremgmt.WithFeatures()
|
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil)
|
||||||
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ds := datasources.DataSource{
|
ds := datasources.DataSource{
|
||||||
@@ -1269,9 +1260,7 @@ func TestIntegrationService_GetHttpTransport(t *testing.T) {
|
|||||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
features := featuremgmt.WithFeatures()
|
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil)
|
||||||
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ds := datasources.DataSource{
|
ds := datasources.DataSource{
|
||||||
@@ -1327,9 +1316,7 @@ func TestIntegrationService_GetHttpTransport(t *testing.T) {
|
|||||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
features := featuremgmt.WithFeatures()
|
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil)
|
||||||
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ds := datasources.DataSource{
|
ds := datasources.DataSource{
|
||||||
@@ -1364,9 +1351,7 @@ func TestIntegrationService_GetHttpTransport(t *testing.T) {
|
|||||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
features := featuremgmt.WithFeatures()
|
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil)
|
||||||
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ds := datasources.DataSource{
|
ds := datasources.DataSource{
|
||||||
@@ -1435,9 +1420,7 @@ func TestIntegrationService_GetHttpTransport(t *testing.T) {
|
|||||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
features := featuremgmt.WithFeatures()
|
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil)
|
||||||
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ds := datasources.DataSource{
|
ds := datasources.DataSource{
|
||||||
@@ -1516,9 +1499,7 @@ func TestIntegrationService_GetHttpTransport(t *testing.T) {
|
|||||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
features := featuremgmt.WithFeatures()
|
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil)
|
||||||
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ds := datasources.DataSource{
|
ds := datasources.DataSource{
|
||||||
@@ -1541,9 +1522,7 @@ func TestIntegrationService_getProxySettings(t *testing.T) {
|
|||||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
features := featuremgmt.WithFeatures()
|
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, &setting.Cfg{}, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil)
|
||||||
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, &setting.Cfg{}, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("Should default to disabled", func(t *testing.T) {
|
t.Run("Should default to disabled", func(t *testing.T) {
|
||||||
@@ -1641,9 +1620,7 @@ func TestIntegrationService_getTimeout(t *testing.T) {
|
|||||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
features := featuremgmt.WithFeatures()
|
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil)
|
||||||
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@@ -1668,9 +1645,7 @@ func TestIntegrationService_GetDecryptedValues(t *testing.T) {
|
|||||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
features := featuremgmt.WithFeatures()
|
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil)
|
||||||
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
jsonData := map[string]string{
|
jsonData := map[string]string{
|
||||||
@@ -1698,9 +1673,7 @@ func TestIntegrationService_GetDecryptedValues(t *testing.T) {
|
|||||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
features := featuremgmt.WithFeatures()
|
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil)
|
||||||
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
jsonData := map[string]string{
|
jsonData := map[string]string{
|
||||||
@@ -1726,9 +1699,7 @@ func TestIntegrationDataSource_CustomHeaders(t *testing.T) {
|
|||||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
features := featuremgmt.WithFeatures()
|
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil)
|
||||||
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, features, acmock.New(), acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil, dsRetriever)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
dsService.cfg = setting.NewCfg()
|
dsService.cfg = setting.NewCfg()
|
||||||
@@ -1817,9 +1788,7 @@ func initDSService(t *testing.T) *Service {
|
|||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
mockPermission := acmock.NewMockedPermissionsService()
|
mockPermission := acmock.NewMockedPermissionsService()
|
||||||
mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
|
mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
|
||||||
features := featuremgmt.WithFeatures()
|
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{
|
||||||
dsRetriever := ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{
|
|
||||||
PluginList: []pluginstore.Plugin{{
|
PluginList: []pluginstore.Plugin{{
|
||||||
JSONData: plugins.JSONData{
|
JSONData: plugins.JSONData{
|
||||||
ID: "test",
|
ID: "test",
|
||||||
@@ -1839,7 +1808,7 @@ func initDSService(t *testing.T) *Service {
|
|||||||
ObjectBytes: req.ObjectBytes,
|
ObjectBytes: req.ObjectBytes,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
}, plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()), dsRetriever)
|
}, plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return dsService
|
return dsService
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DataSourceRetrieverImpl implements DataSourceRetriever by delegating to a Store.
|
|
||||||
type DataSourceRetrieverImpl struct {
|
|
||||||
store Store
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ DataSourceRetriever = (*DataSourceRetrieverImpl)(nil)
|
|
||||||
|
|
||||||
// ProvideDataSourceRetriever creates a DataSourceRetriever for wire injection.
|
|
||||||
func ProvideDataSourceRetriever(db db.DB, features featuremgmt.FeatureToggles) DataSourceRetriever {
|
|
||||||
dslogger := log.New("datasources-retriever")
|
|
||||||
store := &SqlStore{db: db, logger: dslogger, features: features}
|
|
||||||
return &DataSourceRetrieverImpl{store: store}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDataSource gets a datasource.
|
|
||||||
func (r *DataSourceRetrieverImpl) GetDataSource(ctx context.Context, query *datasources.GetDataSourceQuery) (*datasources.DataSource, error) {
|
|
||||||
return r.store.GetDataSource(ctx, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDataSourceInNamespace gets a datasource by namespace, name (datasource uid), and group (datasource type).
|
|
||||||
func (r *DataSourceRetrieverImpl) GetDataSourceInNamespace(ctx context.Context, namespace, name, group string) (*datasources.DataSource, error) {
|
|
||||||
return r.store.GetDataSourceInNamespace(ctx, namespace, name, group)
|
|
||||||
}
|
|
||||||
@@ -542,10 +542,9 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe
|
|||||||
dashService.RegisterDashboardPermissions(acmock.NewMockedPermissionsService())
|
dashService.RegisterDashboardPermissions(acmock.NewMockedPermissionsService())
|
||||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
||||||
dsRetriever := dsservice.ProvideDataSourceRetriever(sqlStore, featuremgmt.WithFeatures())
|
|
||||||
_, err = dsservice.ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(),
|
_, err = dsservice.ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(),
|
||||||
quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, plugincontext.
|
quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, plugincontext.
|
||||||
ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()), dsRetriever)
|
ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
m := metrics.NewNGAlert(prometheus.NewRegistry())
|
m := metrics.NewNGAlert(prometheus.NewRegistry())
|
||||||
|
|
||||||
|
|||||||
@@ -37,10 +37,9 @@ func SetupTestDataSourceSecretMigrationService(t *testing.T, sqlStore db.DB, kvS
|
|||||||
features := featuremgmt.WithFeatures()
|
features := featuremgmt.WithFeatures()
|
||||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
dsRetriever := dsservice.ProvideDataSourceRetriever(sqlStore, features)
|
|
||||||
dsService, err := dsservice.ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(),
|
dsService, err := dsservice.ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New(),
|
||||||
acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{},
|
acmock.NewMockedPermissionsService(), quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{},
|
||||||
plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()), dsRetriever)
|
plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
migService := ProvideDataSourceMigrationService(dsService, kvStore, features)
|
migService := ProvideDataSourceMigrationService(dsService, kvStore, features)
|
||||||
return migService
|
return migService
|
||||||
|
|||||||
@@ -293,15 +293,15 @@ overrides_path = overrides.yaml
|
|||||||
overrides_reload_period = 5s
|
overrides_reload_period = 5s
|
||||||
```
|
```
|
||||||
|
|
||||||
To override the default quota for a tenant, add the following to the `overrides.yaml` file:
|
To overrides the default quota for a tenant, add the following to the overrides.yaml file:
|
||||||
```yaml
|
```yaml
|
||||||
overrides:
|
overrides:
|
||||||
<NAMESPACE>:
|
<NAMESPACE>:
|
||||||
quotas:
|
quotas:
|
||||||
<GROUP>/<RESOURCE>:
|
<GROUP>.<RESOURCE>:
|
||||||
limit: 10
|
limit: 10
|
||||||
```
|
```
|
||||||
Unless otherwise set, the `NAMESPACE` when running locally is `default`.
|
Unless otherwise set, the NAMESPACE when running locally is `default`.
|
||||||
|
|
||||||
To access quotas, use the following API endpoint:
|
To access quotas, use the following API endpoint:
|
||||||
```
|
```
|
||||||
@@ -806,10 +806,8 @@ flowchart TD
|
|||||||
|
|
||||||
#### Setting Dual Writer Mode
|
#### Setting Dual Writer Mode
|
||||||
```ini
|
```ini
|
||||||
; [unified_storage.{resource}.{group}]
|
[unified_storage.{resource}.{kind}.{group}]
|
||||||
[unified_storage.dashboards.dashboard.grafana.app]
|
dualWriterMode = {0-5}
|
||||||
; modes {0-5}
|
|
||||||
dualWriterMode = 0
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Background Sync Configuration
|
#### Background Sync Configuration
|
||||||
@@ -1378,3 +1376,4 @@ disable_data_migrations = false
|
|||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
For detailed information about migration architecture, validators, and troubleshooting, refer to [migrations/README.md](./migrations/README.md).
|
For detailed information about migration architecture, validators, and troubleshooting, refer to [migrations/README.md](./migrations/README.md).
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ INSERT INTO {{ .Ident "resource" }}
|
|||||||
{{ .Ident "previous_resource_version" }}
|
{{ .Ident "previous_resource_version" }}
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
(SELECT {{ .Ident "value" }} FROM {{ .Ident "resource_history" }} WHERE {{ .Ident "guid" }} = {{ .Arg .GUID }}),
|
COALESCE({{ .Arg .Value }}, ""),
|
||||||
{{ .Arg .GUID }},
|
{{ .Arg .GUID }},
|
||||||
{{ .Arg .Group }},
|
{{ .Arg .Group }},
|
||||||
{{ .Arg .Resource }},
|
{{ .Arg .Resource }},
|
||||||
@@ -19,5 +19,13 @@ VALUES (
|
|||||||
{{ .Arg .Name }},
|
{{ .Arg .Name }},
|
||||||
{{ .Arg .Action }},
|
{{ .Arg .Action }},
|
||||||
{{ .Arg .Folder }},
|
{{ .Arg .Folder }},
|
||||||
{{ .Arg .PreviousRV }}
|
CASE WHEN {{ .Arg .Action }} = 1 THEN 0 ELSE (
|
||||||
|
SELECT {{ .Ident "resource_version" }}
|
||||||
|
FROM {{ .Ident "resource" }}
|
||||||
|
WHERE {{ .Ident "group" }} = {{ .Arg .Group }}
|
||||||
|
AND {{ .Ident "resource" }} = {{ .Arg .Resource }}
|
||||||
|
AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }}
|
||||||
|
AND {{ .Ident "name" }} = {{ .Arg .Name }}
|
||||||
|
ORDER BY {{ .Ident "resource_version" }} DESC LIMIT 1
|
||||||
|
) END
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ INSERT INTO {{ .Ident "resource_history" }}
|
|||||||
{{ .Ident "namespace" }},
|
{{ .Ident "namespace" }},
|
||||||
{{ .Ident "name" }},
|
{{ .Ident "name" }},
|
||||||
{{ .Ident "action" }},
|
{{ .Ident "action" }},
|
||||||
{{ .Ident "folder" }}
|
{{ .Ident "folder" }},
|
||||||
|
{{ .Ident "previous_resource_version" }},
|
||||||
|
{{ .Ident "generation" }}
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
COALESCE({{ .Arg .Value }}, ""),
|
COALESCE({{ .Arg .Value }}, ""),
|
||||||
@@ -17,5 +19,26 @@ VALUES (
|
|||||||
{{ .Arg .Namespace }},
|
{{ .Arg .Namespace }},
|
||||||
{{ .Arg .Name }},
|
{{ .Arg .Name }},
|
||||||
{{ .Arg .Action }},
|
{{ .Arg .Action }},
|
||||||
{{ .Arg .Folder }}
|
{{ .Arg .Folder }},
|
||||||
|
CASE WHEN {{ .Arg .Action }} = 1 THEN 0 ELSE (
|
||||||
|
SELECT {{ .Ident "resource_version" }}
|
||||||
|
FROM {{ .Ident "resource_history" }}
|
||||||
|
WHERE {{ .Ident "group" }} = {{ .Arg .Group }}
|
||||||
|
AND {{ .Ident "resource" }} = {{ .Arg .Resource }}
|
||||||
|
AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }}
|
||||||
|
AND {{ .Ident "name" }} = {{ .Arg .Name }}
|
||||||
|
ORDER BY {{ .Ident "resource_version" }} DESC LIMIT 1
|
||||||
|
) END,
|
||||||
|
CASE
|
||||||
|
WHEN {{ .Arg .Action }} = 1 THEN 1
|
||||||
|
WHEN {{ .Arg .Action }} = 3 THEN 0
|
||||||
|
ELSE 1 + (
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM {{ .Ident "resource_history" }}
|
||||||
|
WHERE {{ .Ident "group" }} = {{ .Arg .Group }}
|
||||||
|
AND {{ .Ident "resource" }} = {{ .Arg .Resource }}
|
||||||
|
AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }}
|
||||||
|
AND {{ .Ident "name" }} = {{ .Arg .Name }}
|
||||||
|
)
|
||||||
|
END
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
UPDATE {{ .Ident "resource" }}
|
UPDATE {{ .Ident "resource" }}
|
||||||
SET
|
SET
|
||||||
{{ .Ident "guid" }} = {{ .Arg .GUID }},
|
{{ .Ident "value" }} = {{ .Arg .Value }},
|
||||||
{{ .Ident "value" }} = (SELECT {{ .Ident "value" }} FROM {{ .Ident "resource_history" }} WHERE {{ .Ident "guid" }} = {{ .Arg .GUID }}),
|
|
||||||
{{ .Ident "action" }} = {{ .Arg .Action }},
|
{{ .Ident "action" }} = {{ .Arg .Action }},
|
||||||
{{ .Ident "folder" }} = {{ .Arg .Folder }},
|
{{ .Ident "folder" }} = {{ .Arg .Folder }}
|
||||||
{{ .Ident "previous_resource_version" }} = {{ .Arg .PreviousRV }}
|
|
||||||
WHERE {{ .Ident "group" }} = {{ .Arg .Group }}
|
WHERE {{ .Ident "group" }} = {{ .Arg .Group }}
|
||||||
AND {{ .Ident "resource" }} = {{ .Arg .Resource }}
|
AND {{ .Ident "resource" }} = {{ .Arg .Resource }}
|
||||||
AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }}
|
AND {{ .Ident "namespace" }} = {{ .Arg .Namespace }}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
UPDATE {{ .Ident "resource_history" }}
|
|
||||||
SET
|
|
||||||
{{ .Ident "previous_resource_version" }} = {{ .Arg .PreviousRV }},
|
|
||||||
{{ .Ident "generation" }} = {{ .Arg .Generation }}
|
|
||||||
WHERE {{ .Ident "guid" }} = {{ .Arg .GUID }};
|
|
||||||
@@ -12,9 +12,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/validation"
|
"github.com/grafana/grafana/pkg/apimachinery/validation"
|
||||||
"github.com/grafana/grafana/pkg/storage/unified/sql/db"
|
|
||||||
"github.com/grafana/grafana/pkg/storage/unified/sql/dbutil"
|
|
||||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
|
||||||
gocache "github.com/patrickmn/go-cache"
|
gocache "github.com/patrickmn/go-cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -309,6 +306,10 @@ func (d *dataStore) GetResourceKeyAtRevision(ctx context.Context, key GetRequest
|
|||||||
return DataKey{}, fmt.Errorf("invalid get request key: %w", err)
|
return DataKey{}, fmt.Errorf("invalid get request key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rv == 0 {
|
||||||
|
rv = math.MaxInt64
|
||||||
|
}
|
||||||
|
|
||||||
listKey := ListRequestKey(key)
|
listKey := ListRequestKey(key)
|
||||||
|
|
||||||
iter := d.ListResourceKeysAtRevision(ctx, ListRequestOptions{Key: listKey, ResourceVersion: rv})
|
iter := d.ListResourceKeysAtRevision(ctx, ListRequestOptions{Key: listKey, ResourceVersion: rv})
|
||||||
@@ -597,7 +598,7 @@ func ParseKey(key string) (DataKey, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporary while we need to support unified/sql/backend compatibility.
|
// Temporary while we need to support unified/sql/backend compatibility
|
||||||
// Remove once we stop using RvManager in storage_backend.go
|
// Remove once we stop using RvManager in storage_backend.go
|
||||||
func ParseKeyWithGUID(key string) (DataKey, error) {
|
func ParseKeyWithGUID(key string) (DataKey, error) {
|
||||||
parts := strings.Split(key, "/")
|
parts := strings.Split(key, "/")
|
||||||
@@ -814,121 +815,3 @@ func (d *dataStore) getGroupResources(ctx context.Context) ([]GroupResource, err
|
|||||||
|
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove when backwards compatibility is no longer needed.
|
|
||||||
var (
|
|
||||||
sqlKVUpdateLegacyResourceHistory = mustTemplate("sqlkv_update_legacy_resource_history.sql")
|
|
||||||
sqlKVInsertLegacyResource = mustTemplate("sqlkv_insert_legacy_resource.sql")
|
|
||||||
sqlKVUpdateLegacyResource = mustTemplate("sqlkv_update_legacy_resource.sql")
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: remove when backwards compatibility is no longer needed.
|
|
||||||
type sqlKVLegacySaveRequest struct {
|
|
||||||
sqltemplate.SQLTemplate
|
|
||||||
GUID string
|
|
||||||
Group string
|
|
||||||
Resource string
|
|
||||||
Namespace string
|
|
||||||
Name string
|
|
||||||
Action int64
|
|
||||||
Folder string
|
|
||||||
PreviousRV int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (req sqlKVLegacySaveRequest) Validate() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: remove when backwards compatibility is no longer needed.
|
|
||||||
type sqlKVLegacyUpdateHistoryRequest struct {
|
|
||||||
sqltemplate.SQLTemplate
|
|
||||||
GUID string
|
|
||||||
PreviousRV int64
|
|
||||||
Generation int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (req sqlKVLegacyUpdateHistoryRequest) Validate() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// applyBackwardsCompatibleChanges updates the `resource` and `resource_history` tables
|
|
||||||
// to make sure the sqlkv implementation is backwards-compatible with the existing sql backend.
|
|
||||||
// Specifically, it will update the `resource_history` table to include the previous resource version
|
|
||||||
// and generation, which come from the `WriteEvent`, and also make the corresponding change on the
|
|
||||||
// `resource` table, no longer used in the storage backend.
|
|
||||||
//
|
|
||||||
// TODO: remove when backwards compatibility is no longer needed.
|
|
||||||
func (d *dataStore) applyBackwardsCompatibleChanges(ctx context.Context, tx db.Tx, event WriteEvent, key DataKey) error {
|
|
||||||
kv, isSQLKV := d.kv.(*sqlKV)
|
|
||||||
if !isSQLKV {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := dbutil.Exec(ctx, tx, sqlKVUpdateLegacyResourceHistory, sqlKVLegacyUpdateHistoryRequest{
|
|
||||||
SQLTemplate: sqltemplate.New(kv.dialect),
|
|
||||||
GUID: key.GUID,
|
|
||||||
PreviousRV: event.PreviousRV,
|
|
||||||
Generation: event.Object.GetGeneration(),
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("compatibility layer: failed to insert to resource: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var action int64
|
|
||||||
switch key.Action {
|
|
||||||
case DataActionCreated:
|
|
||||||
action = 1
|
|
||||||
case DataActionUpdated:
|
|
||||||
action = 2
|
|
||||||
case DataActionDeleted:
|
|
||||||
action = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
switch key.Action {
|
|
||||||
case DataActionCreated:
|
|
||||||
_, err := dbutil.Exec(ctx, tx, sqlKVInsertLegacyResource, sqlKVLegacySaveRequest{
|
|
||||||
SQLTemplate: sqltemplate.New(kv.dialect),
|
|
||||||
GUID: key.GUID,
|
|
||||||
Group: key.Group,
|
|
||||||
Resource: key.Resource,
|
|
||||||
Namespace: key.Namespace,
|
|
||||||
Name: key.Name,
|
|
||||||
Action: action,
|
|
||||||
Folder: key.Folder,
|
|
||||||
PreviousRV: event.PreviousRV,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("compatibility layer: failed to insert to resource: %w", err)
|
|
||||||
}
|
|
||||||
case DataActionUpdated:
|
|
||||||
_, err := dbutil.Exec(ctx, tx, sqlKVUpdateLegacyResource, sqlKVLegacySaveRequest{
|
|
||||||
SQLTemplate: sqltemplate.New(kv.dialect),
|
|
||||||
GUID: key.GUID,
|
|
||||||
Group: key.Group,
|
|
||||||
Resource: key.Resource,
|
|
||||||
Namespace: key.Namespace,
|
|
||||||
Name: key.Name,
|
|
||||||
Folder: key.Folder,
|
|
||||||
PreviousRV: event.PreviousRV,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("compatibility layer: failed to update resource: %w", err)
|
|
||||||
}
|
|
||||||
case DataActionDeleted:
|
|
||||||
_, err := dbutil.Exec(ctx, tx, sqlKVDeleteLegacyResource, sqlKVLegacySaveRequest{
|
|
||||||
SQLTemplate: sqltemplate.New(kv.dialect),
|
|
||||||
Resource: key.Resource,
|
|
||||||
Namespace: key.Namespace,
|
|
||||||
Name: key.Name,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("compatibility layer: failed to delete from resource: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ var (
|
|||||||
sqlKVInsertData = mustTemplate("sqlkv_insert_datastore.sql")
|
sqlKVInsertData = mustTemplate("sqlkv_insert_datastore.sql")
|
||||||
sqlKVUpdateData = mustTemplate("sqlkv_update_datastore.sql")
|
sqlKVUpdateData = mustTemplate("sqlkv_update_datastore.sql")
|
||||||
sqlKVInsertLegacyResourceHistory = mustTemplate("sqlkv_insert_legacy_resource_history.sql")
|
sqlKVInsertLegacyResourceHistory = mustTemplate("sqlkv_insert_legacy_resource_history.sql")
|
||||||
|
sqlKVInsertLegacyResource = mustTemplate("sqlkv_insert_legacy_resource.sql")
|
||||||
|
sqlKVUpdateLegacyResource = mustTemplate("sqlkv_update_legacy_resource.sql")
|
||||||
sqlKVDeleteLegacyResource = mustTemplate("sqlkv_delete_legacy_resource.sql")
|
sqlKVDeleteLegacyResource = mustTemplate("sqlkv_delete_legacy_resource.sql")
|
||||||
sqlKVDelete = mustTemplate("sqlkv_delete.sql")
|
sqlKVDelete = mustTemplate("sqlkv_delete.sql")
|
||||||
sqlKVBatchDelete = mustTemplate("sqlkv_batch_delete.sql")
|
sqlKVBatchDelete = mustTemplate("sqlkv_batch_delete.sql")
|
||||||
@@ -155,6 +157,26 @@ func (req sqlKVSaveRequest) Validate() error {
|
|||||||
return req.sqlKVSectionKey.Validate()
|
return req.sqlKVSectionKey.Validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type sqlKVLegacySaveRequest struct {
|
||||||
|
sqltemplate.SQLTemplate
|
||||||
|
Value []byte
|
||||||
|
GUID string
|
||||||
|
Group string
|
||||||
|
Resource string
|
||||||
|
Namespace string
|
||||||
|
Name string
|
||||||
|
Action int64
|
||||||
|
Folder string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req sqlKVLegacySaveRequest) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req sqlKVLegacySaveRequest) Results() ([]byte, error) {
|
||||||
|
return req.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
type sqlKVKeysRequest struct {
|
type sqlKVKeysRequest struct {
|
||||||
sqltemplate.SQLTemplate
|
sqltemplate.SQLTemplate
|
||||||
sqlKVSection
|
sqlKVSection
|
||||||
@@ -370,7 +392,7 @@ func (w *sqlWriteCloser) Close() error {
|
|||||||
// used to keep backwards compatibility between sql-based kvstore and unified/sql/backend
|
// used to keep backwards compatibility between sql-based kvstore and unified/sql/backend
|
||||||
tx, ok := rvmanager.TxFromCtx(w.ctx)
|
tx, ok := rvmanager.TxFromCtx(w.ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
// temporary save for dataStore without rvmanager (non backwards-compatible)
|
// temporary save for dataStore without rvmanager
|
||||||
// we can use the same template as the event one after we:
|
// we can use the same template as the event one after we:
|
||||||
// - move PK from GUID to key_path
|
// - move PK from GUID to key_path
|
||||||
// - remove all unnecessary columns (or at least their NOT NULL constraints)
|
// - remove all unnecessary columns (or at least their NOT NULL constraints)
|
||||||
@@ -407,12 +429,11 @@ func (w *sqlWriteCloser) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// special, temporary backwards-compatible save that includes all the fields in resource_history that are not relevant
|
// special, temporary save that includes all the fields in resource_history that are not relevant for the kvstore,
|
||||||
// for the kvstore, as well as the resource table. This is only called if an RvManager was passed to storage_backend, as that
|
// as well as the resource table. This is only called if an RvManager was passed to storage_backend, as that
|
||||||
// component will be responsible for populating the resource_version and key_path columns.
|
// component will be responsible for populating the resource_version and key_path columns
|
||||||
// For full backwards-compatibility, the `Save` function needs to be called within a callback that updates the resource_history
|
// note that we are not touching resource_version table, neither the resource_version columns or the key_path column
|
||||||
// table with `previous_resource_version` and `generation` and updates the `resource` table accordingly. See the
|
// as the RvManager will be responsible for this
|
||||||
// storage_backend for the full implementation.
|
|
||||||
dataKey, err := ParseKeyWithGUID(w.sectionKey.Key)
|
dataKey, err := ParseKeyWithGUID(w.sectionKey.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse key: %w", err)
|
return fmt.Errorf("failed to parse key: %w", err)
|
||||||
@@ -427,7 +448,7 @@ func (w *sqlWriteCloser) Close() error {
|
|||||||
case DataActionDeleted:
|
case DataActionDeleted:
|
||||||
action = 3
|
action = 3
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("failed to parse key: invalid action")
|
return fmt.Errorf("failed to parse key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = dbutil.Exec(w.ctx, tx, sqlKVInsertLegacyResourceHistory, sqlKVSaveRequest{
|
_, err = dbutil.Exec(w.ctx, tx, sqlKVInsertLegacyResourceHistory, sqlKVSaveRequest{
|
||||||
@@ -447,6 +468,52 @@ func (w *sqlWriteCloser) Close() error {
|
|||||||
return fmt.Errorf("failed to save to resource_history: %w", err)
|
return fmt.Errorf("failed to save to resource_history: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch dataKey.Action {
|
||||||
|
case DataActionCreated:
|
||||||
|
_, err = dbutil.Exec(w.ctx, tx, sqlKVInsertLegacyResource, sqlKVLegacySaveRequest{
|
||||||
|
SQLTemplate: sqltemplate.New(w.kv.dialect),
|
||||||
|
Value: w.buf.Bytes(),
|
||||||
|
GUID: dataKey.GUID,
|
||||||
|
Group: dataKey.Group,
|
||||||
|
Resource: dataKey.Resource,
|
||||||
|
Namespace: dataKey.Namespace,
|
||||||
|
Name: dataKey.Name,
|
||||||
|
Action: action,
|
||||||
|
Folder: dataKey.Folder,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to insert to resource: %w", err)
|
||||||
|
}
|
||||||
|
case DataActionUpdated:
|
||||||
|
_, err = dbutil.Exec(w.ctx, tx, sqlKVUpdateLegacyResource, sqlKVLegacySaveRequest{
|
||||||
|
SQLTemplate: sqltemplate.New(w.kv.dialect),
|
||||||
|
Value: w.buf.Bytes(),
|
||||||
|
Group: dataKey.Group,
|
||||||
|
Resource: dataKey.Resource,
|
||||||
|
Namespace: dataKey.Namespace,
|
||||||
|
Name: dataKey.Name,
|
||||||
|
Action: action,
|
||||||
|
Folder: dataKey.Folder,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update resource: %w", err)
|
||||||
|
}
|
||||||
|
case DataActionDeleted:
|
||||||
|
_, err = dbutil.Exec(w.ctx, tx, sqlKVDeleteLegacyResource, sqlKVLegacySaveRequest{
|
||||||
|
SQLTemplate: sqltemplate.New(w.kv.dialect),
|
||||||
|
Group: dataKey.Group,
|
||||||
|
Resource: dataKey.Resource,
|
||||||
|
Namespace: dataKey.Namespace,
|
||||||
|
Name: dataKey.Name,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete from resource: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -332,14 +332,11 @@ func (k *kvStorageBackend) WriteEvent(ctx context.Context, event WriteEvent) (in
|
|||||||
dataKey.GUID = uuid.New().String()
|
dataKey.GUID = uuid.New().String()
|
||||||
var err error
|
var err error
|
||||||
rv, err = k.rvManager.ExecWithRV(ctx, event.Key, func(tx db.Tx) (string, error) {
|
rv, err = k.rvManager.ExecWithRV(ctx, event.Key, func(tx db.Tx) (string, error) {
|
||||||
if err := k.dataStore.Save(rvmanager.ContextWithTx(ctx, tx), dataKey, bytes.NewReader(event.Value)); err != nil {
|
err := k.dataStore.Save(rvmanager.ContextWithTx(ctx, tx), dataKey, bytes.NewReader(event.Value))
|
||||||
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to write data: %w", err)
|
return "", fmt.Errorf("failed to write data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := k.dataStore.applyBackwardsCompatibleChanges(ctx, tx, event, dataKey); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to apply backwards compatible updates: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataKey.GUID, nil
|
return dataKey.GUID, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
144
pkg/tests/apis/config_test.go
Normal file
144
pkg/tests/apis/config_test.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package apis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||||
|
"github.com/grafana/grafana/pkg/util/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
const pluginsDiscoveryJSON = `[
|
||||||
|
{
|
||||||
|
"version": "v0alpha1",
|
||||||
|
"freshness": "Current",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"resource": "metas",
|
||||||
|
"responseKind": {
|
||||||
|
"group": "",
|
||||||
|
"kind": "Meta",
|
||||||
|
"version": ""
|
||||||
|
},
|
||||||
|
"scope": "Namespaced",
|
||||||
|
"singularResource": "meta",
|
||||||
|
"subresources": [
|
||||||
|
{
|
||||||
|
"responseKind": {
|
||||||
|
"group": "",
|
||||||
|
"kind": "Meta",
|
||||||
|
"version": ""
|
||||||
|
},
|
||||||
|
"subresource": "status",
|
||||||
|
"verbs": [
|
||||||
|
"get",
|
||||||
|
"patch",
|
||||||
|
"update"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"verbs": [
|
||||||
|
"get",
|
||||||
|
"list"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resource": "plugins",
|
||||||
|
"responseKind": {
|
||||||
|
"group": "",
|
||||||
|
"kind": "Plugin",
|
||||||
|
"version": ""
|
||||||
|
},
|
||||||
|
"scope": "Namespaced",
|
||||||
|
"singularResource": "plugin",
|
||||||
|
"subresources": [
|
||||||
|
{
|
||||||
|
"responseKind": {
|
||||||
|
"group": "",
|
||||||
|
"kind": "Plugin",
|
||||||
|
"version": ""
|
||||||
|
},
|
||||||
|
"subresource": "status",
|
||||||
|
"verbs": [
|
||||||
|
"get",
|
||||||
|
"patch",
|
||||||
|
"update"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"verbs": [
|
||||||
|
"create",
|
||||||
|
"delete",
|
||||||
|
"deletecollection",
|
||||||
|
"get",
|
||||||
|
"list",
|
||||||
|
"patch",
|
||||||
|
"update",
|
||||||
|
"watch"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]`
|
||||||
|
|
||||||
|
func setupHelper(t *testing.T, openFeatureAPIEnabled bool) *K8sTestHelper {
|
||||||
|
t.Helper()
|
||||||
|
helper := NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||||
|
AppModeProduction: true,
|
||||||
|
DisableAnonymous: true,
|
||||||
|
APIServerRuntimeConfig: "plugins.grafana.app/v0alpha1=true",
|
||||||
|
OpenFeatureAPIEnabled: openFeatureAPIEnabled,
|
||||||
|
})
|
||||||
|
t.Cleanup(func() { helper.Shutdown() })
|
||||||
|
return helper
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegrationAPIServerRuntimeConfig(t *testing.T) {
|
||||||
|
testutil.SkipIntegrationTestInShortMode(t)
|
||||||
|
|
||||||
|
t.Run("discovery with openfeature api enabled", func(t *testing.T) {
|
||||||
|
helper := setupHelper(t, true)
|
||||||
|
disco, err := helper.GetGroupVersionInfoJSON("features.grafana.app")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, `[
|
||||||
|
{
|
||||||
|
"freshness": "Current",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"resource": "noop",
|
||||||
|
"responseKind": {
|
||||||
|
"group": "",
|
||||||
|
"kind": "Status",
|
||||||
|
"version": ""
|
||||||
|
},
|
||||||
|
"scope": "Namespaced",
|
||||||
|
"singularResource": "noop",
|
||||||
|
"verbs": [
|
||||||
|
"get"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": "v0alpha1"
|
||||||
|
}
|
||||||
|
]`, disco)
|
||||||
|
|
||||||
|
// plugins should still be discoverable
|
||||||
|
disco, err = helper.GetGroupVersionInfoJSON("plugins.grafana.app")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, pluginsDiscoveryJSON, disco)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("discovery with openfeature api false", func(t *testing.T) {
|
||||||
|
helper := setupHelper(t, false)
|
||||||
|
_, err := helper.GetGroupVersionInfoJSON("features.grafana.app")
|
||||||
|
require.Error(t, err, "expected error when openfeature api is disabled")
|
||||||
|
|
||||||
|
// plugins should still be discoverable
|
||||||
|
disco, err := helper.GetGroupVersionInfoJSON("plugins.grafana.app")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, pluginsDiscoveryJSON, disco)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
||||||
"github.com/grafana/grafana/pkg/tests/apis"
|
"github.com/grafana/grafana/pkg/tests/apis"
|
||||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||||
@@ -178,9 +177,6 @@ func setupHelper(t *testing.T) *apis.K8sTestHelper {
|
|||||||
AppModeProduction: true,
|
AppModeProduction: true,
|
||||||
DisableAnonymous: true,
|
DisableAnonymous: true,
|
||||||
APIServerRuntimeConfig: "plugins.grafana.app/v0alpha1=true",
|
APIServerRuntimeConfig: "plugins.grafana.app/v0alpha1=true",
|
||||||
EnableFeatureToggles: []string{
|
|
||||||
featuremgmt.FlagPluginStoreServiceLoading,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
t.Cleanup(func() { helper.Shutdown() })
|
t.Cleanup(func() { helper.Shutdown() })
|
||||||
return helper
|
return helper
|
||||||
|
|||||||
@@ -320,9 +320,8 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = openFeatureSect.NewKey("enable_api", strconv.FormatBool(opts.OpenFeatureAPIEnabled))
|
_, err = openFeatureSect.NewKey("enable_api", strconv.FormatBool(opts.OpenFeatureAPIEnabled))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
if !opts.OpenFeatureAPIEnabled {
|
||||||
if opts.OpenFeatureAPIEnabled {
|
_, err = openFeatureSect.NewKey("provider", "static") // in practice, APIEnabled being false goes with features-service type, but trying to make tests work
|
||||||
_, err = openFeatureSect.NewKey("provider", "static")
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = openFeatureSect.NewKey("targetingKey", "grafana")
|
_, err = openFeatureSect.NewKey("targetingKey", "grafana")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const getFormFieldsForSilence = (silence: Silence): SilenceFormFields =>
|
|||||||
startsAt: interval.start.toISOString(),
|
startsAt: interval.start.toISOString(),
|
||||||
endsAt: interval.end.toISOString(),
|
endsAt: interval.end.toISOString(),
|
||||||
comment: silence.comment,
|
comment: silence.comment,
|
||||||
createdBy: isExpired ? contextSrv.user.name : silence.createdBy,
|
createdBy: silence.createdBy,
|
||||||
duration: intervalToAbbreviatedDurationString(interval),
|
duration: intervalToAbbreviatedDurationString(interval),
|
||||||
isRegex: false,
|
isRegex: false,
|
||||||
matchers: silence.matchers?.map(matcherToMatcherField) || [],
|
matchers: silence.matchers?.map(matcherToMatcherField) || [],
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function RecentlyViewedDashboards() {
|
|||||||
retry();
|
retry();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!evaluateBooleanFlag('recentlyViewedDashboards', false) || recentDashboards.length === 0) {
|
if (!evaluateBooleanFlag('recentlyViewedDashboards', false)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +76,10 @@ export function RecentlyViewedDashboards() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{loading && <Spinner />}
|
{loading && <Spinner />}
|
||||||
|
{/* TODO: Better empty state https://github.com/grafana/grafana/issues/114804 */}
|
||||||
|
{!loading && recentDashboards.length === 0 && (
|
||||||
|
<Text>{t('browse-dashboards.recently-viewed.empty', 'Nothing viewed yet')}</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
{!loading && recentDashboards.length > 0 && (
|
{!loading && recentDashboards.length > 0 && (
|
||||||
<ul className={styles.list}>
|
<ul className={styles.list}>
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ describe('PanelTimeRange', () => {
|
|||||||
expect(panelTime.state.value.to.format('Z')).toBe('+00:00'); // UTC
|
expect(panelTime.state.value.to.format('Z')).toBe('+00:00'); // UTC
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle invalid time reference in timeShift with relative time range', () => {
|
it('should handle invalid time reference in timeShift', () => {
|
||||||
const panelTime = new PanelTimeRange({ timeShift: 'now-1d' });
|
const panelTime = new PanelTimeRange({ timeShift: 'now-1d' });
|
||||||
|
|
||||||
buildAndActivateSceneFor(panelTime);
|
buildAndActivateSceneFor(panelTime);
|
||||||
@@ -139,22 +139,6 @@ describe('PanelTimeRange', () => {
|
|||||||
expect(panelTime.state.to).toBe('now');
|
expect(panelTime.state.to).toBe('now');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle invalid time reference in timeShift with absolute time range', () => {
|
|
||||||
const panelTime = new PanelTimeRange({ timeShift: 'now-1d' });
|
|
||||||
const panel = new SceneCanvasText({ text: 'Hello', $timeRange: panelTime });
|
|
||||||
const absoluteFrom = '2019-02-11T10:00:00.000Z';
|
|
||||||
const absoluteTo = '2019-02-11T16:00:00.000Z';
|
|
||||||
const scene = new SceneFlexLayout({
|
|
||||||
$timeRange: new SceneTimeRange({ from: absoluteFrom, to: absoluteTo }),
|
|
||||||
children: [new SceneFlexItem({ body: panel })],
|
|
||||||
});
|
|
||||||
activateFullSceneTree(scene);
|
|
||||||
|
|
||||||
expect(panelTime.state.timeInfo).toBe('invalid timeshift');
|
|
||||||
expect(panelTime.state.from).toBe(absoluteFrom);
|
|
||||||
expect(panelTime.state.to).toBe(absoluteTo);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle invalid time reference in timeShift combined with timeFrom', () => {
|
it('should handle invalid time reference in timeShift combined with timeFrom', () => {
|
||||||
const panelTime = new PanelTimeRange({
|
const panelTime = new PanelTimeRange({
|
||||||
timeFrom: 'now-2h',
|
timeFrom: 'now-2h',
|
||||||
@@ -169,66 +153,6 @@ describe('PanelTimeRange', () => {
|
|||||||
expect(panelTime.state.to).toBe('now');
|
expect(panelTime.state.to).toBe('now');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('from/to state format for liveNow compatibility', () => {
|
|
||||||
it('should store relative strings in from/to when timeShift is applied to relative time range', () => {
|
|
||||||
const panelTime = new PanelTimeRange({ timeShift: '2h' });
|
|
||||||
|
|
||||||
buildAndActivateSceneFor(panelTime);
|
|
||||||
|
|
||||||
expect(panelTime.state.from).toBe('now-6h-2h');
|
|
||||||
expect(panelTime.state.to).toBe('now-2h');
|
|
||||||
expect(panelTime.state.value.raw.from).toBe('now-6h-2h');
|
|
||||||
expect(panelTime.state.value.raw.to).toBe('now-2h');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should store relative strings when both timeFrom and timeShift are applied', () => {
|
|
||||||
const panelTime = new PanelTimeRange({ timeFrom: '2h', timeShift: '1h' });
|
|
||||||
|
|
||||||
buildAndActivateSceneFor(panelTime);
|
|
||||||
|
|
||||||
expect(panelTime.state.from).toBe('now-2h-1h');
|
|
||||||
expect(panelTime.state.to).toBe('now-1h');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should store ISO strings when timeShift is applied to absolute time range', () => {
|
|
||||||
const panelTime = new PanelTimeRange({ timeShift: '1h' });
|
|
||||||
const panel = new SceneCanvasText({ text: 'Hello', $timeRange: panelTime });
|
|
||||||
const absoluteFrom = '2019-02-11T10:00:00.000Z';
|
|
||||||
const absoluteTo = '2019-02-11T16:00:00.000Z';
|
|
||||||
const scene = new SceneFlexLayout({
|
|
||||||
$timeRange: new SceneTimeRange({ from: absoluteFrom, to: absoluteTo }),
|
|
||||||
children: [new SceneFlexItem({ body: panel })],
|
|
||||||
});
|
|
||||||
activateFullSceneTree(scene);
|
|
||||||
|
|
||||||
expect(panelTime.state.from).toBe('2019-02-11T09:00:00.000Z');
|
|
||||||
expect(panelTime.state.to).toBe('2019-02-11T15:00:00.000Z');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update from/to when ancestor time range changes', () => {
|
|
||||||
const panelTime = new PanelTimeRange({ timeShift: '1h' });
|
|
||||||
const sceneTimeRange = new SceneTimeRange({ from: 'now-6h', to: 'now' });
|
|
||||||
const panel = new SceneCanvasText({ text: 'Hello', $timeRange: panelTime });
|
|
||||||
const scene = new SceneFlexLayout({
|
|
||||||
$timeRange: sceneTimeRange,
|
|
||||||
children: [new SceneFlexItem({ body: panel })],
|
|
||||||
});
|
|
||||||
activateFullSceneTree(scene);
|
|
||||||
|
|
||||||
expect(panelTime.state.from).toBe('now-6h-1h');
|
|
||||||
expect(panelTime.state.to).toBe('now-1h');
|
|
||||||
|
|
||||||
sceneTimeRange.onTimeRangeChange({
|
|
||||||
from: dateTime('2019-02-11T12:00:00.000Z'),
|
|
||||||
to: dateTime('2019-02-11T18:00:00.000Z'),
|
|
||||||
raw: { from: 'now-12h', to: 'now' },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(panelTime.state.from).toBe('now-12h-1h');
|
|
||||||
expect(panelTime.state.to).toBe('now-1h');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('onTimeRangeChange', () => {
|
describe('onTimeRangeChange', () => {
|
||||||
it('should reverse timeShift when updating time range', () => {
|
it('should reverse timeShift when updating time range', () => {
|
||||||
const oneHourShift = '1h';
|
const oneHourShift = '1h';
|
||||||
|
|||||||
@@ -81,19 +81,7 @@ export class PanelTimeRange extends SceneTimeRangeTransformerBase<PanelTimeRange
|
|||||||
}
|
}
|
||||||
|
|
||||||
const overrideResult = this.getTimeOverride(timeRange.value);
|
const overrideResult = this.getTimeOverride(timeRange.value);
|
||||||
const { timeRange: overrideTimeRange } = overrideResult;
|
this.setState({ value: overrideResult.timeRange, timeInfo: overrideResult.timeInfo });
|
||||||
this.setState({
|
|
||||||
value: overrideTimeRange,
|
|
||||||
timeInfo: overrideResult.timeInfo,
|
|
||||||
from:
|
|
||||||
typeof overrideTimeRange.raw.from === 'string'
|
|
||||||
? overrideTimeRange.raw.from
|
|
||||||
: overrideTimeRange.raw.from.toISOString(),
|
|
||||||
to:
|
|
||||||
typeof overrideTimeRange.raw.to === 'string'
|
|
||||||
? overrideTimeRange.raw.to
|
|
||||||
: overrideTimeRange.raw.to.toISOString(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a time shifted request to compare with the primary request.
|
// Get a time shifted request to compare with the primary request.
|
||||||
@@ -165,10 +153,10 @@ export class PanelTimeRange extends SceneTimeRangeTransformerBase<PanelTimeRange
|
|||||||
|
|
||||||
// Only evaluate if the timeFrom if parent time is relative
|
// Only evaluate if the timeFrom if parent time is relative
|
||||||
if (rangeUtil.isRelativeTimeRange(parentTimeRange.raw)) {
|
if (rangeUtil.isRelativeTimeRange(parentTimeRange.raw)) {
|
||||||
const timezone = this.getTimeZone();
|
const timeZone = this.getTimeZone();
|
||||||
newTimeData.timeRange = {
|
newTimeData.timeRange = {
|
||||||
from: dateMath.toDateTime(timeFromInfo.from, { timezone })!,
|
from: dateMath.parse(timeFromInfo.from, undefined, timeZone)!,
|
||||||
to: dateMath.toDateTime(timeFromInfo.to, { timezone })!,
|
to: dateMath.parse(timeFromInfo.to, undefined, timeZone)!,
|
||||||
raw: { from: timeFromInfo.from, to: timeFromInfo.to },
|
raw: { from: timeFromInfo.from, to: timeFromInfo.to },
|
||||||
};
|
};
|
||||||
infoBlocks.push(timeFromInfo.display);
|
infoBlocks.push(timeFromInfo.display);
|
||||||
@@ -184,31 +172,11 @@ export class PanelTimeRange extends SceneTimeRangeTransformerBase<PanelTimeRange
|
|||||||
return newTimeData;
|
return newTimeData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shift = '-' + timeShiftInterpolated;
|
const timeShift = '-' + timeShiftInterpolated;
|
||||||
infoBlocks.push('timeshift ' + shift);
|
infoBlocks.push('timeshift ' + timeShift);
|
||||||
|
|
||||||
if (rangeUtil.isRelativeTimeRange(newTimeData.timeRange.raw)) {
|
const from = dateMath.parseDateMath(timeShift, newTimeData.timeRange.from, false)!;
|
||||||
const timezone = this.getTimeZone();
|
const to = dateMath.parseDateMath(timeShift, newTimeData.timeRange.to, true)!;
|
||||||
|
|
||||||
const rawFromShifted = `${newTimeData.timeRange.raw.from}${shift}`;
|
|
||||||
const rawToShifted = `${newTimeData.timeRange.raw.to}${shift}`;
|
|
||||||
|
|
||||||
const from = dateMath.toDateTime(rawFromShifted, { timezone });
|
|
||||||
const to = dateMath.toDateTime(rawToShifted, { timezone });
|
|
||||||
|
|
||||||
if (!from || !to) {
|
|
||||||
newTimeData.timeInfo = 'invalid timeshift';
|
|
||||||
return newTimeData;
|
|
||||||
}
|
|
||||||
|
|
||||||
newTimeData.timeRange = {
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
raw: { from: rawFromShifted, to: rawToShifted },
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const from = dateMath.parseDateMath(shift, newTimeData.timeRange.from, false);
|
|
||||||
const to = dateMath.parseDateMath(shift, newTimeData.timeRange.to, true);
|
|
||||||
|
|
||||||
if (!from || !to) {
|
if (!from || !to) {
|
||||||
newTimeData.timeInfo = 'invalid timeshift';
|
newTimeData.timeInfo = 'invalid timeshift';
|
||||||
@@ -217,7 +185,6 @@ export class PanelTimeRange extends SceneTimeRangeTransformerBase<PanelTimeRange
|
|||||||
|
|
||||||
newTimeData.timeRange = { from, to, raw: { from, to } };
|
newTimeData.timeRange = { from, to, raw: { from, to } };
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (compareWith) {
|
if (compareWith) {
|
||||||
const option = DEFAULT_COMPARE_OPTIONS.find((x) => x.value === compareWith);
|
const option = DEFAULT_COMPARE_OPTIONS.find((x) => x.value === compareWith);
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ function variableValueOptionsToVariableOptions(varState: MultiValueVariable['sta
|
|||||||
value: String(o.value),
|
value: String(o.value),
|
||||||
text: o.label,
|
text: o.label,
|
||||||
selected: Array.isArray(varState.value) ? varState.value.includes(o.value) : varState.value === o.value,
|
selected: Array.isArray(varState.value) ? varState.value.includes(o.value) : varState.value === o.value,
|
||||||
|
...(o.properties && { properties: o.properties }),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,18 +80,18 @@ const buildLabelPath = (label: string) => {
|
|||||||
return label.includes('.') || label.trim().includes(' ') ? `["${label}"]` : `.${label}`;
|
return label.includes('.') || label.trim().includes(' ') ? `["${label}"]` : `.${label}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVariableValueProperties = (variable: TypedVariableModel): string[] => {
|
const isRecord = (value: unknown): value is Record<string, unknown> => {
|
||||||
if (!('valuesFormat' in variable) || variable.valuesFormat !== 'json') {
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||||
return [];
|
};
|
||||||
}
|
|
||||||
|
|
||||||
function collectFieldPaths(option: Record<string, string>, currentPath: string) {
|
const getVariableValueProperties = (variable: TypedVariableModel): string[] => {
|
||||||
|
function collectFieldPaths(option: Record<string, unknown>, currentPath: string): string[] {
|
||||||
let paths: string[] = [];
|
let paths: string[] = [];
|
||||||
for (const field in option) {
|
for (const field in option) {
|
||||||
if (option.hasOwnProperty(field)) {
|
if (option.hasOwnProperty(field)) {
|
||||||
const newPath = `${currentPath}.${field}`;
|
const newPath = `${currentPath}.${field}`;
|
||||||
const value = option[field];
|
const value = option[field];
|
||||||
if (typeof value === 'object' && value !== null) {
|
if (isRecord(value)) {
|
||||||
paths = [...paths, ...collectFieldPaths(value, newPath)];
|
paths = [...paths, ...collectFieldPaths(value, newPath)];
|
||||||
}
|
}
|
||||||
paths.push(newPath);
|
paths.push(newPath);
|
||||||
@@ -100,11 +100,23 @@ const getVariableValueProperties = (variable: TypedVariableModel): string[] => {
|
|||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ('valuesFormat' in variable && variable.valuesFormat === 'json') {
|
||||||
try {
|
try {
|
||||||
return collectFieldPaths(JSON.parse(variable.query)[0], variable.name);
|
return collectFieldPaths(JSON.parse(variable.query)[0], variable.name);
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('options' in variable && Array.isArray(variable.options) && variable.options.length > 0) {
|
||||||
|
for (const opt of variable.options) {
|
||||||
|
if ('properties' in opt && isRecord(opt.properties) && Object.keys(opt.properties).length > 0) {
|
||||||
|
return collectFieldPaths(opt.properties, variable.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [
|
export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
SQLQuery,
|
SQLQuery,
|
||||||
SQLSelectableValue,
|
SQLSelectableValue,
|
||||||
SqlDatasource,
|
SqlDatasource,
|
||||||
|
SQLVariableSupport,
|
||||||
formatSQL,
|
formatSQL,
|
||||||
} from '@grafana/sql';
|
} from '@grafana/sql';
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ export class PostgresDatasource extends SqlDatasource {
|
|||||||
|
|
||||||
constructor(instanceSettings: DataSourceInstanceSettings<PostgresOptions>) {
|
constructor(instanceSettings: DataSourceInstanceSettings<PostgresOptions>) {
|
||||||
super(instanceSettings);
|
super(instanceSettings);
|
||||||
|
this.variables = new SQLVariableSupport(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getQueryModel(target?: SQLQuery, templateSrv?: TemplateSrv, scopedVars?: ScopedVars): PostgresQueryModel {
|
getQueryModel(target?: SQLQuery, templateSrv?: TemplateSrv, scopedVars?: ScopedVars): PostgresQueryModel {
|
||||||
|
|||||||
@@ -3791,6 +3791,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"empty": "",
|
||||||
"error": "",
|
"error": "",
|
||||||
"retry": "",
|
"retry": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
@@ -4453,7 +4454,6 @@
|
|||||||
},
|
},
|
||||||
"no-properties-changed": "Žádné relevantní vlastnosti se nezměnily",
|
"no-properties-changed": "Žádné relevantní vlastnosti se nezměnily",
|
||||||
"table": {
|
"table": {
|
||||||
"notes": "",
|
|
||||||
"updated": "Datum",
|
"updated": "Datum",
|
||||||
"updatedBy": "Aktualizoval uživatel",
|
"updatedBy": "Aktualizoval uživatel",
|
||||||
"version": "Verze"
|
"version": "Verze"
|
||||||
@@ -4912,8 +4912,7 @@
|
|||||||
"apply": "",
|
"apply": "",
|
||||||
"change-value": "",
|
"change-value": "",
|
||||||
"discard": "",
|
"discard": "",
|
||||||
"modal-title": "",
|
"modal-title": ""
|
||||||
"values": "Hodnoty oddělené čárkou"
|
|
||||||
},
|
},
|
||||||
"datasource-options": {
|
"datasource-options": {
|
||||||
"name-filter": "Filtr názvu",
|
"name-filter": "Filtr názvu",
|
||||||
@@ -6011,9 +6010,6 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "Vlastní možnosti",
|
"custom-options": "Vlastní možnosti",
|
||||||
"json-values-tooltip": "",
|
|
||||||
"name-csv-values": "",
|
|
||||||
"name-json-values": "",
|
|
||||||
"name-values-separated-comma": "Hodnoty oddělené čárkou",
|
"name-values-separated-comma": "Hodnoty oddělené čárkou",
|
||||||
"selection-options": "Možnosti výběru"
|
"selection-options": "Možnosti výběru"
|
||||||
},
|
},
|
||||||
@@ -6605,11 +6601,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"use-modal-editor": {
|
|
||||||
"description": {
|
|
||||||
"change-variable-query": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"use-save-dashboard": {
|
"use-save-dashboard": {
|
||||||
"message-dashboard-saved": "Nástěnka byla uložena"
|
"message-dashboard-saved": "Nástěnka byla uložena"
|
||||||
},
|
},
|
||||||
@@ -6633,7 +6624,6 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"description": "",
|
|
||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden-label": {
|
"hidden-label": {
|
||||||
@@ -6693,11 +6683,8 @@
|
|||||||
"tooltip-show-usages": "Zobrazit použití"
|
"tooltip-show-usages": "Zobrazit použití"
|
||||||
},
|
},
|
||||||
"variable-values-preview": {
|
"variable-values-preview": {
|
||||||
"show-more": "Zobrazit více",
|
"preview-of-values": "Náhled hodnot",
|
||||||
"preview-of-values_one": "",
|
"show-more": "Zobrazit více"
|
||||||
"preview-of-values_few": "",
|
|
||||||
"preview-of-values_many": "",
|
|
||||||
"preview-of-values_other": ""
|
|
||||||
},
|
},
|
||||||
"version-history": {
|
"version-history": {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
|
|||||||
@@ -3759,6 +3759,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"empty": "",
|
||||||
"error": "",
|
"error": "",
|
||||||
"retry": "",
|
"retry": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
@@ -4415,7 +4416,6 @@
|
|||||||
},
|
},
|
||||||
"no-properties-changed": "Keine relevanten Eigenschaften geändert",
|
"no-properties-changed": "Keine relevanten Eigenschaften geändert",
|
||||||
"table": {
|
"table": {
|
||||||
"notes": "",
|
|
||||||
"updated": "Datum",
|
"updated": "Datum",
|
||||||
"updatedBy": "Aktualisiert von",
|
"updatedBy": "Aktualisiert von",
|
||||||
"version": "Version"
|
"version": "Version"
|
||||||
@@ -4874,8 +4874,7 @@
|
|||||||
"apply": "",
|
"apply": "",
|
||||||
"change-value": "",
|
"change-value": "",
|
||||||
"discard": "",
|
"discard": "",
|
||||||
"modal-title": "",
|
"modal-title": ""
|
||||||
"values": "Werte werden durch Komma getrennt"
|
|
||||||
},
|
},
|
||||||
"datasource-options": {
|
"datasource-options": {
|
||||||
"name-filter": "Namensfilter",
|
"name-filter": "Namensfilter",
|
||||||
@@ -5969,9 +5968,6 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "Benutzerdefinierte Optionen",
|
"custom-options": "Benutzerdefinierte Optionen",
|
||||||
"json-values-tooltip": "",
|
|
||||||
"name-csv-values": "",
|
|
||||||
"name-json-values": "",
|
|
||||||
"name-values-separated-comma": "Werte werden durch Komma getrennt",
|
"name-values-separated-comma": "Werte werden durch Komma getrennt",
|
||||||
"selection-options": "Auswahloptionen"
|
"selection-options": "Auswahloptionen"
|
||||||
},
|
},
|
||||||
@@ -6559,11 +6555,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"use-modal-editor": {
|
|
||||||
"description": {
|
|
||||||
"change-variable-query": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"use-save-dashboard": {
|
"use-save-dashboard": {
|
||||||
"message-dashboard-saved": "Dashboard gespeichert"
|
"message-dashboard-saved": "Dashboard gespeichert"
|
||||||
},
|
},
|
||||||
@@ -6587,7 +6578,6 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"description": "",
|
|
||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden-label": {
|
"hidden-label": {
|
||||||
@@ -6647,9 +6637,8 @@
|
|||||||
"tooltip-show-usages": "Nutzungen anzeigen"
|
"tooltip-show-usages": "Nutzungen anzeigen"
|
||||||
},
|
},
|
||||||
"variable-values-preview": {
|
"variable-values-preview": {
|
||||||
"show-more": "Mehr anzeigen",
|
"preview-of-values": "Vorschau der Werte",
|
||||||
"preview-of-values_one": "",
|
"show-more": "Mehr anzeigen"
|
||||||
"preview-of-values_other": ""
|
|
||||||
},
|
},
|
||||||
"version-history": {
|
"version-history": {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
|
|||||||
@@ -3759,6 +3759,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "Clear history",
|
"clear": "Clear history",
|
||||||
|
"empty": "Nothing viewed yet",
|
||||||
"error": "Recently viewed dashboards couldn’t be loaded.",
|
"error": "Recently viewed dashboards couldn’t be loaded.",
|
||||||
"retry": "Retry",
|
"retry": "Retry",
|
||||||
"title": "Recently viewed"
|
"title": "Recently viewed"
|
||||||
|
|||||||
@@ -3759,6 +3759,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"empty": "",
|
||||||
"error": "",
|
"error": "",
|
||||||
"retry": "",
|
"retry": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
@@ -4415,7 +4416,6 @@
|
|||||||
},
|
},
|
||||||
"no-properties-changed": "No se ha cambiado ninguna propiedad relevante",
|
"no-properties-changed": "No se ha cambiado ninguna propiedad relevante",
|
||||||
"table": {
|
"table": {
|
||||||
"notes": "",
|
|
||||||
"updated": "Fecha",
|
"updated": "Fecha",
|
||||||
"updatedBy": "Actualizada por",
|
"updatedBy": "Actualizada por",
|
||||||
"version": "Versión"
|
"version": "Versión"
|
||||||
@@ -4874,8 +4874,7 @@
|
|||||||
"apply": "",
|
"apply": "",
|
||||||
"change-value": "",
|
"change-value": "",
|
||||||
"discard": "",
|
"discard": "",
|
||||||
"modal-title": "",
|
"modal-title": ""
|
||||||
"values": "Valores separados por coma"
|
|
||||||
},
|
},
|
||||||
"datasource-options": {
|
"datasource-options": {
|
||||||
"name-filter": "Nombrar filtro",
|
"name-filter": "Nombrar filtro",
|
||||||
@@ -5969,9 +5968,6 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "Opciones personalizadas",
|
"custom-options": "Opciones personalizadas",
|
||||||
"json-values-tooltip": "",
|
|
||||||
"name-csv-values": "",
|
|
||||||
"name-json-values": "",
|
|
||||||
"name-values-separated-comma": "Valores separados por comas",
|
"name-values-separated-comma": "Valores separados por comas",
|
||||||
"selection-options": "Opciones de selección"
|
"selection-options": "Opciones de selección"
|
||||||
},
|
},
|
||||||
@@ -6559,11 +6555,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"use-modal-editor": {
|
|
||||||
"description": {
|
|
||||||
"change-variable-query": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"use-save-dashboard": {
|
"use-save-dashboard": {
|
||||||
"message-dashboard-saved": "Dashboard guardado"
|
"message-dashboard-saved": "Dashboard guardado"
|
||||||
},
|
},
|
||||||
@@ -6587,7 +6578,6 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"description": "",
|
|
||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden-label": {
|
"hidden-label": {
|
||||||
@@ -6647,9 +6637,8 @@
|
|||||||
"tooltip-show-usages": "Mostrar usos"
|
"tooltip-show-usages": "Mostrar usos"
|
||||||
},
|
},
|
||||||
"variable-values-preview": {
|
"variable-values-preview": {
|
||||||
"show-more": "Mostrar más",
|
"preview-of-values": "Vista previa de los valores",
|
||||||
"preview-of-values_one": "",
|
"show-more": "Mostrar más"
|
||||||
"preview-of-values_other": ""
|
|
||||||
},
|
},
|
||||||
"version-history": {
|
"version-history": {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
|
|||||||
@@ -3759,6 +3759,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"empty": "",
|
||||||
"error": "",
|
"error": "",
|
||||||
"retry": "",
|
"retry": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
@@ -4415,7 +4416,6 @@
|
|||||||
},
|
},
|
||||||
"no-properties-changed": "Aucune propriété pertinente n’a été modifiée",
|
"no-properties-changed": "Aucune propriété pertinente n’a été modifiée",
|
||||||
"table": {
|
"table": {
|
||||||
"notes": "",
|
|
||||||
"updated": "Date",
|
"updated": "Date",
|
||||||
"updatedBy": "Mis à jour par",
|
"updatedBy": "Mis à jour par",
|
||||||
"version": "Version"
|
"version": "Version"
|
||||||
@@ -4874,8 +4874,7 @@
|
|||||||
"apply": "",
|
"apply": "",
|
||||||
"change-value": "",
|
"change-value": "",
|
||||||
"discard": "",
|
"discard": "",
|
||||||
"modal-title": "",
|
"modal-title": ""
|
||||||
"values": "Valeurs séparées par une virgule"
|
|
||||||
},
|
},
|
||||||
"datasource-options": {
|
"datasource-options": {
|
||||||
"name-filter": "Nom du filtre",
|
"name-filter": "Nom du filtre",
|
||||||
@@ -5969,9 +5968,6 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "Personnaliser les options",
|
"custom-options": "Personnaliser les options",
|
||||||
"json-values-tooltip": "",
|
|
||||||
"name-csv-values": "",
|
|
||||||
"name-json-values": "",
|
|
||||||
"name-values-separated-comma": "Valeurs séparées par des virgules",
|
"name-values-separated-comma": "Valeurs séparées par des virgules",
|
||||||
"selection-options": "Options de sélection"
|
"selection-options": "Options de sélection"
|
||||||
},
|
},
|
||||||
@@ -6559,11 +6555,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"use-modal-editor": {
|
|
||||||
"description": {
|
|
||||||
"change-variable-query": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"use-save-dashboard": {
|
"use-save-dashboard": {
|
||||||
"message-dashboard-saved": "Tableau de bord enregistré"
|
"message-dashboard-saved": "Tableau de bord enregistré"
|
||||||
},
|
},
|
||||||
@@ -6587,7 +6578,6 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"description": "",
|
|
||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden-label": {
|
"hidden-label": {
|
||||||
@@ -6647,9 +6637,8 @@
|
|||||||
"tooltip-show-usages": "Afficher les usages"
|
"tooltip-show-usages": "Afficher les usages"
|
||||||
},
|
},
|
||||||
"variable-values-preview": {
|
"variable-values-preview": {
|
||||||
"show-more": "Afficher plus",
|
"preview-of-values": "Aperçu des valeurs",
|
||||||
"preview-of-values_one": "",
|
"show-more": "Afficher plus"
|
||||||
"preview-of-values_other": ""
|
|
||||||
},
|
},
|
||||||
"version-history": {
|
"version-history": {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
|
|||||||
@@ -3759,6 +3759,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"empty": "",
|
||||||
"error": "",
|
"error": "",
|
||||||
"retry": "",
|
"retry": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
@@ -4415,7 +4416,6 @@
|
|||||||
},
|
},
|
||||||
"no-properties-changed": "Nem változtak meg a releváns tulajdonságok",
|
"no-properties-changed": "Nem változtak meg a releváns tulajdonságok",
|
||||||
"table": {
|
"table": {
|
||||||
"notes": "",
|
|
||||||
"updated": "Dátum",
|
"updated": "Dátum",
|
||||||
"updatedBy": "Frissítette:",
|
"updatedBy": "Frissítette:",
|
||||||
"version": "Verzió"
|
"version": "Verzió"
|
||||||
@@ -4874,8 +4874,7 @@
|
|||||||
"apply": "",
|
"apply": "",
|
||||||
"change-value": "",
|
"change-value": "",
|
||||||
"discard": "",
|
"discard": "",
|
||||||
"modal-title": "",
|
"modal-title": ""
|
||||||
"values": "Értékek vesszővel elválasztva"
|
|
||||||
},
|
},
|
||||||
"datasource-options": {
|
"datasource-options": {
|
||||||
"name-filter": "Névszűrő",
|
"name-filter": "Névszűrő",
|
||||||
@@ -5969,9 +5968,6 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "Egyéni opciók",
|
"custom-options": "Egyéni opciók",
|
||||||
"json-values-tooltip": "",
|
|
||||||
"name-csv-values": "",
|
|
||||||
"name-json-values": "",
|
|
||||||
"name-values-separated-comma": "Értékek vesszővel elválasztva",
|
"name-values-separated-comma": "Értékek vesszővel elválasztva",
|
||||||
"selection-options": "Kijelölés beállításai"
|
"selection-options": "Kijelölés beállításai"
|
||||||
},
|
},
|
||||||
@@ -6559,11 +6555,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"use-modal-editor": {
|
|
||||||
"description": {
|
|
||||||
"change-variable-query": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"use-save-dashboard": {
|
"use-save-dashboard": {
|
||||||
"message-dashboard-saved": "Irányítópult elmentve"
|
"message-dashboard-saved": "Irányítópult elmentve"
|
||||||
},
|
},
|
||||||
@@ -6587,7 +6578,6 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"description": "",
|
|
||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden-label": {
|
"hidden-label": {
|
||||||
@@ -6647,9 +6637,8 @@
|
|||||||
"tooltip-show-usages": "Használatok megjelenítése"
|
"tooltip-show-usages": "Használatok megjelenítése"
|
||||||
},
|
},
|
||||||
"variable-values-preview": {
|
"variable-values-preview": {
|
||||||
"show-more": "Több megjelenítése",
|
"preview-of-values": "Értékek előnézete",
|
||||||
"preview-of-values_one": "",
|
"show-more": "Több megjelenítése"
|
||||||
"preview-of-values_other": ""
|
|
||||||
},
|
},
|
||||||
"version-history": {
|
"version-history": {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
|
|||||||
@@ -3743,6 +3743,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"empty": "",
|
||||||
"error": "",
|
"error": "",
|
||||||
"retry": "",
|
"retry": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
@@ -4396,7 +4397,6 @@
|
|||||||
},
|
},
|
||||||
"no-properties-changed": "Tidak ada properti yang relevan yang diubah",
|
"no-properties-changed": "Tidak ada properti yang relevan yang diubah",
|
||||||
"table": {
|
"table": {
|
||||||
"notes": "",
|
|
||||||
"updated": "Tanggal",
|
"updated": "Tanggal",
|
||||||
"updatedBy": "Diperbarui Oleh",
|
"updatedBy": "Diperbarui Oleh",
|
||||||
"version": "Versi"
|
"version": "Versi"
|
||||||
@@ -4855,8 +4855,7 @@
|
|||||||
"apply": "",
|
"apply": "",
|
||||||
"change-value": "",
|
"change-value": "",
|
||||||
"discard": "",
|
"discard": "",
|
||||||
"modal-title": "",
|
"modal-title": ""
|
||||||
"values": "Nilai dipisahkan dengan koma"
|
|
||||||
},
|
},
|
||||||
"datasource-options": {
|
"datasource-options": {
|
||||||
"name-filter": "Filter nama",
|
"name-filter": "Filter nama",
|
||||||
@@ -5948,9 +5947,6 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "Opsi kustom",
|
"custom-options": "Opsi kustom",
|
||||||
"json-values-tooltip": "",
|
|
||||||
"name-csv-values": "",
|
|
||||||
"name-json-values": "",
|
|
||||||
"name-values-separated-comma": "Nilai dipisahkan dengan koma",
|
"name-values-separated-comma": "Nilai dipisahkan dengan koma",
|
||||||
"selection-options": "Opsi pemilihan"
|
"selection-options": "Opsi pemilihan"
|
||||||
},
|
},
|
||||||
@@ -6536,11 +6532,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"use-modal-editor": {
|
|
||||||
"description": {
|
|
||||||
"change-variable-query": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"use-save-dashboard": {
|
"use-save-dashboard": {
|
||||||
"message-dashboard-saved": "Dasbor disimpan"
|
"message-dashboard-saved": "Dasbor disimpan"
|
||||||
},
|
},
|
||||||
@@ -6564,7 +6555,6 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"description": "",
|
|
||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden-label": {
|
"hidden-label": {
|
||||||
@@ -6624,8 +6614,8 @@
|
|||||||
"tooltip-show-usages": "Tampilkan penggunaan"
|
"tooltip-show-usages": "Tampilkan penggunaan"
|
||||||
},
|
},
|
||||||
"variable-values-preview": {
|
"variable-values-preview": {
|
||||||
"show-more": "Tampilkan lebih banyak",
|
"preview-of-values": "Pratinjau nilai",
|
||||||
"preview-of-values_other": ""
|
"show-more": "Tampilkan lebih banyak"
|
||||||
},
|
},
|
||||||
"version-history": {
|
"version-history": {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
|
|||||||
@@ -3759,6 +3759,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"empty": "",
|
||||||
"error": "",
|
"error": "",
|
||||||
"retry": "",
|
"retry": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
@@ -4415,7 +4416,6 @@
|
|||||||
},
|
},
|
||||||
"no-properties-changed": "Nessuna proprietà rilevante modificata",
|
"no-properties-changed": "Nessuna proprietà rilevante modificata",
|
||||||
"table": {
|
"table": {
|
||||||
"notes": "",
|
|
||||||
"updated": "Data",
|
"updated": "Data",
|
||||||
"updatedBy": "Aggiornato da",
|
"updatedBy": "Aggiornato da",
|
||||||
"version": "Versione"
|
"version": "Versione"
|
||||||
@@ -4874,8 +4874,7 @@
|
|||||||
"apply": "",
|
"apply": "",
|
||||||
"change-value": "",
|
"change-value": "",
|
||||||
"discard": "",
|
"discard": "",
|
||||||
"modal-title": "",
|
"modal-title": ""
|
||||||
"values": "Valori separati da virgola"
|
|
||||||
},
|
},
|
||||||
"datasource-options": {
|
"datasource-options": {
|
||||||
"name-filter": "Filtro nome",
|
"name-filter": "Filtro nome",
|
||||||
@@ -5969,9 +5968,6 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "Opzioni personalizzate",
|
"custom-options": "Opzioni personalizzate",
|
||||||
"json-values-tooltip": "",
|
|
||||||
"name-csv-values": "",
|
|
||||||
"name-json-values": "",
|
|
||||||
"name-values-separated-comma": "Valori separati da virgola",
|
"name-values-separated-comma": "Valori separati da virgola",
|
||||||
"selection-options": "Seleziona opzioni"
|
"selection-options": "Seleziona opzioni"
|
||||||
},
|
},
|
||||||
@@ -6559,11 +6555,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"use-modal-editor": {
|
|
||||||
"description": {
|
|
||||||
"change-variable-query": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"use-save-dashboard": {
|
"use-save-dashboard": {
|
||||||
"message-dashboard-saved": "Dashboard salvata"
|
"message-dashboard-saved": "Dashboard salvata"
|
||||||
},
|
},
|
||||||
@@ -6587,7 +6578,6 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"description": "",
|
|
||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden-label": {
|
"hidden-label": {
|
||||||
@@ -6647,9 +6637,8 @@
|
|||||||
"tooltip-show-usages": "Mostra utilizzi"
|
"tooltip-show-usages": "Mostra utilizzi"
|
||||||
},
|
},
|
||||||
"variable-values-preview": {
|
"variable-values-preview": {
|
||||||
"show-more": "Mostra di più",
|
"preview-of-values": "Anteprima dei valori",
|
||||||
"preview-of-values_one": "",
|
"show-more": "Mostra di più"
|
||||||
"preview-of-values_other": ""
|
|
||||||
},
|
},
|
||||||
"version-history": {
|
"version-history": {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
|
|||||||
@@ -3743,6 +3743,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"empty": "",
|
||||||
"error": "",
|
"error": "",
|
||||||
"retry": "",
|
"retry": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
@@ -4396,7 +4397,6 @@
|
|||||||
},
|
},
|
||||||
"no-properties-changed": "関連するプロパティは変更されていません",
|
"no-properties-changed": "関連するプロパティは変更されていません",
|
||||||
"table": {
|
"table": {
|
||||||
"notes": "",
|
|
||||||
"updated": "日付",
|
"updated": "日付",
|
||||||
"updatedBy": "更新者",
|
"updatedBy": "更新者",
|
||||||
"version": "バージョン"
|
"version": "バージョン"
|
||||||
@@ -4855,8 +4855,7 @@
|
|||||||
"apply": "",
|
"apply": "",
|
||||||
"change-value": "",
|
"change-value": "",
|
||||||
"discard": "",
|
"discard": "",
|
||||||
"modal-title": "",
|
"modal-title": ""
|
||||||
"values": "カンマで区切った値"
|
|
||||||
},
|
},
|
||||||
"datasource-options": {
|
"datasource-options": {
|
||||||
"name-filter": "名前フィルター",
|
"name-filter": "名前フィルター",
|
||||||
@@ -5948,9 +5947,6 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "カスタムオプション",
|
"custom-options": "カスタムオプション",
|
||||||
"json-values-tooltip": "",
|
|
||||||
"name-csv-values": "",
|
|
||||||
"name-json-values": "",
|
|
||||||
"name-values-separated-comma": "カンマ区切りの値",
|
"name-values-separated-comma": "カンマ区切りの値",
|
||||||
"selection-options": "選択オプション"
|
"selection-options": "選択オプション"
|
||||||
},
|
},
|
||||||
@@ -6536,11 +6532,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"use-modal-editor": {
|
|
||||||
"description": {
|
|
||||||
"change-variable-query": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"use-save-dashboard": {
|
"use-save-dashboard": {
|
||||||
"message-dashboard-saved": "ダッシュボードが保存されました"
|
"message-dashboard-saved": "ダッシュボードが保存されました"
|
||||||
},
|
},
|
||||||
@@ -6564,7 +6555,6 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"description": "",
|
|
||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden-label": {
|
"hidden-label": {
|
||||||
@@ -6624,8 +6614,8 @@
|
|||||||
"tooltip-show-usages": "使用状況を表示"
|
"tooltip-show-usages": "使用状況を表示"
|
||||||
},
|
},
|
||||||
"variable-values-preview": {
|
"variable-values-preview": {
|
||||||
"show-more": "さらに表示",
|
"preview-of-values": "値のプレビュー",
|
||||||
"preview-of-values_other": ""
|
"show-more": "さらに表示"
|
||||||
},
|
},
|
||||||
"version-history": {
|
"version-history": {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
|
|||||||
@@ -3743,6 +3743,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"empty": "",
|
||||||
"error": "",
|
"error": "",
|
||||||
"retry": "",
|
"retry": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
@@ -4396,7 +4397,6 @@
|
|||||||
},
|
},
|
||||||
"no-properties-changed": "변경된 관련 속성 없음",
|
"no-properties-changed": "변경된 관련 속성 없음",
|
||||||
"table": {
|
"table": {
|
||||||
"notes": "",
|
|
||||||
"updated": "날짜",
|
"updated": "날짜",
|
||||||
"updatedBy": "업데이트한 사용자",
|
"updatedBy": "업데이트한 사용자",
|
||||||
"version": "버전"
|
"version": "버전"
|
||||||
@@ -4855,8 +4855,7 @@
|
|||||||
"apply": "",
|
"apply": "",
|
||||||
"change-value": "",
|
"change-value": "",
|
||||||
"discard": "",
|
"discard": "",
|
||||||
"modal-title": "",
|
"modal-title": ""
|
||||||
"values": "쉼표로 구분된 값"
|
|
||||||
},
|
},
|
||||||
"datasource-options": {
|
"datasource-options": {
|
||||||
"name-filter": "이름 필터",
|
"name-filter": "이름 필터",
|
||||||
@@ -5948,9 +5947,6 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "사용자 지정 옵션",
|
"custom-options": "사용자 지정 옵션",
|
||||||
"json-values-tooltip": "",
|
|
||||||
"name-csv-values": "",
|
|
||||||
"name-json-values": "",
|
|
||||||
"name-values-separated-comma": "쉼표로 구분된 값",
|
"name-values-separated-comma": "쉼표로 구분된 값",
|
||||||
"selection-options": "선택 옵션"
|
"selection-options": "선택 옵션"
|
||||||
},
|
},
|
||||||
@@ -6536,11 +6532,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"use-modal-editor": {
|
|
||||||
"description": {
|
|
||||||
"change-variable-query": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"use-save-dashboard": {
|
"use-save-dashboard": {
|
||||||
"message-dashboard-saved": "대시보드가 저장되었습니다"
|
"message-dashboard-saved": "대시보드가 저장되었습니다"
|
||||||
},
|
},
|
||||||
@@ -6564,7 +6555,6 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"description": "",
|
|
||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden-label": {
|
"hidden-label": {
|
||||||
@@ -6624,8 +6614,8 @@
|
|||||||
"tooltip-show-usages": "사용처 표시"
|
"tooltip-show-usages": "사용처 표시"
|
||||||
},
|
},
|
||||||
"variable-values-preview": {
|
"variable-values-preview": {
|
||||||
"show-more": "더 보기",
|
"preview-of-values": "값 미리 보기",
|
||||||
"preview-of-values_other": ""
|
"show-more": "더 보기"
|
||||||
},
|
},
|
||||||
"version-history": {
|
"version-history": {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
|
|||||||
@@ -3759,6 +3759,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"empty": "",
|
||||||
"error": "",
|
"error": "",
|
||||||
"retry": "",
|
"retry": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
@@ -4415,7 +4416,6 @@
|
|||||||
},
|
},
|
||||||
"no-properties-changed": "Geen relevante eigenschappen gewijzigd",
|
"no-properties-changed": "Geen relevante eigenschappen gewijzigd",
|
||||||
"table": {
|
"table": {
|
||||||
"notes": "",
|
|
||||||
"updated": "Datum",
|
"updated": "Datum",
|
||||||
"updatedBy": "Bijgewerkt door",
|
"updatedBy": "Bijgewerkt door",
|
||||||
"version": "Versie"
|
"version": "Versie"
|
||||||
@@ -4874,8 +4874,7 @@
|
|||||||
"apply": "",
|
"apply": "",
|
||||||
"change-value": "",
|
"change-value": "",
|
||||||
"discard": "",
|
"discard": "",
|
||||||
"modal-title": "",
|
"modal-title": ""
|
||||||
"values": "Waarden gescheiden door komma"
|
|
||||||
},
|
},
|
||||||
"datasource-options": {
|
"datasource-options": {
|
||||||
"name-filter": "Filter een naam geven",
|
"name-filter": "Filter een naam geven",
|
||||||
@@ -5969,9 +5968,6 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "Aangepaste opties",
|
"custom-options": "Aangepaste opties",
|
||||||
"json-values-tooltip": "",
|
|
||||||
"name-csv-values": "",
|
|
||||||
"name-json-values": "",
|
|
||||||
"name-values-separated-comma": "Waarden gescheiden door komma",
|
"name-values-separated-comma": "Waarden gescheiden door komma",
|
||||||
"selection-options": "Selectiemogelijkheden"
|
"selection-options": "Selectiemogelijkheden"
|
||||||
},
|
},
|
||||||
@@ -6559,11 +6555,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"use-modal-editor": {
|
|
||||||
"description": {
|
|
||||||
"change-variable-query": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"use-save-dashboard": {
|
"use-save-dashboard": {
|
||||||
"message-dashboard-saved": "Dashboard opgeslagen"
|
"message-dashboard-saved": "Dashboard opgeslagen"
|
||||||
},
|
},
|
||||||
@@ -6587,7 +6578,6 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"description": "",
|
|
||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden-label": {
|
"hidden-label": {
|
||||||
@@ -6647,9 +6637,8 @@
|
|||||||
"tooltip-show-usages": "Gebruik weergeven"
|
"tooltip-show-usages": "Gebruik weergeven"
|
||||||
},
|
},
|
||||||
"variable-values-preview": {
|
"variable-values-preview": {
|
||||||
"show-more": "Meer weergeven",
|
"preview-of-values": "Voorbeeldweergave van waarden",
|
||||||
"preview-of-values_one": "",
|
"show-more": "Meer weergeven"
|
||||||
"preview-of-values_other": ""
|
|
||||||
},
|
},
|
||||||
"version-history": {
|
"version-history": {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
|
|||||||
@@ -3791,6 +3791,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"empty": "",
|
||||||
"error": "",
|
"error": "",
|
||||||
"retry": "",
|
"retry": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
@@ -4453,7 +4454,6 @@
|
|||||||
},
|
},
|
||||||
"no-properties-changed": "Nie zmieniono istotnych właściwości",
|
"no-properties-changed": "Nie zmieniono istotnych właściwości",
|
||||||
"table": {
|
"table": {
|
||||||
"notes": "",
|
|
||||||
"updated": "Data",
|
"updated": "Data",
|
||||||
"updatedBy": "Zaktualizowane przez",
|
"updatedBy": "Zaktualizowane przez",
|
||||||
"version": "Wersja"
|
"version": "Wersja"
|
||||||
@@ -4912,8 +4912,7 @@
|
|||||||
"apply": "",
|
"apply": "",
|
||||||
"change-value": "",
|
"change-value": "",
|
||||||
"discard": "",
|
"discard": "",
|
||||||
"modal-title": "",
|
"modal-title": ""
|
||||||
"values": "Wartości rozdzielone przecinkami"
|
|
||||||
},
|
},
|
||||||
"datasource-options": {
|
"datasource-options": {
|
||||||
"name-filter": "Filtr nazwy",
|
"name-filter": "Filtr nazwy",
|
||||||
@@ -6011,9 +6010,6 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "Opcje niestandardowe",
|
"custom-options": "Opcje niestandardowe",
|
||||||
"json-values-tooltip": "",
|
|
||||||
"name-csv-values": "",
|
|
||||||
"name-json-values": "",
|
|
||||||
"name-values-separated-comma": "Wartości rozdzielone przecinkami",
|
"name-values-separated-comma": "Wartości rozdzielone przecinkami",
|
||||||
"selection-options": "Opcje wyboru"
|
"selection-options": "Opcje wyboru"
|
||||||
},
|
},
|
||||||
@@ -6605,11 +6601,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"use-modal-editor": {
|
|
||||||
"description": {
|
|
||||||
"change-variable-query": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"use-save-dashboard": {
|
"use-save-dashboard": {
|
||||||
"message-dashboard-saved": "Pulpit został zapisany"
|
"message-dashboard-saved": "Pulpit został zapisany"
|
||||||
},
|
},
|
||||||
@@ -6633,7 +6624,6 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"description": "",
|
|
||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden-label": {
|
"hidden-label": {
|
||||||
@@ -6693,11 +6683,8 @@
|
|||||||
"tooltip-show-usages": "Wyświetl użycie"
|
"tooltip-show-usages": "Wyświetl użycie"
|
||||||
},
|
},
|
||||||
"variable-values-preview": {
|
"variable-values-preview": {
|
||||||
"show-more": "Pokaż więcej",
|
"preview-of-values": "Podgląd wartości",
|
||||||
"preview-of-values_one": "",
|
"show-more": "Pokaż więcej"
|
||||||
"preview-of-values_few": "",
|
|
||||||
"preview-of-values_many": "",
|
|
||||||
"preview-of-values_other": ""
|
|
||||||
},
|
},
|
||||||
"version-history": {
|
"version-history": {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
|
|||||||
@@ -3759,6 +3759,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"empty": "",
|
||||||
"error": "",
|
"error": "",
|
||||||
"retry": "",
|
"retry": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
@@ -4415,7 +4416,6 @@
|
|||||||
},
|
},
|
||||||
"no-properties-changed": "Nenhuma propriedade relevante alterada",
|
"no-properties-changed": "Nenhuma propriedade relevante alterada",
|
||||||
"table": {
|
"table": {
|
||||||
"notes": "",
|
|
||||||
"updated": "Data",
|
"updated": "Data",
|
||||||
"updatedBy": "Atualizada por",
|
"updatedBy": "Atualizada por",
|
||||||
"version": "Versão"
|
"version": "Versão"
|
||||||
@@ -4874,8 +4874,7 @@
|
|||||||
"apply": "",
|
"apply": "",
|
||||||
"change-value": "",
|
"change-value": "",
|
||||||
"discard": "",
|
"discard": "",
|
||||||
"modal-title": "",
|
"modal-title": ""
|
||||||
"values": "Valores separados por vírgula"
|
|
||||||
},
|
},
|
||||||
"datasource-options": {
|
"datasource-options": {
|
||||||
"name-filter": "Filtro de nome",
|
"name-filter": "Filtro de nome",
|
||||||
@@ -5969,9 +5968,6 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "Opções personalizadas",
|
"custom-options": "Opções personalizadas",
|
||||||
"json-values-tooltip": "",
|
|
||||||
"name-csv-values": "",
|
|
||||||
"name-json-values": "",
|
|
||||||
"name-values-separated-comma": "Valores separados por vírgula",
|
"name-values-separated-comma": "Valores separados por vírgula",
|
||||||
"selection-options": "Opções de seleção"
|
"selection-options": "Opções de seleção"
|
||||||
},
|
},
|
||||||
@@ -6559,11 +6555,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"use-modal-editor": {
|
|
||||||
"description": {
|
|
||||||
"change-variable-query": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"use-save-dashboard": {
|
"use-save-dashboard": {
|
||||||
"message-dashboard-saved": "Painel de controle salvo"
|
"message-dashboard-saved": "Painel de controle salvo"
|
||||||
},
|
},
|
||||||
@@ -6587,7 +6578,6 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"description": "",
|
|
||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden-label": {
|
"hidden-label": {
|
||||||
@@ -6647,9 +6637,8 @@
|
|||||||
"tooltip-show-usages": "Exibir usos"
|
"tooltip-show-usages": "Exibir usos"
|
||||||
},
|
},
|
||||||
"variable-values-preview": {
|
"variable-values-preview": {
|
||||||
"show-more": "Exibir mais",
|
"preview-of-values": "Pré-visualização de valores",
|
||||||
"preview-of-values_one": "",
|
"show-more": "Exibir mais"
|
||||||
"preview-of-values_other": ""
|
|
||||||
},
|
},
|
||||||
"version-history": {
|
"version-history": {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
|
|||||||
@@ -3759,6 +3759,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"empty": "",
|
||||||
"error": "",
|
"error": "",
|
||||||
"retry": "",
|
"retry": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
@@ -4415,7 +4416,6 @@
|
|||||||
},
|
},
|
||||||
"no-properties-changed": "Nenhuma propriedade relevante alterada",
|
"no-properties-changed": "Nenhuma propriedade relevante alterada",
|
||||||
"table": {
|
"table": {
|
||||||
"notes": "",
|
|
||||||
"updated": "Data",
|
"updated": "Data",
|
||||||
"updatedBy": "Atualizado por",
|
"updatedBy": "Atualizado por",
|
||||||
"version": "Versão"
|
"version": "Versão"
|
||||||
@@ -4874,8 +4874,7 @@
|
|||||||
"apply": "",
|
"apply": "",
|
||||||
"change-value": "",
|
"change-value": "",
|
||||||
"discard": "",
|
"discard": "",
|
||||||
"modal-title": "",
|
"modal-title": ""
|
||||||
"values": "Valores separados por vírgulas"
|
|
||||||
},
|
},
|
||||||
"datasource-options": {
|
"datasource-options": {
|
||||||
"name-filter": "Filtro de nome",
|
"name-filter": "Filtro de nome",
|
||||||
@@ -5969,9 +5968,6 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "Opções personalizadas",
|
"custom-options": "Opções personalizadas",
|
||||||
"json-values-tooltip": "",
|
|
||||||
"name-csv-values": "",
|
|
||||||
"name-json-values": "",
|
|
||||||
"name-values-separated-comma": "Valores separados por vírgulas",
|
"name-values-separated-comma": "Valores separados por vírgulas",
|
||||||
"selection-options": "Opções de seleção"
|
"selection-options": "Opções de seleção"
|
||||||
},
|
},
|
||||||
@@ -6559,11 +6555,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"use-modal-editor": {
|
|
||||||
"description": {
|
|
||||||
"change-variable-query": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"use-save-dashboard": {
|
"use-save-dashboard": {
|
||||||
"message-dashboard-saved": "Painel de controlo guardado"
|
"message-dashboard-saved": "Painel de controlo guardado"
|
||||||
},
|
},
|
||||||
@@ -6587,7 +6578,6 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"description": "",
|
|
||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden-label": {
|
"hidden-label": {
|
||||||
@@ -6647,9 +6637,8 @@
|
|||||||
"tooltip-show-usages": "Mostrar utilizações"
|
"tooltip-show-usages": "Mostrar utilizações"
|
||||||
},
|
},
|
||||||
"variable-values-preview": {
|
"variable-values-preview": {
|
||||||
"show-more": "Mostrar mais",
|
"preview-of-values": "Pré-visualização de valores",
|
||||||
"preview-of-values_one": "",
|
"show-more": "Mostrar mais"
|
||||||
"preview-of-values_other": ""
|
|
||||||
},
|
},
|
||||||
"version-history": {
|
"version-history": {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
|
|||||||
@@ -3791,6 +3791,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"empty": "",
|
||||||
"error": "",
|
"error": "",
|
||||||
"retry": "",
|
"retry": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
@@ -4453,7 +4454,6 @@
|
|||||||
},
|
},
|
||||||
"no-properties-changed": "Нет изменений соответствующих свойств",
|
"no-properties-changed": "Нет изменений соответствующих свойств",
|
||||||
"table": {
|
"table": {
|
||||||
"notes": "",
|
|
||||||
"updated": "Дата",
|
"updated": "Дата",
|
||||||
"updatedBy": "Обновлено",
|
"updatedBy": "Обновлено",
|
||||||
"version": "Версия"
|
"version": "Версия"
|
||||||
@@ -4912,8 +4912,7 @@
|
|||||||
"apply": "",
|
"apply": "",
|
||||||
"change-value": "",
|
"change-value": "",
|
||||||
"discard": "",
|
"discard": "",
|
||||||
"modal-title": "",
|
"modal-title": ""
|
||||||
"values": "Значения, разделенные запятыми"
|
|
||||||
},
|
},
|
||||||
"datasource-options": {
|
"datasource-options": {
|
||||||
"name-filter": "Фильтр по названию",
|
"name-filter": "Фильтр по названию",
|
||||||
@@ -6011,9 +6010,6 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "Пользовательские параметры",
|
"custom-options": "Пользовательские параметры",
|
||||||
"json-values-tooltip": "",
|
|
||||||
"name-csv-values": "",
|
|
||||||
"name-json-values": "",
|
|
||||||
"name-values-separated-comma": "Значения, разделенные запятыми",
|
"name-values-separated-comma": "Значения, разделенные запятыми",
|
||||||
"selection-options": "Параметры выбора"
|
"selection-options": "Параметры выбора"
|
||||||
},
|
},
|
||||||
@@ -6605,11 +6601,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"use-modal-editor": {
|
|
||||||
"description": {
|
|
||||||
"change-variable-query": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"use-save-dashboard": {
|
"use-save-dashboard": {
|
||||||
"message-dashboard-saved": "Дашборд сохранен"
|
"message-dashboard-saved": "Дашборд сохранен"
|
||||||
},
|
},
|
||||||
@@ -6633,7 +6624,6 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"description": "",
|
|
||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden-label": {
|
"hidden-label": {
|
||||||
@@ -6693,11 +6683,8 @@
|
|||||||
"tooltip-show-usages": "Показать варианты использования"
|
"tooltip-show-usages": "Показать варианты использования"
|
||||||
},
|
},
|
||||||
"variable-values-preview": {
|
"variable-values-preview": {
|
||||||
"show-more": "Показать еще",
|
"preview-of-values": "Просмотр значений",
|
||||||
"preview-of-values_one": "",
|
"show-more": "Показать еще"
|
||||||
"preview-of-values_few": "",
|
|
||||||
"preview-of-values_many": "",
|
|
||||||
"preview-of-values_other": ""
|
|
||||||
},
|
},
|
||||||
"version-history": {
|
"version-history": {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
|
|||||||
@@ -3759,6 +3759,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"empty": "",
|
||||||
"error": "",
|
"error": "",
|
||||||
"retry": "",
|
"retry": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
@@ -4415,7 +4416,6 @@
|
|||||||
},
|
},
|
||||||
"no-properties-changed": "Inga relevanta egenskaper har ändrats",
|
"no-properties-changed": "Inga relevanta egenskaper har ändrats",
|
||||||
"table": {
|
"table": {
|
||||||
"notes": "",
|
|
||||||
"updated": "Datum",
|
"updated": "Datum",
|
||||||
"updatedBy": "Uppdaterad per",
|
"updatedBy": "Uppdaterad per",
|
||||||
"version": "Version"
|
"version": "Version"
|
||||||
@@ -4874,8 +4874,7 @@
|
|||||||
"apply": "",
|
"apply": "",
|
||||||
"change-value": "",
|
"change-value": "",
|
||||||
"discard": "",
|
"discard": "",
|
||||||
"modal-title": "",
|
"modal-title": ""
|
||||||
"values": "Värden åtskilda med kommatecken"
|
|
||||||
},
|
},
|
||||||
"datasource-options": {
|
"datasource-options": {
|
||||||
"name-filter": "Namnfilter",
|
"name-filter": "Namnfilter",
|
||||||
@@ -5969,9 +5968,6 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "Anpassade alternativ",
|
"custom-options": "Anpassade alternativ",
|
||||||
"json-values-tooltip": "",
|
|
||||||
"name-csv-values": "",
|
|
||||||
"name-json-values": "",
|
|
||||||
"name-values-separated-comma": "Värden åtskilda med kommatecken",
|
"name-values-separated-comma": "Värden åtskilda med kommatecken",
|
||||||
"selection-options": "Urvalsalternativ"
|
"selection-options": "Urvalsalternativ"
|
||||||
},
|
},
|
||||||
@@ -6559,11 +6555,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"use-modal-editor": {
|
|
||||||
"description": {
|
|
||||||
"change-variable-query": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"use-save-dashboard": {
|
"use-save-dashboard": {
|
||||||
"message-dashboard-saved": "Kontrollpanelen sparades"
|
"message-dashboard-saved": "Kontrollpanelen sparades"
|
||||||
},
|
},
|
||||||
@@ -6587,7 +6578,6 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"description": "",
|
|
||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden-label": {
|
"hidden-label": {
|
||||||
@@ -6647,9 +6637,8 @@
|
|||||||
"tooltip-show-usages": "Visa användningar"
|
"tooltip-show-usages": "Visa användningar"
|
||||||
},
|
},
|
||||||
"variable-values-preview": {
|
"variable-values-preview": {
|
||||||
"show-more": "Visa mer",
|
"preview-of-values": "Förhandsgranska värden",
|
||||||
"preview-of-values_one": "",
|
"show-more": "Visa mer"
|
||||||
"preview-of-values_other": ""
|
|
||||||
},
|
},
|
||||||
"version-history": {
|
"version-history": {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
|
|||||||
@@ -3759,6 +3759,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"empty": "",
|
||||||
"error": "",
|
"error": "",
|
||||||
"retry": "",
|
"retry": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
@@ -4415,7 +4416,6 @@
|
|||||||
},
|
},
|
||||||
"no-properties-changed": "İlgili hiçbir özellik değiştirilmedi",
|
"no-properties-changed": "İlgili hiçbir özellik değiştirilmedi",
|
||||||
"table": {
|
"table": {
|
||||||
"notes": "",
|
|
||||||
"updated": "Tarih",
|
"updated": "Tarih",
|
||||||
"updatedBy": "Güncelleyen:",
|
"updatedBy": "Güncelleyen:",
|
||||||
"version": "Sürüm"
|
"version": "Sürüm"
|
||||||
@@ -4874,8 +4874,7 @@
|
|||||||
"apply": "",
|
"apply": "",
|
||||||
"change-value": "",
|
"change-value": "",
|
||||||
"discard": "",
|
"discard": "",
|
||||||
"modal-title": "",
|
"modal-title": ""
|
||||||
"values": "Virgülle ayrılmış değerler"
|
|
||||||
},
|
},
|
||||||
"datasource-options": {
|
"datasource-options": {
|
||||||
"name-filter": "Ad filtresi",
|
"name-filter": "Ad filtresi",
|
||||||
@@ -5969,9 +5968,6 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "Özel seçenekler",
|
"custom-options": "Özel seçenekler",
|
||||||
"json-values-tooltip": "",
|
|
||||||
"name-csv-values": "",
|
|
||||||
"name-json-values": "",
|
|
||||||
"name-values-separated-comma": "Virgülle ayrılmış değerler",
|
"name-values-separated-comma": "Virgülle ayrılmış değerler",
|
||||||
"selection-options": "Seçim ayarları"
|
"selection-options": "Seçim ayarları"
|
||||||
},
|
},
|
||||||
@@ -6559,11 +6555,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"use-modal-editor": {
|
|
||||||
"description": {
|
|
||||||
"change-variable-query": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"use-save-dashboard": {
|
"use-save-dashboard": {
|
||||||
"message-dashboard-saved": "Pano kaydedildi"
|
"message-dashboard-saved": "Pano kaydedildi"
|
||||||
},
|
},
|
||||||
@@ -6587,7 +6578,6 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"description": "",
|
|
||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden-label": {
|
"hidden-label": {
|
||||||
@@ -6647,9 +6637,8 @@
|
|||||||
"tooltip-show-usages": "Kullanımları göster"
|
"tooltip-show-usages": "Kullanımları göster"
|
||||||
},
|
},
|
||||||
"variable-values-preview": {
|
"variable-values-preview": {
|
||||||
"show-more": "Daha fazla göster",
|
"preview-of-values": "Değerlerin ön izlemesi",
|
||||||
"preview-of-values_one": "",
|
"show-more": "Daha fazla göster"
|
||||||
"preview-of-values_other": ""
|
|
||||||
},
|
},
|
||||||
"version-history": {
|
"version-history": {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
|
|||||||
@@ -3743,6 +3743,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"empty": "",
|
||||||
"error": "",
|
"error": "",
|
||||||
"retry": "",
|
"retry": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
@@ -4396,7 +4397,6 @@
|
|||||||
},
|
},
|
||||||
"no-properties-changed": "没有相关属性更改",
|
"no-properties-changed": "没有相关属性更改",
|
||||||
"table": {
|
"table": {
|
||||||
"notes": "",
|
|
||||||
"updated": "日期",
|
"updated": "日期",
|
||||||
"updatedBy": "更新者",
|
"updatedBy": "更新者",
|
||||||
"version": "版本"
|
"version": "版本"
|
||||||
@@ -4855,8 +4855,7 @@
|
|||||||
"apply": "",
|
"apply": "",
|
||||||
"change-value": "",
|
"change-value": "",
|
||||||
"discard": "",
|
"discard": "",
|
||||||
"modal-title": "",
|
"modal-title": ""
|
||||||
"values": "以逗号分隔的值"
|
|
||||||
},
|
},
|
||||||
"datasource-options": {
|
"datasource-options": {
|
||||||
"name-filter": "名称筛选器",
|
"name-filter": "名称筛选器",
|
||||||
@@ -5948,9 +5947,6 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "自定义选项",
|
"custom-options": "自定义选项",
|
||||||
"json-values-tooltip": "",
|
|
||||||
"name-csv-values": "",
|
|
||||||
"name-json-values": "",
|
|
||||||
"name-values-separated-comma": "以逗号分隔的值",
|
"name-values-separated-comma": "以逗号分隔的值",
|
||||||
"selection-options": "选择内容选项"
|
"selection-options": "选择内容选项"
|
||||||
},
|
},
|
||||||
@@ -6536,11 +6532,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"use-modal-editor": {
|
|
||||||
"description": {
|
|
||||||
"change-variable-query": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"use-save-dashboard": {
|
"use-save-dashboard": {
|
||||||
"message-dashboard-saved": "数据面板已保存"
|
"message-dashboard-saved": "数据面板已保存"
|
||||||
},
|
},
|
||||||
@@ -6564,7 +6555,6 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"description": "",
|
|
||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden-label": {
|
"hidden-label": {
|
||||||
@@ -6624,8 +6614,8 @@
|
|||||||
"tooltip-show-usages": "显示使用情况"
|
"tooltip-show-usages": "显示使用情况"
|
||||||
},
|
},
|
||||||
"variable-values-preview": {
|
"variable-values-preview": {
|
||||||
"show-more": "显示更多",
|
"preview-of-values": "值预览",
|
||||||
"preview-of-values_other": ""
|
"show-more": "显示更多"
|
||||||
},
|
},
|
||||||
"version-history": {
|
"version-history": {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
|
|||||||
@@ -3743,6 +3743,7 @@
|
|||||||
},
|
},
|
||||||
"recently-viewed": {
|
"recently-viewed": {
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"empty": "",
|
||||||
"error": "",
|
"error": "",
|
||||||
"retry": "",
|
"retry": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
@@ -4396,7 +4397,6 @@
|
|||||||
},
|
},
|
||||||
"no-properties-changed": "沒有相關的屬性變更",
|
"no-properties-changed": "沒有相關的屬性變更",
|
||||||
"table": {
|
"table": {
|
||||||
"notes": "",
|
|
||||||
"updated": "日期",
|
"updated": "日期",
|
||||||
"updatedBy": "更新者",
|
"updatedBy": "更新者",
|
||||||
"version": "版本"
|
"version": "版本"
|
||||||
@@ -4855,8 +4855,7 @@
|
|||||||
"apply": "",
|
"apply": "",
|
||||||
"change-value": "",
|
"change-value": "",
|
||||||
"discard": "",
|
"discard": "",
|
||||||
"modal-title": "",
|
"modal-title": ""
|
||||||
"values": "以逗號分隔的值"
|
|
||||||
},
|
},
|
||||||
"datasource-options": {
|
"datasource-options": {
|
||||||
"name-filter": "名稱篩選",
|
"name-filter": "名稱篩選",
|
||||||
@@ -5948,9 +5947,6 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "自訂選項",
|
"custom-options": "自訂選項",
|
||||||
"json-values-tooltip": "",
|
|
||||||
"name-csv-values": "",
|
|
||||||
"name-json-values": "",
|
|
||||||
"name-values-separated-comma": "以逗號分隔的值",
|
"name-values-separated-comma": "以逗號分隔的值",
|
||||||
"selection-options": "選擇選項"
|
"selection-options": "選擇選項"
|
||||||
},
|
},
|
||||||
@@ -6536,11 +6532,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"use-modal-editor": {
|
|
||||||
"description": {
|
|
||||||
"change-variable-query": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"use-save-dashboard": {
|
"use-save-dashboard": {
|
||||||
"message-dashboard-saved": "儀表板已儲存"
|
"message-dashboard-saved": "儀表板已儲存"
|
||||||
},
|
},
|
||||||
@@ -6564,7 +6555,6 @@
|
|||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"description": "",
|
|
||||||
"label": ""
|
"label": ""
|
||||||
},
|
},
|
||||||
"hidden-label": {
|
"hidden-label": {
|
||||||
@@ -6624,8 +6614,8 @@
|
|||||||
"tooltip-show-usages": "顯示使用情況"
|
"tooltip-show-usages": "顯示使用情況"
|
||||||
},
|
},
|
||||||
"variable-values-preview": {
|
"variable-values-preview": {
|
||||||
"show-more": "顯示更多",
|
"preview-of-values": "數值預覽",
|
||||||
"preview-of-values_other": ""
|
"show-more": "顯示更多"
|
||||||
},
|
},
|
||||||
"version-history": {
|
"version-history": {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
|
|||||||
Reference in New Issue
Block a user