Compare commits
4 Commits
grambbledo
...
sriram/SQL
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40fd558587 | ||
|
|
c5bff2df50 | ||
|
|
c621dbc325 | ||
|
|
ecd3f0b490 |
@@ -13,7 +13,7 @@ import (
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaReceiver = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", NewReceiver(), &ReceiverList{}, resource.WithKind("Receiver"),
|
||||
resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{
|
||||
resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{resource.SelectableField{
|
||||
FieldSelector: "spec.title",
|
||||
FieldValueFunc: func(o resource.Object) (string, error) {
|
||||
cast, ok := o.(*Receiver)
|
||||
|
||||
@@ -790,6 +790,8 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -794,6 +794,8 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -301,6 +301,8 @@ var _ resource.ListObject = &DashboardList{}
|
||||
|
||||
// Copy methods for all subresource types
|
||||
|
||||
|
||||
|
||||
// DeepCopy creates a full deep copy of DashboardStatus
|
||||
func (s *DashboardStatus) DeepCopy() *DashboardStatus {
|
||||
cpy := &DashboardStatus{}
|
||||
|
||||
@@ -301,6 +301,8 @@ var _ resource.ListObject = &DashboardList{}
|
||||
|
||||
// Copy methods for all subresource types
|
||||
|
||||
|
||||
|
||||
// DeepCopy creates a full deep copy of DashboardStatus
|
||||
func (s *DashboardStatus) DeepCopy() *DashboardStatus {
|
||||
cpy := &DashboardStatus{}
|
||||
|
||||
@@ -794,6 +794,8 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -1411,6 +1411,8 @@ type DashboardVariableOption struct {
|
||||
Text DashboardStringOrArrayOfString `json:"text"`
|
||||
// Value of the option
|
||||
Value DashboardStringOrArrayOfString `json:"value"`
|
||||
// Additional properties for multi-props variables
|
||||
Properties map[string]string `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// NewDashboardVariableOption creates a new DashboardVariableOption object.
|
||||
|
||||
@@ -798,6 +798,8 @@ VariableOption: {
|
||||
text: string | [...string]
|
||||
// Value of the option
|
||||
value: string | [...string]
|
||||
// Additional properties for multi-props variables
|
||||
properties?: {[string]: string}
|
||||
}
|
||||
|
||||
// Query variable specification
|
||||
|
||||
@@ -1414,6 +1414,8 @@ type DashboardVariableOption struct {
|
||||
Text DashboardStringOrArrayOfString `json:"text"`
|
||||
// Value of the option
|
||||
Value DashboardStringOrArrayOfString `json:"value"`
|
||||
// Additional properties for multi-props variables
|
||||
Properties map[string]string `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// NewDashboardVariableOption creates a new DashboardVariableOption object.
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
var ()
|
||||
|
||||
var appManifestData = app.ManifestData{
|
||||
AppName: "folder",
|
||||
Group: "folder.grafana.app",
|
||||
|
||||
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/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/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/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/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/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/beevik/etree v1.4.1 // @grafana/grafana-backend-group
|
||||
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/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/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/s3/manager v1.17.84 // 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/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/sts v1.39.1 // indirect
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
|
||||
|
||||
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 { SqlQueryEditorLazy } from './components/QueryEditorLazy';
|
||||
export type { QueryHeaderProps } from './components/QueryHeader';
|
||||
export { SQLVariableSupport } from './SQLVariableSupport';
|
||||
export { createSelectClause, haveColumns } from './utils/sql.utils';
|
||||
export { applyQueryDefaults } from './defaults';
|
||||
export { makeVariable } from './utils/testHelpers';
|
||||
|
||||
@@ -69,6 +69,12 @@
|
||||
"placeholder-select-format": "Select format",
|
||||
"run-query": "Run query"
|
||||
},
|
||||
"query-meta": {
|
||||
"variables": {
|
||||
"textField": "Text Field",
|
||||
"valueField": "Value Field"
|
||||
}
|
||||
},
|
||||
"query-toolbox": {
|
||||
"content-hit-ctrlcmdreturn-to-run-query": "Hit CTRL/CMD+Return to run query",
|
||||
"tooltip-collapse": "Collapse editor",
|
||||
|
||||
@@ -50,6 +50,8 @@ export enum QueryFormat {
|
||||
Table = 'table',
|
||||
}
|
||||
|
||||
export type SQLQueryMeta = { valueField?: string; textField?: string };
|
||||
|
||||
export interface SQLQuery extends DataQuery {
|
||||
alias?: string;
|
||||
format?: QueryFormat;
|
||||
@@ -59,6 +61,7 @@ export interface SQLQuery extends DataQuery {
|
||||
sql?: SQLExpression;
|
||||
editorMode?: EditorMode;
|
||||
rawQuery?: boolean;
|
||||
meta?: SQLQueryMeta;
|
||||
}
|
||||
|
||||
export interface NameValue {
|
||||
|
||||
@@ -11,9 +11,6 @@ import (
|
||||
_ "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
|
||||
_ "github.com/Azure/go-autorest/autorest"
|
||||
_ "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/blugelabs/bluge"
|
||||
_ "github.com/blugelabs/bluge_segment_api"
|
||||
@@ -49,6 +46,7 @@ import (
|
||||
_ "sigs.k8s.io/randfill"
|
||||
_ "xorm.io/builder"
|
||||
|
||||
_ "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
|
||||
_ "github.com/grafana/authlib/authn"
|
||||
_ "github.com/grafana/authlib/authz"
|
||||
_ "github.com/grafana/authlib/cache"
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -27,7 +26,7 @@ type OpenFeatureConfig struct {
|
||||
// HTTPClient is a pre-configured HTTP client (optional, used by features-service + OFREP providers)
|
||||
HTTPClient *http.Client
|
||||
// StaticFlags are the feature flags to use with static provider
|
||||
StaticFlags map[string]memprovider.InMemoryFlag
|
||||
StaticFlags map[string]bool
|
||||
// TargetingKey is used for evaluation context
|
||||
TargetingKey string
|
||||
// ContextAttrs are additional attributes for evaluation context
|
||||
@@ -101,7 +100,7 @@ func InitOpenFeatureWithCfg(cfg *setting.Cfg) error {
|
||||
func createProvider(
|
||||
providerType string,
|
||||
u *url.URL,
|
||||
staticFlags map[string]memprovider.InMemoryFlag,
|
||||
staticFlags map[string]bool,
|
||||
httpClient *http.Client,
|
||||
) (openfeature.FeatureProvider, error) {
|
||||
if providerType == setting.FeaturesServiceProviderType || providerType == setting.OFREPProviderType {
|
||||
@@ -118,7 +117,7 @@ func createProvider(
|
||||
}
|
||||
}
|
||||
|
||||
return newStaticProvider(staticFlags, standardFeatureFlags)
|
||||
return newStaticProvider(staticFlags)
|
||||
}
|
||||
|
||||
func createHTTPClient(m *clientauthmiddleware.TokenExchangeMiddleware) (*http.Client, error) {
|
||||
|
||||
@@ -47,8 +47,7 @@ func ProvideManagerService(cfg *setting.Cfg) (*FeatureManager, error) {
|
||||
}
|
||||
mgmt.warnings[key] = "unknown flag in config"
|
||||
}
|
||||
|
||||
mgmt.startup[key] = val.Variants[val.DefaultVariant] == true
|
||||
mgmt.startup[key] = val
|
||||
}
|
||||
|
||||
// update the values
|
||||
|
||||
@@ -29,7 +29,7 @@ func CreateStaticEvaluator(cfg *setting.Cfg) (StaticFlagEvaluator, error) {
|
||||
return nil, fmt.Errorf("failed to read feature flags from config: %w", err)
|
||||
}
|
||||
|
||||
staticProvider, err := newStaticProvider(staticFlags, standardFeatureFlags)
|
||||
staticProvider, err := newStaticProvider(staticFlags)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create static provider: %w", err)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
package featuremgmt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
// inMemoryBulkProvider is a wrapper around memprovider.InMemoryProvider that
|
||||
@@ -33,21 +28,37 @@ func (p *inMemoryBulkProvider) ListFlags() ([]string, error) {
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func newStaticProvider(confFlags map[string]memprovider.InMemoryFlag, standardFlags []FeatureFlag) (openfeature.FeatureProvider, error) {
|
||||
flags := make(map[string]memprovider.InMemoryFlag, len(standardFlags))
|
||||
|
||||
// Parse and add standard flags
|
||||
for _, flag := range standardFlags {
|
||||
inMemFlag, err := setting.ParseFlag(flag.Name, flag.Expression)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse flag %s: %w", flag.Name, err)
|
||||
}
|
||||
|
||||
flags[flag.Name] = inMemFlag
|
||||
}
|
||||
func newStaticProvider(confFlags map[string]bool) (openfeature.FeatureProvider, error) {
|
||||
flags := make(map[string]memprovider.InMemoryFlag, len(standardFeatureFlags))
|
||||
|
||||
// Add flags from config.ini file
|
||||
maps.Copy(flags, confFlags)
|
||||
for name, value := range confFlags {
|
||||
flags[name] = createInMemoryFlag(name, value)
|
||||
}
|
||||
|
||||
// Add standard flags
|
||||
for _, flag := range standardFeatureFlags {
|
||||
if _, exists := flags[flag.Name]; !exists {
|
||||
enabled := flag.Expression == "true"
|
||||
flags[flag.Name] = createInMemoryFlag(flag.Name, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
return newInMemoryBulkProvider(flags), nil
|
||||
}
|
||||
|
||||
func createInMemoryFlag(name string, enabled bool) memprovider.InMemoryFlag {
|
||||
variant := "disabled"
|
||||
if enabled {
|
||||
variant = "enabled"
|
||||
}
|
||||
|
||||
return memprovider.InMemoryFlag{
|
||||
Key: name,
|
||||
DefaultVariant: variant,
|
||||
Variants: map[string]interface{}{
|
||||
"enabled": true,
|
||||
"disabled": false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -94,147 +93,3 @@ ABCD = true
|
||||
enabledFeatureManager := mgr.GetEnabled(ctx)
|
||||
assert.Equal(t, openFeatureEnabledFlags, enabledFeatureManager)
|
||||
}
|
||||
|
||||
func Test_StaticProvider_TypedFlags(t *testing.T) {
|
||||
tests := []struct {
|
||||
flags FeatureFlag
|
||||
defaultValue any
|
||||
expectedValue any
|
||||
}{
|
||||
{
|
||||
flags: FeatureFlag{
|
||||
Name: "Flag",
|
||||
Expression: "true",
|
||||
},
|
||||
defaultValue: false,
|
||||
expectedValue: true,
|
||||
},
|
||||
{
|
||||
flags: FeatureFlag{
|
||||
Name: "Flag",
|
||||
Expression: "1.0",
|
||||
},
|
||||
defaultValue: 0.0,
|
||||
expectedValue: 1.0,
|
||||
},
|
||||
{
|
||||
flags: FeatureFlag{
|
||||
Name: "Flag",
|
||||
Expression: "blue",
|
||||
},
|
||||
defaultValue: "red",
|
||||
expectedValue: "blue",
|
||||
},
|
||||
{
|
||||
flags: FeatureFlag{
|
||||
Name: "Flag",
|
||||
Expression: "1",
|
||||
},
|
||||
defaultValue: int64(0),
|
||||
expectedValue: int64(1),
|
||||
},
|
||||
{
|
||||
flags: FeatureFlag{
|
||||
Name: "Flag",
|
||||
Expression: `{ "foo": "bar" }`,
|
||||
},
|
||||
expectedValue: map[string]any{"foo": "bar"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
provider, err := newStaticProvider(nil, []FeatureFlag{tt.flags})
|
||||
assert.NoError(t, err)
|
||||
|
||||
var result any
|
||||
switch tt.expectedValue.(type) {
|
||||
case bool:
|
||||
result = provider.BooleanEvaluation(t.Context(), tt.flags.Name, tt.defaultValue.(bool), openfeature.FlattenedContext{}).Value
|
||||
case float64:
|
||||
result = provider.FloatEvaluation(t.Context(), tt.flags.Name, tt.defaultValue.(float64), openfeature.FlattenedContext{}).Value
|
||||
case string:
|
||||
result = provider.StringEvaluation(t.Context(), tt.flags.Name, tt.defaultValue.(string), openfeature.FlattenedContext{}).Value
|
||||
case int64:
|
||||
result = provider.IntEvaluation(t.Context(), tt.flags.Name, tt.defaultValue.(int64), openfeature.FlattenedContext{}).Value
|
||||
case map[string]any:
|
||||
result = provider.ObjectEvaluation(t.Context(), tt.flags.Name, tt.defaultValue, openfeature.FlattenedContext{}).Value
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expectedValue, result)
|
||||
}
|
||||
}
|
||||
func Test_StaticProvider_ConfigOverride(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
originalValue string
|
||||
configValue any
|
||||
}{
|
||||
{
|
||||
name: "bool",
|
||||
originalValue: "false",
|
||||
configValue: true,
|
||||
},
|
||||
{
|
||||
name: "int",
|
||||
originalValue: "0",
|
||||
configValue: int64(1),
|
||||
},
|
||||
{
|
||||
name: "float",
|
||||
originalValue: "0.0",
|
||||
configValue: 1.0,
|
||||
},
|
||||
{
|
||||
name: "string",
|
||||
originalValue: "foo",
|
||||
configValue: "bar",
|
||||
},
|
||||
{
|
||||
name: "structure",
|
||||
originalValue: "{}",
|
||||
configValue: make(map[string]any),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
configFlags, standardFlags := makeFlags(tt)
|
||||
provider, err := newStaticProvider(configFlags, standardFlags)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var result any
|
||||
switch tt.configValue.(type) {
|
||||
case bool:
|
||||
result = provider.BooleanEvaluation(t.Context(), tt.name, false, openfeature.FlattenedContext{}).Value
|
||||
case float64:
|
||||
result = provider.FloatEvaluation(t.Context(), tt.name, 0.0, openfeature.FlattenedContext{}).Value
|
||||
case string:
|
||||
result = provider.StringEvaluation(t.Context(), tt.name, "foo", openfeature.FlattenedContext{}).Value
|
||||
case int64:
|
||||
result = provider.IntEvaluation(t.Context(), tt.name, 1, openfeature.FlattenedContext{}).Value
|
||||
case map[string]any:
|
||||
result = provider.ObjectEvaluation(t.Context(), tt.name, make(map[string]any), openfeature.FlattenedContext{}).Value
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.configValue, result)
|
||||
}
|
||||
}
|
||||
|
||||
func makeFlags(tt struct {
|
||||
name string
|
||||
originalValue string
|
||||
configValue any
|
||||
}) (map[string]memprovider.InMemoryFlag, []FeatureFlag) {
|
||||
orig := FeatureFlag{
|
||||
Name: tt.name,
|
||||
Expression: tt.originalValue,
|
||||
}
|
||||
|
||||
config := map[string]memprovider.InMemoryFlag{
|
||||
tt.name: {
|
||||
Key: tt.name,
|
||||
Variants: map[string]any{"": tt.configValue},
|
||||
},
|
||||
}
|
||||
|
||||
return config, []FeatureFlag{orig}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
@@ -379,10 +378,8 @@ func setupOpenFeatureProvider(t *testing.T, flagValue bool) {
|
||||
|
||||
err := featuremgmt.InitOpenFeature(featuremgmt.OpenFeatureConfig{
|
||||
ProviderType: setting.StaticProviderType,
|
||||
StaticFlags: map[string]memprovider.InMemoryFlag{
|
||||
featuremgmt.FlagPluginsAutoUpdate: {
|
||||
Key: featuremgmt.FlagPluginsAutoUpdate, Variants: map[string]any{"": flagValue},
|
||||
},
|
||||
StaticFlags: map[string]bool{
|
||||
featuremgmt.FlagPluginsAutoUpdate: flagValue,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
@@ -18,27 +15,18 @@ func (cfg *Cfg) readFeatureToggles(iniFile *ini.File) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO IsFeatureToggleEnabled has been deprecated for 2 years now, we should remove this function completely
|
||||
// nolint:staticcheck
|
||||
cfg.IsFeatureToggleEnabled = func(key string) bool {
|
||||
toggle, ok := toggles[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
val, ok := toggle.Variants[toggle.DefaultVariant].(bool)
|
||||
return ok && val
|
||||
}
|
||||
cfg.IsFeatureToggleEnabled = func(key string) bool { return toggles[key] }
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]memprovider.InMemoryFlag, error) {
|
||||
featureToggles := make(map[string]memprovider.InMemoryFlag, 10)
|
||||
func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[string]bool, error) {
|
||||
featureToggles := make(map[string]bool, 10)
|
||||
|
||||
// parse the comma separated list in `enable`.
|
||||
featuresTogglesStr := valueAsString(featureTogglesSection, "enable", "")
|
||||
for _, feature := range util.SplitString(featuresTogglesStr) {
|
||||
featureToggles[feature] = memprovider.InMemoryFlag{Key: feature, Variants: map[string]any{"": true}}
|
||||
featureToggles[feature] = true
|
||||
}
|
||||
|
||||
// read all other settings under [feature_toggles]. If a toggle is
|
||||
@@ -48,7 +36,7 @@ func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[str
|
||||
continue
|
||||
}
|
||||
|
||||
b, err := ParseFlag(v.Name(), v.Value())
|
||||
b, err := strconv.ParseBool(v.Value())
|
||||
if err != nil {
|
||||
return featureToggles, err
|
||||
}
|
||||
@@ -57,24 +45,3 @@ func ReadFeatureTogglesFromInitFile(featureTogglesSection *ini.Section) (map[str
|
||||
}
|
||||
return featureToggles, nil
|
||||
}
|
||||
|
||||
func ParseFlag(name, value string) (memprovider.InMemoryFlag, error) {
|
||||
if integer, err := strconv.Atoi(value); err == nil {
|
||||
return memprovider.InMemoryFlag{Key: name, Variants: map[string]any{"": integer}}, nil
|
||||
}
|
||||
|
||||
if float, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
return memprovider.InMemoryFlag{Key: name, Variants: map[string]any{"": float}}, nil
|
||||
}
|
||||
|
||||
var structure map[string]any
|
||||
if err := json.Unmarshal([]byte(value), &structure); err == nil {
|
||||
return memprovider.InMemoryFlag{Key: name, Variants: map[string]any{"": structure}}, nil
|
||||
}
|
||||
|
||||
if boolean, err := strconv.ParseBool(value); err == nil {
|
||||
return memprovider.InMemoryFlag{Key: name, Variants: map[string]any{"": boolean}}, nil
|
||||
}
|
||||
|
||||
return memprovider.InMemoryFlag{Key: name, Variants: map[string]any{"": value}}, nil
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
@@ -12,16 +12,17 @@ func TestFeatureToggles(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
conf map[string]string
|
||||
expectedToggles map[string]memprovider.InMemoryFlag
|
||||
err error
|
||||
expectedToggles map[string]bool
|
||||
}{
|
||||
{
|
||||
name: "can parse feature toggles passed in the `enable` array",
|
||||
conf: map[string]string{
|
||||
"enable": "feature1,feature2",
|
||||
},
|
||||
expectedToggles: map[string]memprovider.InMemoryFlag{
|
||||
"feature1": {Key: "feature1", Variants: map[string]any{"": true}},
|
||||
"feature2": {Key: "feature2", Variants: map[string]any{"": true}},
|
||||
expectedToggles: map[string]bool{
|
||||
"feature1": true,
|
||||
"feature2": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -30,10 +31,10 @@ func TestFeatureToggles(t *testing.T) {
|
||||
"enable": "feature1,feature2",
|
||||
"feature3": "true",
|
||||
},
|
||||
expectedToggles: map[string]memprovider.InMemoryFlag{
|
||||
"feature1": {Key: "feature1", Variants: map[string]any{"": true}},
|
||||
"feature2": {Key: "feature2", Variants: map[string]any{"": true}},
|
||||
"feature3": {Key: "feature3", Variants: map[string]any{"": true}},
|
||||
expectedToggles: map[string]bool{
|
||||
"feature1": true,
|
||||
"feature2": true,
|
||||
"feature3": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -42,26 +43,19 @@ func TestFeatureToggles(t *testing.T) {
|
||||
"enable": "feature1,feature2",
|
||||
"feature2": "false",
|
||||
},
|
||||
expectedToggles: map[string]memprovider.InMemoryFlag{
|
||||
"feature1": {Key: "feature1", Variants: map[string]any{"": true}},
|
||||
"feature2": {Key: "feature2", Variants: map[string]any{"": false}},
|
||||
expectedToggles: map[string]bool{
|
||||
"feature1": true,
|
||||
"feature2": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "type of the feature flag is handled correctly",
|
||||
name: "invalid boolean value should return syntax error",
|
||||
conf: map[string]string{
|
||||
"feature1": "1", "feature2": "1.0",
|
||||
"feature3": `{"foo":"bar"}`, "feature4": "bar",
|
||||
"feature5": "t", "feature6": "T",
|
||||
},
|
||||
expectedToggles: map[string]memprovider.InMemoryFlag{
|
||||
"feature1": {Key: "feature1", Variants: map[string]any{"": 1}},
|
||||
"feature2": {Key: "feature2", Variants: map[string]any{"": 1.0}},
|
||||
"feature3": {Key: "feature3", Variants: map[string]any{"": map[string]any{"foo": "bar"}}},
|
||||
"feature4": {Key: "feature4", Variants: map[string]any{"": "bar"}},
|
||||
"feature5": {Key: "feature5", Variants: map[string]any{"": true}},
|
||||
"feature6": {Key: "feature6", Variants: map[string]any{"": true}},
|
||||
"enable": "feature1,feature2",
|
||||
"feature2": "invalid",
|
||||
},
|
||||
expectedToggles: map[string]bool{},
|
||||
err: strconv.ErrSyntax,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -75,11 +69,12 @@ func TestFeatureToggles(t *testing.T) {
|
||||
}
|
||||
|
||||
featureToggles, err := ReadFeatureTogglesFromInitFile(toggles)
|
||||
require.NoError(t, err)
|
||||
require.ErrorIs(t, err, tc.err)
|
||||
|
||||
for k, v := range featureToggles {
|
||||
toggle := tc.expectedToggles[k]
|
||||
require.Equal(t, toggle, v, tc.name)
|
||||
if err == nil {
|
||||
for k, v := range featureToggles {
|
||||
require.Equal(t, tc.expectedToggles[k], v, tc.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -806,10 +806,8 @@ flowchart TD
|
||||
|
||||
#### Setting Dual Writer Mode
|
||||
```ini
|
||||
; [unified_storage.{resource}.{group}]
|
||||
[unified_storage.dashboards.dashboard.grafana.app]
|
||||
; modes {0-5}
|
||||
dualWriterMode = 0
|
||||
[unified_storage.{resource}.{kind}.{group}]
|
||||
dualWriterMode = {0-5}
|
||||
```
|
||||
|
||||
#### Background Sync Configuration
|
||||
@@ -1378,3 +1376,4 @@ disable_data_migrations = false
|
||||
### Documentation
|
||||
|
||||
For detailed information about migration architecture, validators, and troubleshooting, refer to [migrations/README.md](./migrations/README.md).
|
||||
|
||||
@@ -44,6 +44,6 @@ func TestIntegrationFeatures(t *testing.T) {
|
||||
"value": true,
|
||||
"key":"`+flag+`",
|
||||
"reason":"static provider evaluation result",
|
||||
"variant":"default"}`, string(rsp.Body))
|
||||
"variant":"enabled"}`, string(rsp.Body))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export const getFormFieldsForSilence = (silence: Silence): SilenceFormFields =>
|
||||
startsAt: interval.start.toISOString(),
|
||||
endsAt: interval.end.toISOString(),
|
||||
comment: silence.comment,
|
||||
createdBy: isExpired ? contextSrv.user.name : silence.createdBy,
|
||||
createdBy: silence.createdBy,
|
||||
duration: intervalToAbbreviatedDurationString(interval),
|
||||
isRegex: false,
|
||||
matchers: silence.matchers?.map(matcherToMatcherField) || [],
|
||||
|
||||
@@ -284,6 +284,7 @@ function variableValueOptionsToVariableOptions(varState: MultiValueVariable['sta
|
||||
value: String(o.value),
|
||||
text: o.label,
|
||||
selected: Array.isArray(varState.value) ? varState.value.includes(o.value) : varState.value === o.value,
|
||||
...(o.properties && { properties: o.properties }),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -80,18 +80,18 @@ const buildLabelPath = (label: string) => {
|
||||
return label.includes('.') || label.trim().includes(' ') ? `["${label}"]` : `.${label}`;
|
||||
};
|
||||
|
||||
const getVariableValueProperties = (variable: TypedVariableModel): string[] => {
|
||||
if (!('valuesFormat' in variable) || variable.valuesFormat !== 'json') {
|
||||
return [];
|
||||
}
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> => {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
};
|
||||
|
||||
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[] = [];
|
||||
for (const field in option) {
|
||||
if (option.hasOwnProperty(field)) {
|
||||
const newPath = `${currentPath}.${field}`;
|
||||
const value = option[field];
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (isRecord(value)) {
|
||||
paths = [...paths, ...collectFieldPaths(value, newPath)];
|
||||
}
|
||||
paths.push(newPath);
|
||||
@@ -100,11 +100,23 @@ const getVariableValueProperties = (variable: TypedVariableModel): string[] => {
|
||||
return paths;
|
||||
}
|
||||
|
||||
try {
|
||||
return collectFieldPaths(JSON.parse(variable.query)[0], variable.name);
|
||||
} catch {
|
||||
return [];
|
||||
if ('valuesFormat' in variable && variable.valuesFormat === 'json') {
|
||||
try {
|
||||
return collectFieldPaths(JSON.parse(variable.query)[0], variable.name);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
if ('options' in variable && Array.isArray(variable.options) && variable.options.length > 0) {
|
||||
for (const opt of variable.options) {
|
||||
if ('properties' in opt && isRecord(opt.properties) && Object.keys(opt.properties).length > 0) {
|
||||
return collectFieldPaths(opt.properties, variable.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
SQLQuery,
|
||||
SQLSelectableValue,
|
||||
SqlDatasource,
|
||||
SQLVariableSupport,
|
||||
formatSQL,
|
||||
} from '@grafana/sql';
|
||||
|
||||
@@ -25,6 +26,7 @@ export class PostgresDatasource extends SqlDatasource {
|
||||
|
||||
constructor(instanceSettings: DataSourceInstanceSettings<PostgresOptions>) {
|
||||
super(instanceSettings);
|
||||
this.variables = new SQLVariableSupport(this);
|
||||
}
|
||||
|
||||
getQueryModel(target?: SQLQuery, templateSrv?: TemplateSrv, scopedVars?: ScopedVars): PostgresQueryModel {
|
||||
|
||||
Reference in New Issue
Block a user