mirror of
https://github.com/grafana/grafana.git
synced 2026-02-03 20:49:50 -05:00
GrafanaBootData: Deprecate config.apps (#115610)
* GrafanaBootData: decouple `config.apps` from boot data IV * chore: changed to openfeature flags eval * chore: updates after PR feedback * chore: updates after PR feedback * chore: copy types to runtime package * chore: add code ownership * chore: deprecate in interface too * chore: add important notice to comments * chore: deprecate the whole interface
This commit is contained in:
parent
215d25ef69
commit
bd0140b6f0
34 changed files with 6718 additions and 6 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
|
@ -658,6 +658,7 @@ i18next.config.ts @grafana/grafana-frontend-platform
|
|||
/packages/grafana-runtime/src/services/LocationService.tsx @grafana/grafana-search-navigate-organise
|
||||
/packages/grafana-runtime/src/services/LocationSrv.ts @grafana/grafana-search-navigate-organise
|
||||
/packages/grafana-runtime/src/services/live.ts @grafana/dashboards-squad
|
||||
/packages/grafana-runtime/src/services/pluginMeta @grafana/plugins-platform-frontend
|
||||
/packages/grafana-runtime/src/utils/chromeHeaderHeight.ts @grafana/grafana-search-navigate-organise
|
||||
/packages/grafana-runtime/src/utils/DataSourceWithBackend* @grafana/grafana-datasources-core-services
|
||||
/packages/grafana-runtime/src/utils/licensing.ts @grafana/grafana-operator-experience-squad
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
include ../sdk.mk
|
||||
|
||||
.PHONY: generate # Run Grafana App SDK code generation
|
||||
generate: install-app-sdk update-app-sdk
|
||||
.PHONY: internal-generate # Run Grafana App SDK code generation
|
||||
internal-generate: install-app-sdk update-app-sdk
|
||||
@$(APP_SDK_BIN) generate \
|
||||
--source=./kinds/ \
|
||||
--gogenpath=./pkg/apis \
|
||||
--grouping=group \
|
||||
--defencoding=none
|
||||
--defencoding=none
|
||||
|
||||
.PHONY: generate
|
||||
generate: internal-generate # copy files to packages/grafana-runtime/src/services/pluginMeta/types
|
||||
rm -f ./packages/grafana-runtime/src/services/pluginMeta/types/*.ts
|
||||
cp plugin/src/generated/meta/v0alpha1/meta_object_gen.ts ../../packages/grafana-runtime/src/services/pluginMeta/types/meta_object_gen.ts
|
||||
cp plugin/src/generated/meta/v0alpha1/types.spec.gen.ts ../../packages/grafana-runtime/src/services/pluginMeta/types/types.spec.gen.ts
|
||||
cp plugin/src/generated/meta/v0alpha1/types.status.gen.ts ../../packages/grafana-runtime/src/services/pluginMeta/types/types.status.gen.ts
|
||||
|
|
@ -4,8 +4,7 @@ API documentation is available at http://localhost:3000/swagger?api=plugins.graf
|
|||
|
||||
## Codegen
|
||||
|
||||
- Go: `make generate`
|
||||
- Frontend: Follow instructions in this [README](../..//packages/grafana-api-clients/README.md)
|
||||
- Go and TypeScript: `make generate`
|
||||
|
||||
## Plugin sync
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ manifest: {
|
|||
v0alpha1Version: {
|
||||
served: true
|
||||
codegen: {
|
||||
ts: {enabled: false}
|
||||
ts: {enabled: true}
|
||||
go: {enabled: true}
|
||||
}
|
||||
kinds: [
|
||||
|
|
|
|||
49
apps/plugins/plugin/src/generated/meta/v0alpha1/meta_object_gen.ts
generated
Normal file
49
apps/plugins/plugin/src/generated/meta/v0alpha1/meta_object_gen.ts
generated
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* This file was generated by grafana-app-sdk. DO NOT EDIT.
|
||||
*/
|
||||
import { Spec } from './types.spec.gen';
|
||||
import { Status } from './types.status.gen';
|
||||
|
||||
export interface Metadata {
|
||||
name: string;
|
||||
namespace: string;
|
||||
generateName?: string;
|
||||
selfLink?: string;
|
||||
uid?: string;
|
||||
resourceVersion?: string;
|
||||
generation?: number;
|
||||
creationTimestamp?: string;
|
||||
deletionTimestamp?: string;
|
||||
deletionGracePeriodSeconds?: number;
|
||||
labels?: Record<string, string>;
|
||||
annotations?: Record<string, string>;
|
||||
ownerReferences?: OwnerReference[];
|
||||
finalizers?: string[];
|
||||
managedFields?: ManagedFieldsEntry[];
|
||||
}
|
||||
|
||||
export interface OwnerReference {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
uid: string;
|
||||
controller?: boolean;
|
||||
blockOwnerDeletion?: boolean;
|
||||
}
|
||||
|
||||
export interface ManagedFieldsEntry {
|
||||
manager?: string;
|
||||
operation?: string;
|
||||
apiVersion?: string;
|
||||
time?: string;
|
||||
fieldsType?: string;
|
||||
subresource?: string;
|
||||
}
|
||||
|
||||
export interface Meta {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
metadata: Metadata;
|
||||
spec: Spec;
|
||||
status: Status;
|
||||
}
|
||||
30
apps/plugins/plugin/src/generated/meta/v0alpha1/types.metadata.gen.ts
generated
Normal file
30
apps/plugins/plugin/src/generated/meta/v0alpha1/types.metadata.gen.ts
generated
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
// metadata contains embedded CommonMetadata and can be extended with custom string fields
|
||||
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
|
||||
// without external reference as using the CommonMetadata reference breaks thema codegen.
|
||||
export interface Metadata {
|
||||
updateTimestamp: string;
|
||||
createdBy: string;
|
||||
uid: string;
|
||||
creationTimestamp: string;
|
||||
deletionTimestamp?: string;
|
||||
finalizers: string[];
|
||||
resourceVersion: string;
|
||||
generation: number;
|
||||
updatedBy: string;
|
||||
labels: Record<string, string>;
|
||||
}
|
||||
|
||||
export const defaultMetadata = (): Metadata => ({
|
||||
updateTimestamp: "",
|
||||
createdBy: "",
|
||||
uid: "",
|
||||
creationTimestamp: "",
|
||||
finalizers: [],
|
||||
resourceVersion: "",
|
||||
generation: 0,
|
||||
updatedBy: "",
|
||||
labels: {},
|
||||
});
|
||||
|
||||
278
apps/plugins/plugin/src/generated/meta/v0alpha1/types.spec.gen.ts
generated
Normal file
278
apps/plugins/plugin/src/generated/meta/v0alpha1/types.spec.gen.ts
generated
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
// JSON configuration schema for Grafana plugins
|
||||
// Converted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json
|
||||
export interface JSONData {
|
||||
// Unique name of the plugin
|
||||
id: string;
|
||||
// Plugin type
|
||||
type: "app" | "datasource" | "panel" | "renderer";
|
||||
// Human-readable name of the plugin
|
||||
name: string;
|
||||
// Metadata for the plugin
|
||||
info: Info;
|
||||
// Dependency information
|
||||
dependencies: Dependencies;
|
||||
// Optional fields
|
||||
alerting?: boolean;
|
||||
annotations?: boolean;
|
||||
autoEnabled?: boolean;
|
||||
backend?: boolean;
|
||||
buildMode?: string;
|
||||
builtIn?: boolean;
|
||||
category?: "tsdb" | "logging" | "cloud" | "tracing" | "profiling" | "sql" | "enterprise" | "iot" | "other";
|
||||
enterpriseFeatures?: EnterpriseFeatures;
|
||||
executable?: string;
|
||||
hideFromList?: boolean;
|
||||
// +listType=atomic
|
||||
includes?: Include[];
|
||||
logs?: boolean;
|
||||
metrics?: boolean;
|
||||
multiValueFilterOperators?: boolean;
|
||||
pascalName?: string;
|
||||
preload?: boolean;
|
||||
queryOptions?: QueryOptions;
|
||||
// +listType=atomic
|
||||
routes?: Route[];
|
||||
skipDataQuery?: boolean;
|
||||
state?: "alpha" | "beta";
|
||||
streaming?: boolean;
|
||||
suggestions?: boolean;
|
||||
tracing?: boolean;
|
||||
iam?: IAM;
|
||||
// +listType=atomic
|
||||
roles?: Role[];
|
||||
extensions?: Extensions;
|
||||
}
|
||||
|
||||
export const defaultJSONData = (): JSONData => ({
|
||||
id: "",
|
||||
type: "app",
|
||||
name: "",
|
||||
info: defaultInfo(),
|
||||
dependencies: defaultDependencies(),
|
||||
});
|
||||
|
||||
export interface Info {
|
||||
// Required fields
|
||||
// +listType=set
|
||||
keywords: string[];
|
||||
logos: {
|
||||
small: string;
|
||||
large: string;
|
||||
};
|
||||
updated: string;
|
||||
version: string;
|
||||
// Optional fields
|
||||
author?: {
|
||||
name?: string;
|
||||
email?: string;
|
||||
url?: string;
|
||||
};
|
||||
description?: string;
|
||||
// +listType=atomic
|
||||
links?: {
|
||||
name?: string;
|
||||
url?: string;
|
||||
}[];
|
||||
// +listType=atomic
|
||||
screenshots?: {
|
||||
name?: string;
|
||||
path?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const defaultInfo = (): Info => ({
|
||||
keywords: [],
|
||||
logos: {
|
||||
small: "",
|
||||
large: "",
|
||||
},
|
||||
updated: "",
|
||||
version: "",
|
||||
});
|
||||
|
||||
export interface Dependencies {
|
||||
// Required field
|
||||
grafanaDependency: string;
|
||||
// Optional fields
|
||||
grafanaVersion?: string;
|
||||
// +listType=set
|
||||
// +listMapKey=id
|
||||
plugins?: {
|
||||
id: string;
|
||||
type: "app" | "datasource" | "panel";
|
||||
name: string;
|
||||
}[];
|
||||
extensions?: {
|
||||
// +listType=set
|
||||
exposedComponents?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultDependencies = (): Dependencies => ({
|
||||
grafanaDependency: "",
|
||||
});
|
||||
|
||||
export interface EnterpriseFeatures {
|
||||
// Allow additional properties
|
||||
healthDiagnosticsErrors?: boolean;
|
||||
}
|
||||
|
||||
export const defaultEnterpriseFeatures = (): EnterpriseFeatures => ({
|
||||
healthDiagnosticsErrors: false,
|
||||
});
|
||||
|
||||
export interface Include {
|
||||
uid?: string;
|
||||
type?: "dashboard" | "page" | "panel" | "datasource";
|
||||
name?: string;
|
||||
component?: string;
|
||||
role?: "Admin" | "Editor" | "Viewer" | "None";
|
||||
action?: string;
|
||||
path?: string;
|
||||
addToNav?: boolean;
|
||||
defaultNav?: boolean;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export const defaultInclude = (): Include => ({
|
||||
});
|
||||
|
||||
export interface QueryOptions {
|
||||
maxDataPoints?: boolean;
|
||||
minInterval?: boolean;
|
||||
cacheTimeout?: boolean;
|
||||
}
|
||||
|
||||
export const defaultQueryOptions = (): QueryOptions => ({
|
||||
});
|
||||
|
||||
export interface Route {
|
||||
path?: string;
|
||||
method?: string;
|
||||
url?: string;
|
||||
reqSignedIn?: boolean;
|
||||
reqRole?: string;
|
||||
reqAction?: string;
|
||||
// +listType=atomic
|
||||
headers?: string[];
|
||||
body?: Record<string, any>;
|
||||
tokenAuth?: {
|
||||
url?: string;
|
||||
// +listType=set
|
||||
scopes?: string[];
|
||||
params?: Record<string, any>;
|
||||
};
|
||||
jwtTokenAuth?: {
|
||||
url?: string;
|
||||
// +listType=set
|
||||
scopes?: string[];
|
||||
params?: Record<string, any>;
|
||||
};
|
||||
// +listType=atomic
|
||||
urlParams?: {
|
||||
name?: string;
|
||||
content?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const defaultRoute = (): Route => ({
|
||||
});
|
||||
|
||||
export interface IAM {
|
||||
// +listType=atomic
|
||||
permissions?: {
|
||||
action?: string;
|
||||
scope?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const defaultIAM = (): IAM => ({
|
||||
});
|
||||
|
||||
export interface Role {
|
||||
role?: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
// +listType=atomic
|
||||
permissions?: {
|
||||
action?: string;
|
||||
scope?: string;
|
||||
}[];
|
||||
};
|
||||
// +listType=set
|
||||
grants?: string[];
|
||||
}
|
||||
|
||||
export const defaultRole = (): Role => ({
|
||||
});
|
||||
|
||||
export interface Extensions {
|
||||
// +listType=atomic
|
||||
addedComponents?: {
|
||||
// +listType=set
|
||||
targets: string[];
|
||||
title: string;
|
||||
description?: string;
|
||||
}[];
|
||||
// +listType=atomic
|
||||
addedLinks?: {
|
||||
// +listType=set
|
||||
targets: string[];
|
||||
title: string;
|
||||
description?: string;
|
||||
}[];
|
||||
// +listType=atomic
|
||||
addedFunctions?: {
|
||||
// +listType=set
|
||||
targets: string[];
|
||||
title: string;
|
||||
description?: string;
|
||||
}[];
|
||||
// +listType=set
|
||||
// +listMapKey=id
|
||||
exposedComponents?: {
|
||||
id: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}[];
|
||||
// +listType=set
|
||||
// +listMapKey=id
|
||||
extensionPoints?: {
|
||||
id: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const defaultExtensions = (): Extensions => ({
|
||||
});
|
||||
|
||||
export interface Spec {
|
||||
pluginJson: JSONData;
|
||||
class: "core" | "external";
|
||||
module?: {
|
||||
path: string;
|
||||
hash?: string;
|
||||
loadingStrategy?: "fetch" | "script";
|
||||
};
|
||||
baseURL?: string;
|
||||
signature?: {
|
||||
status: "internal" | "valid" | "invalid" | "modified" | "unsigned";
|
||||
type?: "grafana" | "commercial" | "community" | "private" | "private-glob";
|
||||
org?: string;
|
||||
};
|
||||
angular?: {
|
||||
detected: boolean;
|
||||
};
|
||||
translations?: Record<string, string>;
|
||||
// +listType=atomic
|
||||
children?: string[];
|
||||
}
|
||||
|
||||
export const defaultSpec = (): Spec => ({
|
||||
pluginJson: defaultJSONData(),
|
||||
class: "core",
|
||||
});
|
||||
|
||||
30
apps/plugins/plugin/src/generated/meta/v0alpha1/types.status.gen.ts
generated
Normal file
30
apps/plugins/plugin/src/generated/meta/v0alpha1/types.status.gen.ts
generated
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
export interface OperatorState {
|
||||
// lastEvaluation is the ResourceVersion last evaluated
|
||||
lastEvaluation: string;
|
||||
// state describes the state of the lastEvaluation.
|
||||
// It is limited to three possible states for machine evaluation.
|
||||
state: "success" | "in_progress" | "failed";
|
||||
// descriptiveState is an optional more descriptive state field which has no requirements on format
|
||||
descriptiveState?: string;
|
||||
// details contains any extra information that is operator-specific
|
||||
details?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const defaultOperatorState = (): OperatorState => ({
|
||||
lastEvaluation: "",
|
||||
state: "success",
|
||||
});
|
||||
|
||||
export interface Status {
|
||||
// operatorStates is a map of operator ID to operator state evaluations.
|
||||
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
|
||||
operatorStates?: Record<string, OperatorState>;
|
||||
// additionalFields is reserved for future use
|
||||
additionalFields?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const defaultStatus = (): Status => ({
|
||||
});
|
||||
|
||||
49
apps/plugins/plugin/src/generated/plugin/v0alpha1/plugin_object_gen.ts
generated
Normal file
49
apps/plugins/plugin/src/generated/plugin/v0alpha1/plugin_object_gen.ts
generated
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* This file was generated by grafana-app-sdk. DO NOT EDIT.
|
||||
*/
|
||||
import { Spec } from './types.spec.gen';
|
||||
import { Status } from './types.status.gen';
|
||||
|
||||
export interface Metadata {
|
||||
name: string;
|
||||
namespace: string;
|
||||
generateName?: string;
|
||||
selfLink?: string;
|
||||
uid?: string;
|
||||
resourceVersion?: string;
|
||||
generation?: number;
|
||||
creationTimestamp?: string;
|
||||
deletionTimestamp?: string;
|
||||
deletionGracePeriodSeconds?: number;
|
||||
labels?: Record<string, string>;
|
||||
annotations?: Record<string, string>;
|
||||
ownerReferences?: OwnerReference[];
|
||||
finalizers?: string[];
|
||||
managedFields?: ManagedFieldsEntry[];
|
||||
}
|
||||
|
||||
export interface OwnerReference {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
uid: string;
|
||||
controller?: boolean;
|
||||
blockOwnerDeletion?: boolean;
|
||||
}
|
||||
|
||||
export interface ManagedFieldsEntry {
|
||||
manager?: string;
|
||||
operation?: string;
|
||||
apiVersion?: string;
|
||||
time?: string;
|
||||
fieldsType?: string;
|
||||
subresource?: string;
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
metadata: Metadata;
|
||||
spec: Spec;
|
||||
status: Status;
|
||||
}
|
||||
30
apps/plugins/plugin/src/generated/plugin/v0alpha1/types.metadata.gen.ts
generated
Normal file
30
apps/plugins/plugin/src/generated/plugin/v0alpha1/types.metadata.gen.ts
generated
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
// metadata contains embedded CommonMetadata and can be extended with custom string fields
|
||||
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
|
||||
// without external reference as using the CommonMetadata reference breaks thema codegen.
|
||||
export interface Metadata {
|
||||
updateTimestamp: string;
|
||||
createdBy: string;
|
||||
uid: string;
|
||||
creationTimestamp: string;
|
||||
deletionTimestamp?: string;
|
||||
finalizers: string[];
|
||||
resourceVersion: string;
|
||||
generation: number;
|
||||
updatedBy: string;
|
||||
labels: Record<string, string>;
|
||||
}
|
||||
|
||||
export const defaultMetadata = (): Metadata => ({
|
||||
updateTimestamp: "",
|
||||
createdBy: "",
|
||||
uid: "",
|
||||
creationTimestamp: "",
|
||||
finalizers: [],
|
||||
resourceVersion: "",
|
||||
generation: 0,
|
||||
updatedBy: "",
|
||||
labels: {},
|
||||
});
|
||||
|
||||
13
apps/plugins/plugin/src/generated/plugin/v0alpha1/types.spec.gen.ts
generated
Normal file
13
apps/plugins/plugin/src/generated/plugin/v0alpha1/types.spec.gen.ts
generated
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
export interface Spec {
|
||||
id: string;
|
||||
version: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export const defaultSpec = (): Spec => ({
|
||||
id: "",
|
||||
version: "",
|
||||
});
|
||||
|
||||
30
apps/plugins/plugin/src/generated/plugin/v0alpha1/types.status.gen.ts
generated
Normal file
30
apps/plugins/plugin/src/generated/plugin/v0alpha1/types.status.gen.ts
generated
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
export interface OperatorState {
|
||||
// lastEvaluation is the ResourceVersion last evaluated
|
||||
lastEvaluation: string;
|
||||
// state describes the state of the lastEvaluation.
|
||||
// It is limited to three possible states for machine evaluation.
|
||||
state: "success" | "in_progress" | "failed";
|
||||
// descriptiveState is an optional more descriptive state field which has no requirements on format
|
||||
descriptiveState?: string;
|
||||
// details contains any extra information that is operator-specific
|
||||
details?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const defaultOperatorState = (): OperatorState => ({
|
||||
lastEvaluation: "",
|
||||
state: "success",
|
||||
});
|
||||
|
||||
export interface Status {
|
||||
// operatorStates is a map of operator ID to operator state evaluations.
|
||||
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
|
||||
operatorStates?: Record<string, OperatorState>;
|
||||
// additionalFields is reserved for future use
|
||||
additionalFields?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const defaultStatus = (): Status => ({
|
||||
});
|
||||
|
||||
|
|
@ -1337,6 +1337,11 @@
|
|||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/api/onCallApi.test.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/components/AnnotationDetailsField.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
|
|
@ -1377,6 +1382,11 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/components/import-to-gma/ConfirmConvertModal.test.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/components/import-to-gma/NamespaceAndGroupFilter.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 2
|
||||
|
|
@ -1617,11 +1627,31 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/mocks/server/configure.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/mocks/server/handlers/plugins.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/rule-editor/clone.utils.test.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/rule-editor/formDefaults.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/rule-list/hooks/grafanaFilter.test.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/types/alerting.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 5
|
||||
|
|
@ -1632,6 +1662,16 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/utils/config.test.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/utils/config.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/utils/datasource.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 2
|
||||
|
|
@ -1663,12 +1703,20 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/utils/rules.test.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/utils/rules.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 3
|
||||
},
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx": {
|
||||
|
|
@ -1724,6 +1772,16 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.test.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/connections/tabs/ConnectData/ConnectData.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
|
|
@ -2063,6 +2121,11 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard/components/GenAI/utils.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard/components/HelpWizard/HelpWizard.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 3
|
||||
|
|
@ -2889,6 +2952,71 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.test.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/usePluginComponent.test.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/usePluginComponents.test.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/usePluginFunctions.test.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/usePluginLinks.test.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/utils.test.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 27
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/utils.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 7
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/validators.test.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 30
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/extensions/validators.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/sandbox/codeLoader.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/sandbox/distortions.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
|
|
|
|||
|
|
@ -117,6 +117,8 @@ module.exports = [
|
|||
'scripts/grafana-server/tmp',
|
||||
'packages/grafana-ui/src/graveyard', // deprecated UI components slated for removal
|
||||
'public/build-swagger', // swagger build output
|
||||
'apps/plugins/plugin/src/generated/meta/v0alpha1',
|
||||
'apps/plugins/plugin/src/generated/plugin/v0alpha1',
|
||||
],
|
||||
},
|
||||
...grafanaConfig,
|
||||
|
|
@ -575,6 +577,42 @@ module.exports = [
|
|||
"Property[key.name='a11y'][value.type='ObjectExpression'] Property[key.name='test'][value.value='off']",
|
||||
message: 'Skipping a11y tests is not allowed. Please fix the component or story instead.',
|
||||
},
|
||||
{
|
||||
selector: 'MemberExpression[object.name="config"][property.name="apps"]',
|
||||
message:
|
||||
'Usage of config.apps is not allowed. Use the function getAppPluginMetas or useAppPluginMetas from @grafana/runtime instead',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [...commonTestIgnores],
|
||||
ignores: [
|
||||
// FIXME: Remove once all enterprise issues are fixed -
|
||||
// we don't have a suppressions file/approach for enterprise code yet
|
||||
...enterpriseIgnores,
|
||||
],
|
||||
rules: {
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: 'MemberExpression[object.name="config"][property.name="apps"]',
|
||||
message:
|
||||
'Usage of config.apps is not allowed. Use the function getAppPluginMetas or useAppPluginMetas from @grafana/runtime instead',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [...enterpriseIgnores],
|
||||
rules: {
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: 'MemberExpression[object.name="config"][property.name="apps"]',
|
||||
message:
|
||||
'Usage of config.apps is not allowed. Use the function getAppPluginMetas or useAppPluginMetas from @grafana/runtime instead',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export type AppPluginConfig = {
|
|||
path: string;
|
||||
version: string;
|
||||
preload: boolean;
|
||||
/** @deprecated it will be removed in a future release */
|
||||
angular: AngularMeta;
|
||||
loadingStrategy: PluginLoadingStrategy;
|
||||
dependencies: PluginDependencies;
|
||||
|
|
@ -219,6 +220,7 @@ export interface GrafanaConfig {
|
|||
snapshotEnabled: boolean;
|
||||
datasources: { [str: string]: DataSourceInstanceSettings };
|
||||
panels: { [key: string]: PanelPluginMeta };
|
||||
/** @deprecated it will be removed in a future release */
|
||||
apps: Record<string, AppPluginConfig>;
|
||||
auth: AuthSettings;
|
||||
minRefreshInterval: string;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export interface PluginError {
|
|||
pluginType?: PluginType;
|
||||
}
|
||||
|
||||
/** @deprecated it will be removed in a future release */
|
||||
export interface AngularMeta {
|
||||
detected: boolean;
|
||||
hideDeprecation: boolean;
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ export class GrafanaBootConfig {
|
|||
snapshotEnabled = true;
|
||||
datasources: { [str: string]: DataSourceInstanceSettings } = {};
|
||||
panels: { [key: string]: PanelPluginMeta } = {};
|
||||
/** @deprecated it will be removed in a future release, use isAppPluginInstalled or getAppPluginVersion instead */
|
||||
apps: Record<string, AppPluginConfigGrafanaData> = {};
|
||||
auth: AuthSettings = {};
|
||||
minRefreshInterval = '';
|
||||
|
|
|
|||
|
|
@ -77,3 +77,5 @@ export {
|
|||
getCorrelationsService,
|
||||
setCorrelationsService,
|
||||
} from './services/CorrelationsService';
|
||||
export { getAppPluginVersion, isAppPluginInstalled } from './services/pluginMeta/apps';
|
||||
export { useAppPluginInstalled, useAppPluginVersion } from './services/pluginMeta/hooks';
|
||||
|
|
|
|||
|
|
@ -29,3 +29,5 @@ export {
|
|||
export { UserStorage } from '../utils/userStorage';
|
||||
|
||||
export { initOpenFeature, evaluateBooleanFlag } from './openFeature';
|
||||
export { getAppPluginMeta, getAppPluginMetas, setAppPluginMetas } from '../services/pluginMeta/apps';
|
||||
export { useAppPluginMeta, useAppPluginMetas } from '../services/pluginMeta/hooks';
|
||||
|
|
|
|||
258
packages/grafana-runtime/src/services/pluginMeta/apps.test.ts
Normal file
258
packages/grafana-runtime/src/services/pluginMeta/apps.test.ts
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
import { evaluateBooleanFlag } from '../../internal/openFeature';
|
||||
|
||||
import {
|
||||
getAppPluginMeta,
|
||||
getAppPluginMetas,
|
||||
getAppPluginVersion,
|
||||
isAppPluginInstalled,
|
||||
setAppPluginMetas,
|
||||
} from './apps';
|
||||
import { initPluginMetas } from './plugins';
|
||||
import { app } from './test-fixtures/config.apps';
|
||||
|
||||
jest.mock('./plugins', () => ({ ...jest.requireActual('./plugins'), initPluginMetas: jest.fn() }));
|
||||
jest.mock('../../internal/openFeature', () => ({
|
||||
...jest.requireActual('../../internal/openFeature'),
|
||||
evaluateBooleanFlag: jest.fn(),
|
||||
}));
|
||||
|
||||
const initPluginMetasMock = jest.mocked(initPluginMetas);
|
||||
const evaluateBooleanFlagMock = jest.mocked(evaluateBooleanFlag);
|
||||
|
||||
describe('when useMTPlugins flag is enabled and apps is not initialized', () => {
|
||||
beforeEach(() => {
|
||||
setAppPluginMetas({});
|
||||
jest.resetAllMocks();
|
||||
initPluginMetasMock.mockResolvedValue({ items: [] });
|
||||
evaluateBooleanFlagMock.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('getAppPluginMetas should call initPluginMetas and return correct result', async () => {
|
||||
const apps = await getAppPluginMetas();
|
||||
|
||||
expect(apps).toEqual([]);
|
||||
expect(initPluginMetasMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('getAppPluginMeta should call initPluginMetas and return correct result', async () => {
|
||||
const result = await getAppPluginMeta('myorg-someplugin-app');
|
||||
|
||||
expect(result).toEqual(null);
|
||||
expect(initPluginMetasMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('isAppPluginInstalled should call initPluginMetas and return false', async () => {
|
||||
const installed = await isAppPluginInstalled('myorg-someplugin-app');
|
||||
|
||||
expect(installed).toEqual(false);
|
||||
expect(initPluginMetasMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('getAppPluginVersion should call initPluginMetas and return null', async () => {
|
||||
const result = await getAppPluginVersion('myorg-someplugin-app');
|
||||
|
||||
expect(result).toEqual(null);
|
||||
expect(initPluginMetasMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when useMTPlugins flag is enabled and apps is initialized', () => {
|
||||
beforeEach(() => {
|
||||
setAppPluginMetas({ 'myorg-someplugin-app': app });
|
||||
jest.resetAllMocks();
|
||||
evaluateBooleanFlagMock.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('getAppPluginMetas should not call initPluginMetas and return correct result', async () => {
|
||||
const apps = await getAppPluginMetas();
|
||||
|
||||
expect(apps).toEqual([app]);
|
||||
expect(initPluginMetasMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('getAppPluginMeta should not call initPluginMetas and return correct result', async () => {
|
||||
const result = await getAppPluginMeta('myorg-someplugin-app');
|
||||
|
||||
expect(result).toEqual(app);
|
||||
expect(initPluginMetasMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('getAppPluginMeta should return null if the pluginId is not found', async () => {
|
||||
const result = await getAppPluginMeta('otherorg-otherplugin-app');
|
||||
|
||||
expect(result).toEqual(null);
|
||||
});
|
||||
|
||||
it('isAppPluginInstalled should not call initPluginMetas and return true', async () => {
|
||||
const installed = await isAppPluginInstalled('myorg-someplugin-app');
|
||||
|
||||
expect(installed).toEqual(true);
|
||||
expect(initPluginMetasMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('isAppPluginInstalled should return false if the pluginId is not found', async () => {
|
||||
const result = await isAppPluginInstalled('otherorg-otherplugin-app');
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it('getAppPluginVersion should not call initPluginMetas and return correct result', async () => {
|
||||
const result = await getAppPluginVersion('myorg-someplugin-app');
|
||||
|
||||
expect(result).toEqual('1.0.0');
|
||||
expect(initPluginMetasMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('getAppPluginVersion should return null if the pluginId is not found', async () => {
|
||||
const result = await getAppPluginVersion('otherorg-otherplugin-app');
|
||||
|
||||
expect(result).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when useMTPlugins flag is disabled and apps is not initialized', () => {
|
||||
beforeEach(() => {
|
||||
setAppPluginMetas({});
|
||||
jest.resetAllMocks();
|
||||
evaluateBooleanFlagMock.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('getAppPluginMetas should not call initPluginMetas and return correct result', async () => {
|
||||
const apps = await getAppPluginMetas();
|
||||
|
||||
expect(apps).toEqual([]);
|
||||
expect(initPluginMetasMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('getAppPluginMeta should not call initPluginMetas and return correct result', async () => {
|
||||
const result = await getAppPluginMeta('myorg-someplugin-app');
|
||||
|
||||
expect(result).toEqual(null);
|
||||
expect(initPluginMetasMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('isAppPluginInstalled should not call initPluginMetas and return false', async () => {
|
||||
const result = await isAppPluginInstalled('myorg-someplugin-app');
|
||||
|
||||
expect(result).toEqual(false);
|
||||
expect(initPluginMetasMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('getAppPluginVersion should not call initPluginMetas and return correct result', async () => {
|
||||
const result = await getAppPluginVersion('myorg-someplugin-app');
|
||||
|
||||
expect(result).toEqual(null);
|
||||
expect(initPluginMetasMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when useMTPlugins flag is disabled and apps is initialized', () => {
|
||||
beforeEach(() => {
|
||||
setAppPluginMetas({ 'myorg-someplugin-app': app });
|
||||
jest.resetAllMocks();
|
||||
evaluateBooleanFlagMock.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('getAppPluginMetas should not call initPluginMetas and return correct result', async () => {
|
||||
const apps = await getAppPluginMetas();
|
||||
|
||||
expect(apps).toEqual([app]);
|
||||
expect(initPluginMetasMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('getAppPluginMeta should not call initPluginMetas and return correct result', async () => {
|
||||
const result = await getAppPluginMeta('myorg-someplugin-app');
|
||||
|
||||
expect(result).toEqual(app);
|
||||
expect(initPluginMetasMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('getAppPluginMeta should return null if the pluginId is not found', async () => {
|
||||
const result = await getAppPluginMeta('otherorg-otherplugin-app');
|
||||
|
||||
expect(result).toEqual(null);
|
||||
});
|
||||
|
||||
it('isAppPluginInstalled should not call initPluginMetas and return true', async () => {
|
||||
const result = await isAppPluginInstalled('myorg-someplugin-app');
|
||||
|
||||
expect(result).toEqual(true);
|
||||
expect(initPluginMetasMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('isAppPluginInstalled should return false if the pluginId is not found', async () => {
|
||||
const result = await isAppPluginInstalled('otherorg-otherplugin-app');
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it('getAppPluginVersion should not call initPluginMetas and return correct result', async () => {
|
||||
const result = await getAppPluginVersion('myorg-someplugin-app');
|
||||
|
||||
expect(result).toEqual('1.0.0');
|
||||
expect(initPluginMetasMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('getAppPluginVersion should return null if the pluginId is not found', async () => {
|
||||
const result = await getAppPluginVersion('otherorg-otherplugin-app');
|
||||
|
||||
expect(result).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('immutability', () => {
|
||||
beforeEach(() => {
|
||||
setAppPluginMetas({ 'myorg-someplugin-app': app });
|
||||
jest.resetAllMocks();
|
||||
evaluateBooleanFlagMock.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('getAppPluginMetas should return a deep clone', async () => {
|
||||
const mutatedApps = await getAppPluginMetas();
|
||||
|
||||
// assert we have correct props
|
||||
expect(mutatedApps).toHaveLength(1);
|
||||
expect(mutatedApps[0].dependencies.grafanaDependency).toEqual('>=10.4.0');
|
||||
expect(mutatedApps[0].extensions.addedLinks).toHaveLength(0);
|
||||
|
||||
// mutate deep props
|
||||
mutatedApps[0].dependencies.grafanaDependency = '';
|
||||
mutatedApps[0].extensions.addedLinks.push({ targets: [], title: '', description: '' });
|
||||
|
||||
// assert we have mutated props
|
||||
expect(mutatedApps[0].dependencies.grafanaDependency).toEqual('');
|
||||
expect(mutatedApps[0].extensions.addedLinks).toHaveLength(1);
|
||||
expect(mutatedApps[0].extensions.addedLinks[0]).toEqual({ targets: [], title: '', description: '' });
|
||||
|
||||
const apps = await getAppPluginMetas();
|
||||
|
||||
// assert that we have not mutated the source
|
||||
expect(apps[0].dependencies.grafanaDependency).toEqual('>=10.4.0');
|
||||
expect(apps[0].extensions.addedLinks).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('getAppPluginMeta should return a deep clone', async () => {
|
||||
const mutatedApp = await getAppPluginMeta('myorg-someplugin-app');
|
||||
|
||||
// assert we have correct props
|
||||
expect(mutatedApp).toBeDefined();
|
||||
expect(mutatedApp!.dependencies.grafanaDependency).toEqual('>=10.4.0');
|
||||
expect(mutatedApp!.extensions.addedLinks).toHaveLength(0);
|
||||
|
||||
// mutate deep props
|
||||
mutatedApp!.dependencies.grafanaDependency = '';
|
||||
mutatedApp!.extensions.addedLinks.push({ targets: [], title: '', description: '' });
|
||||
|
||||
// assert we have mutated props
|
||||
expect(mutatedApp!.dependencies.grafanaDependency).toEqual('');
|
||||
expect(mutatedApp!.extensions.addedLinks).toHaveLength(1);
|
||||
expect(mutatedApp!.extensions.addedLinks[0]).toEqual({ targets: [], title: '', description: '' });
|
||||
|
||||
const result = await getAppPluginMeta('myorg-someplugin-app');
|
||||
|
||||
// assert that we have not mutated the source
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.dependencies.grafanaDependency).toEqual('>=10.4.0');
|
||||
expect(result!.extensions.addedLinks).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
71
packages/grafana-runtime/src/services/pluginMeta/apps.ts
Normal file
71
packages/grafana-runtime/src/services/pluginMeta/apps.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import type { AppPluginConfig } from '@grafana/data';
|
||||
|
||||
import { config } from '../../config';
|
||||
import { evaluateBooleanFlag } from '../../internal/openFeature';
|
||||
|
||||
import { getAppPluginMapper } from './mappers/mappers';
|
||||
import { initPluginMetas } from './plugins';
|
||||
import type { AppPluginMetas } from './types';
|
||||
|
||||
let apps: AppPluginMetas = {};
|
||||
|
||||
function initialized(): boolean {
|
||||
return Boolean(Object.keys(apps).length);
|
||||
}
|
||||
|
||||
async function initAppPluginMetas(): Promise<void> {
|
||||
if (!evaluateBooleanFlag('useMTPlugins', false)) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
apps = config.apps;
|
||||
return;
|
||||
}
|
||||
|
||||
const metas = await initPluginMetas();
|
||||
const mapper = getAppPluginMapper();
|
||||
apps = mapper(metas);
|
||||
}
|
||||
|
||||
export async function getAppPluginMetas(): Promise<AppPluginConfig[]> {
|
||||
if (!initialized()) {
|
||||
await initAppPluginMetas();
|
||||
}
|
||||
|
||||
return Object.values(structuredClone(apps));
|
||||
}
|
||||
|
||||
export async function getAppPluginMeta(pluginId: string): Promise<AppPluginConfig | null> {
|
||||
if (!initialized()) {
|
||||
await initAppPluginMetas();
|
||||
}
|
||||
|
||||
const app = apps[pluginId];
|
||||
return app ? structuredClone(app) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an app plugin is installed. The function does not check if the app plugin is enabled.
|
||||
* @param pluginId - The id of the app plugin.
|
||||
* @returns True if the app plugin is installed, false otherwise.
|
||||
*/
|
||||
export async function isAppPluginInstalled(pluginId: string): Promise<boolean> {
|
||||
const app = await getAppPluginMeta(pluginId);
|
||||
return Boolean(app);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of an app plugin.
|
||||
* @param pluginId - The id of the app plugin.
|
||||
* @returns The version of the app plugin, or null if the plugin is not installed.
|
||||
*/
|
||||
export async function getAppPluginVersion(pluginId: string): Promise<string | null> {
|
||||
const app = await getAppPluginMeta(pluginId);
|
||||
return app?.version ?? null;
|
||||
}
|
||||
|
||||
export function setAppPluginMetas(override: AppPluginMetas): void {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
throw new Error('setAppPluginMetas() function can only be called from tests.');
|
||||
}
|
||||
|
||||
apps = structuredClone(override);
|
||||
}
|
||||
214
packages/grafana-runtime/src/services/pluginMeta/hooks.test.tsx
Normal file
214
packages/grafana-runtime/src/services/pluginMeta/hooks.test.tsx
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
|
||||
import {
|
||||
getAppPluginMeta,
|
||||
getAppPluginMetas,
|
||||
getAppPluginVersion,
|
||||
isAppPluginInstalled,
|
||||
setAppPluginMetas,
|
||||
} from './apps';
|
||||
import { useAppPluginMeta, useAppPluginMetas, useAppPluginInstalled, useAppPluginVersion } from './hooks';
|
||||
import { apps } from './test-fixtures/config.apps';
|
||||
|
||||
const actualApps = jest.requireActual<typeof import('./apps')>('./apps');
|
||||
jest.mock('./apps', () => ({
|
||||
...jest.requireActual('./apps'),
|
||||
getAppPluginMetas: jest.fn(),
|
||||
getAppPluginMeta: jest.fn(),
|
||||
isAppPluginInstalled: jest.fn(),
|
||||
getAppPluginVersion: jest.fn(),
|
||||
}));
|
||||
const getAppPluginMetaMock = jest.mocked(getAppPluginMeta);
|
||||
const getAppPluginMetasMock = jest.mocked(getAppPluginMetas);
|
||||
const isAppPluginInstalledMock = jest.mocked(isAppPluginInstalled);
|
||||
const getAppPluginVersionMock = jest.mocked(getAppPluginVersion);
|
||||
|
||||
describe('useAppPluginMeta', () => {
|
||||
beforeEach(() => {
|
||||
setAppPluginMetas(apps);
|
||||
jest.resetAllMocks();
|
||||
getAppPluginMetaMock.mockImplementation(actualApps.getAppPluginMeta);
|
||||
});
|
||||
|
||||
it('should return correct default values', async () => {
|
||||
const { result } = renderHook(() => useAppPluginMeta('grafana-exploretraces-app'));
|
||||
|
||||
expect(result.current.loading).toEqual(true);
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.value).toBeUndefined();
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toEqual(true));
|
||||
});
|
||||
|
||||
it('should return correct values after loading', async () => {
|
||||
const { result } = renderHook(() => useAppPluginMeta('grafana-exploretraces-app'));
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toEqual(false));
|
||||
|
||||
expect(result.current.loading).toEqual(false);
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.value).toEqual(apps['grafana-exploretraces-app']);
|
||||
});
|
||||
|
||||
it('should return correct values if the pluginId does not exist', async () => {
|
||||
const { result } = renderHook(() => useAppPluginMeta('otherorg-otherplugin-app'));
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toEqual(false));
|
||||
|
||||
expect(result.current.loading).toEqual(false);
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.value).toEqual(null);
|
||||
});
|
||||
|
||||
it('should return correct values if useAppPluginMeta throws', async () => {
|
||||
getAppPluginMetaMock.mockRejectedValue(new Error('Some error'));
|
||||
|
||||
const { result } = renderHook(() => useAppPluginMeta('otherorg-otherplugin-app'));
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toEqual(false));
|
||||
|
||||
expect(result.current.loading).toEqual(false);
|
||||
expect(result.current.error).toEqual(new Error('Some error'));
|
||||
expect(result.current.value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useAppPluginMetas', () => {
|
||||
beforeEach(() => {
|
||||
setAppPluginMetas(apps);
|
||||
jest.resetAllMocks();
|
||||
getAppPluginMetasMock.mockImplementation(actualApps.getAppPluginMetas);
|
||||
});
|
||||
|
||||
it('should return correct default values', async () => {
|
||||
const { result } = renderHook(() => useAppPluginMetas());
|
||||
|
||||
expect(result.current.loading).toEqual(true);
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.value).toBeUndefined();
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toEqual(true));
|
||||
});
|
||||
|
||||
it('should return correct values after loading', async () => {
|
||||
const { result } = renderHook(() => useAppPluginMetas());
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toEqual(false));
|
||||
|
||||
expect(result.current.loading).toEqual(false);
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.value).toEqual(Object.values(apps));
|
||||
});
|
||||
|
||||
it('should return correct values if useAppPluginMetas throws', async () => {
|
||||
getAppPluginMetasMock.mockRejectedValue(new Error('Some error'));
|
||||
|
||||
const { result } = renderHook(() => useAppPluginMetas());
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toEqual(false));
|
||||
|
||||
expect(result.current.loading).toEqual(false);
|
||||
expect(result.current.error).toEqual(new Error('Some error'));
|
||||
expect(result.current.value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useAppPluginInstalled', () => {
|
||||
beforeEach(() => {
|
||||
setAppPluginMetas(apps);
|
||||
jest.resetAllMocks();
|
||||
isAppPluginInstalledMock.mockImplementation(actualApps.isAppPluginInstalled);
|
||||
});
|
||||
|
||||
it('should return correct default values', async () => {
|
||||
const { result } = renderHook(() => useAppPluginInstalled('grafana-exploretraces-app'));
|
||||
|
||||
expect(result.current.loading).toEqual(true);
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.value).toBeUndefined();
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toEqual(true));
|
||||
});
|
||||
|
||||
it('should return correct values after loading', async () => {
|
||||
const { result } = renderHook(() => useAppPluginInstalled('grafana-exploretraces-app'));
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toEqual(false));
|
||||
|
||||
expect(result.current.loading).toEqual(false);
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.value).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return correct values if the pluginId does not exist', async () => {
|
||||
const { result } = renderHook(() => useAppPluginInstalled('otherorg-otherplugin-app'));
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toEqual(false));
|
||||
|
||||
expect(result.current.loading).toEqual(false);
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.value).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return correct values if isAppPluginInstalled throws', async () => {
|
||||
isAppPluginInstalledMock.mockRejectedValue(new Error('Some error'));
|
||||
|
||||
const { result } = renderHook(() => useAppPluginInstalled('otherorg-otherplugin-app'));
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toEqual(false));
|
||||
|
||||
expect(result.current.loading).toEqual(false);
|
||||
expect(result.current.error).toEqual(new Error('Some error'));
|
||||
expect(result.current.value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useAppPluginVersion', () => {
|
||||
beforeEach(() => {
|
||||
setAppPluginMetas(apps);
|
||||
jest.resetAllMocks();
|
||||
getAppPluginVersionMock.mockImplementation(actualApps.getAppPluginVersion);
|
||||
});
|
||||
|
||||
it('should return correct default values', async () => {
|
||||
const { result } = renderHook(() => useAppPluginVersion('grafana-exploretraces-app'));
|
||||
|
||||
expect(result.current.loading).toEqual(true);
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.value).toBeUndefined();
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toEqual(true));
|
||||
});
|
||||
|
||||
it('should return correct values after loading', async () => {
|
||||
const { result } = renderHook(() => useAppPluginVersion('grafana-exploretraces-app'));
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toEqual(false));
|
||||
|
||||
expect(result.current.loading).toEqual(false);
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.value).toEqual('1.2.2');
|
||||
});
|
||||
|
||||
it('should return correct values if the pluginId does not exist', async () => {
|
||||
const { result } = renderHook(() => useAppPluginVersion('otherorg-otherplugin-app'));
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toEqual(false));
|
||||
|
||||
expect(result.current.loading).toEqual(false);
|
||||
expect(result.current.error).toBeUndefined();
|
||||
expect(result.current.value).toEqual(null);
|
||||
});
|
||||
|
||||
it('should return correct values if getAppPluginVersion throws', async () => {
|
||||
getAppPluginVersionMock.mockRejectedValue(new Error('Some error'));
|
||||
|
||||
const { result } = renderHook(() => useAppPluginVersion('otherorg-otherplugin-app'));
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toEqual(false));
|
||||
|
||||
expect(result.current.loading).toEqual(false);
|
||||
expect(result.current.error).toEqual(new Error('Some error'));
|
||||
expect(result.current.value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
35
packages/grafana-runtime/src/services/pluginMeta/hooks.tsx
Normal file
35
packages/grafana-runtime/src/services/pluginMeta/hooks.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { useAsync } from 'react-use';
|
||||
|
||||
import { getAppPluginMeta, getAppPluginMetas, getAppPluginVersion, isAppPluginInstalled } from './apps';
|
||||
|
||||
export function useAppPluginMetas() {
|
||||
const { loading, error, value } = useAsync(async () => getAppPluginMetas());
|
||||
return { loading, error, value };
|
||||
}
|
||||
|
||||
export function useAppPluginMeta(pluginId: string) {
|
||||
const { loading, error, value } = useAsync(async () => getAppPluginMeta(pluginId));
|
||||
return { loading, error, value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that checks if an app plugin is installed. The hook does not check if the app plugin is enabled.
|
||||
* @param pluginId - The ID of the app plugin.
|
||||
* @returns loading, error, value of the app plugin installed status.
|
||||
* The value is true if the app plugin is installed, false otherwise.
|
||||
*/
|
||||
export function useAppPluginInstalled(pluginId: string) {
|
||||
const { loading, error, value } = useAsync(async () => isAppPluginInstalled(pluginId));
|
||||
return { loading, error, value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that gets the version of an app plugin.
|
||||
* @param pluginId - The ID of the app plugin.
|
||||
* @returns loading, error, value of the app plugin version.
|
||||
* The value is the version of the app plugin, or null if the plugin is not installed.
|
||||
*/
|
||||
export function useAppPluginVersion(pluginId: string) {
|
||||
const { loading, error, value } = useAsync(async () => getAppPluginVersion(pluginId));
|
||||
return { loading, error, value };
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { AppPluginMetasMapper, PluginMetasResponse } from '../types';
|
||||
|
||||
import { v0alpha1AppMapper } from './v0alpha1AppMapper';
|
||||
|
||||
export function getAppPluginMapper(): AppPluginMetasMapper<PluginMetasResponse> {
|
||||
return v0alpha1AppMapper;
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import { apps } from '../test-fixtures/config.apps';
|
||||
import { v0alpha1Response } from '../test-fixtures/v0alpha1Response';
|
||||
|
||||
import { v0alpha1AppMapper } from './v0alpha1AppMapper';
|
||||
|
||||
const PLUGIN_IDS = v0alpha1Response.items
|
||||
.filter((i) => i.spec.pluginJson.type === 'app')
|
||||
.map((i) => ({ pluginId: i.spec.pluginJson.id }));
|
||||
|
||||
describe('v0alpha1AppMapper', () => {
|
||||
describe.each(PLUGIN_IDS)('when called for pluginId:$pluginId', ({ pluginId }) => {
|
||||
it('should map id property correctly', () => {
|
||||
const result = v0alpha1AppMapper(v0alpha1Response);
|
||||
|
||||
expect(result[pluginId].id).toEqual(apps[pluginId].id);
|
||||
});
|
||||
|
||||
it('should map path property correctly', () => {
|
||||
const result = v0alpha1AppMapper(v0alpha1Response);
|
||||
|
||||
expect(result[pluginId].path).toEqual(apps[pluginId].path);
|
||||
});
|
||||
|
||||
it('should map version property correctly', () => {
|
||||
const result = v0alpha1AppMapper(v0alpha1Response);
|
||||
|
||||
expect(result[pluginId].version).toEqual(apps[pluginId].version);
|
||||
});
|
||||
|
||||
it('should map preload property correctly', () => {
|
||||
const result = v0alpha1AppMapper(v0alpha1Response);
|
||||
|
||||
expect(result[pluginId].preload).toEqual(apps[pluginId].preload);
|
||||
});
|
||||
|
||||
it('should map angular property correctly', () => {
|
||||
const result = v0alpha1AppMapper(v0alpha1Response);
|
||||
|
||||
expect(result[pluginId].angular).toEqual({});
|
||||
});
|
||||
|
||||
it('should map loadingStrategy property correctly', () => {
|
||||
const result = v0alpha1AppMapper(v0alpha1Response);
|
||||
|
||||
expect(result[pluginId].loadingStrategy).toEqual(apps[pluginId].loadingStrategy);
|
||||
});
|
||||
|
||||
it('should map dependencies property correctly', () => {
|
||||
const result = v0alpha1AppMapper(v0alpha1Response);
|
||||
|
||||
expect(result[pluginId].dependencies).toEqual(apps[pluginId].dependencies);
|
||||
});
|
||||
|
||||
it('should map extensions property correctly', () => {
|
||||
const result = v0alpha1AppMapper(v0alpha1Response);
|
||||
|
||||
expect(result[pluginId].extensions.addedComponents).toEqual(apps[pluginId].extensions.addedComponents);
|
||||
expect(result[pluginId].extensions.addedFunctions).toEqual(apps[pluginId].extensions.addedFunctions);
|
||||
expect(result[pluginId].extensions.addedLinks).toEqual(apps[pluginId].extensions.addedLinks);
|
||||
expect(result[pluginId].extensions.exposedComponents).toEqual(apps[pluginId].extensions.exposedComponents);
|
||||
expect(result[pluginId].extensions.extensionPoints).toEqual(apps[pluginId].extensions.extensionPoints);
|
||||
});
|
||||
|
||||
it('should map moduleHash property correctly', () => {
|
||||
const result = v0alpha1AppMapper(v0alpha1Response);
|
||||
|
||||
expect(result[pluginId].moduleHash).toEqual(apps[pluginId].moduleHash);
|
||||
});
|
||||
|
||||
it('should map buildMode property correctly', () => {
|
||||
const result = v0alpha1AppMapper(v0alpha1Response);
|
||||
|
||||
expect(result[pluginId].buildMode).toEqual(apps[pluginId].buildMode);
|
||||
});
|
||||
});
|
||||
|
||||
it('should only map specs with type app', () => {
|
||||
const result = v0alpha1AppMapper(v0alpha1Response);
|
||||
|
||||
expect(v0alpha1Response.items).toHaveLength(58);
|
||||
expect(Object.keys(result)).toHaveLength(5);
|
||||
expect(Object.keys(result)).toEqual(Object.keys(apps));
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
import {
|
||||
type AngularMeta,
|
||||
type AppPluginConfig,
|
||||
type PluginDependencies,
|
||||
type PluginExtensions,
|
||||
PluginLoadingStrategy,
|
||||
type PluginType,
|
||||
} from '@grafana/data';
|
||||
|
||||
import type { AppPluginMetas, AppPluginMetasMapper, PluginMetasResponse } from '../types';
|
||||
import type { Spec as v0alpha1Spec } from '../types/types.spec.gen';
|
||||
|
||||
function angularyMapper(spec: v0alpha1Spec): AngularMeta {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return {} as AngularMeta;
|
||||
}
|
||||
|
||||
function dependenciesMapper(spec: v0alpha1Spec): PluginDependencies {
|
||||
const plugins = (spec.pluginJson.dependencies?.plugins ?? []).map((v) => ({
|
||||
...v,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
type: v.type as PluginType,
|
||||
version: '',
|
||||
}));
|
||||
|
||||
const dependencies: PluginDependencies = {
|
||||
...spec.pluginJson.dependencies,
|
||||
extensions: {
|
||||
exposedComponents: spec.pluginJson.dependencies.extensions?.exposedComponents ?? [],
|
||||
},
|
||||
grafanaDependency: spec.pluginJson.dependencies.grafanaDependency,
|
||||
grafanaVersion: spec.pluginJson.dependencies.grafanaVersion ?? '',
|
||||
plugins,
|
||||
};
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
function extensionsMapper(spec: v0alpha1Spec): PluginExtensions {
|
||||
const addedComponents = spec.pluginJson.extensions?.addedComponents ?? [];
|
||||
const addedFunctions = spec.pluginJson.extensions?.addedFunctions ?? [];
|
||||
const addedLinks = spec.pluginJson.extensions?.addedLinks ?? [];
|
||||
const exposedComponents = (spec.pluginJson.extensions?.exposedComponents ?? []).map((v) => ({
|
||||
...v,
|
||||
description: v.description ?? '',
|
||||
title: v.title ?? '',
|
||||
}));
|
||||
const extensionPoints = (spec.pluginJson.extensions?.extensionPoints ?? []).map((v) => ({
|
||||
...v,
|
||||
description: v.description ?? '',
|
||||
title: v.title ?? '',
|
||||
}));
|
||||
|
||||
const extensions: PluginExtensions = {
|
||||
addedComponents,
|
||||
addedFunctions,
|
||||
addedLinks,
|
||||
exposedComponents,
|
||||
extensionPoints,
|
||||
};
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
function loadingStrategyMapper(spec: v0alpha1Spec): PluginLoadingStrategy {
|
||||
const loadingStrategy = spec.module?.loadingStrategy ?? PluginLoadingStrategy.fetch;
|
||||
if (loadingStrategy === PluginLoadingStrategy.script) {
|
||||
return PluginLoadingStrategy.script;
|
||||
}
|
||||
|
||||
return PluginLoadingStrategy.fetch;
|
||||
}
|
||||
|
||||
function specMapper(spec: v0alpha1Spec): AppPluginConfig {
|
||||
const { id, info, preload = false } = spec.pluginJson;
|
||||
const angular = angularyMapper(spec);
|
||||
const dependencies = dependenciesMapper(spec);
|
||||
const extensions = extensionsMapper(spec);
|
||||
const loadingStrategy = loadingStrategyMapper(spec);
|
||||
const path = spec.module?.path ?? '';
|
||||
const version = info.version;
|
||||
const buildMode = spec.pluginJson.buildMode ?? 'production';
|
||||
const moduleHash = spec.module?.hash;
|
||||
|
||||
return {
|
||||
id,
|
||||
angular,
|
||||
dependencies,
|
||||
extensions,
|
||||
loadingStrategy,
|
||||
path,
|
||||
preload,
|
||||
version,
|
||||
buildMode,
|
||||
moduleHash,
|
||||
};
|
||||
}
|
||||
|
||||
export const v0alpha1AppMapper: AppPluginMetasMapper<PluginMetasResponse> = (response) => {
|
||||
const result: AppPluginMetas = {};
|
||||
|
||||
return response.items.reduce((acc, curr) => {
|
||||
if (curr.spec.pluginJson.type !== 'app') {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const config = specMapper(curr.spec);
|
||||
acc[config.id] = config;
|
||||
return acc;
|
||||
}, result);
|
||||
};
|
||||
153
packages/grafana-runtime/src/services/pluginMeta/plugins.test.ts
Normal file
153
packages/grafana-runtime/src/services/pluginMeta/plugins.test.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import { evaluateBooleanFlag } from '../../internal/openFeature';
|
||||
|
||||
import { clearCache, initPluginMetas } from './plugins';
|
||||
import { v0alpha1Meta } from './test-fixtures/v0alpha1Response';
|
||||
|
||||
jest.mock('../../internal/openFeature', () => ({
|
||||
...jest.requireActual('../../internal/openFeature'),
|
||||
evaluateBooleanFlag: jest.fn(),
|
||||
}));
|
||||
|
||||
const evaluateBooleanFlagMock = jest.mocked(evaluateBooleanFlag);
|
||||
|
||||
describe('when useMTPlugins toggle is enabled and cache is not initialized', () => {
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
clearCache();
|
||||
evaluateBooleanFlagMock.mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it('initPluginMetas should call loadPluginMetas and return correct result if response is ok', async () => {
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve({ items: [v0alpha1Meta] }),
|
||||
});
|
||||
|
||||
const response = await initPluginMetas();
|
||||
|
||||
expect(response.items).toHaveLength(1);
|
||||
expect(response.items[0]).toEqual(v0alpha1Meta);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(global.fetch).toHaveBeenCalledWith('/apis/plugins.grafana.app/v0alpha1/namespaces/default/metas');
|
||||
});
|
||||
|
||||
it('initPluginMetas should call loadPluginMetas and return correct result if response is not ok', async () => {
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not found',
|
||||
});
|
||||
|
||||
await expect(initPluginMetas()).rejects.toThrow(new Error(`Failed to load plugin metas 404:Not found`));
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(global.fetch).toHaveBeenCalledWith('/apis/plugins.grafana.app/v0alpha1/namespaces/default/metas');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when useMTPlugins toggle is enabled and cache is initialized', () => {
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
clearCache();
|
||||
evaluateBooleanFlagMock.mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it('initPluginMetas should return cache', async () => {
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve({ items: [v0alpha1Meta] }),
|
||||
});
|
||||
|
||||
const original = await initPluginMetas();
|
||||
const cached = await initPluginMetas();
|
||||
|
||||
expect(original).toEqual(cached);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('initPluginMetas should return inflight promise', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve({ items: [v0alpha1Meta] }),
|
||||
});
|
||||
|
||||
const original = initPluginMetas();
|
||||
const cached = initPluginMetas();
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
expect(original).toEqual(cached);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when useMTPlugins toggle is disabled and cache is not initialized', () => {
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
clearCache();
|
||||
global.fetch = jest.fn();
|
||||
evaluateBooleanFlagMock.mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it('initPluginMetas should call loadPluginMetas and return correct result if response is ok', async () => {
|
||||
const response = await initPluginMetas();
|
||||
|
||||
expect(response.items).toHaveLength(0);
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when useMTPlugins toggle is disabled and cache is initialized', () => {
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
clearCache();
|
||||
global.fetch = jest.fn();
|
||||
evaluateBooleanFlagMock.mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it('initPluginMetas should return cache', async () => {
|
||||
const original = await initPluginMetas();
|
||||
const cached = await initPluginMetas();
|
||||
|
||||
expect(original).toEqual(cached);
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('initPluginMetas should return inflight promise', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const original = initPluginMetas();
|
||||
const cached = initPluginMetas();
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
expect(original).toEqual(cached);
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
41
packages/grafana-runtime/src/services/pluginMeta/plugins.ts
Normal file
41
packages/grafana-runtime/src/services/pluginMeta/plugins.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { config } from '../../config';
|
||||
import { evaluateBooleanFlag } from '../../internal/openFeature';
|
||||
|
||||
import type { PluginMetasResponse } from './types';
|
||||
|
||||
let initPromise: Promise<PluginMetasResponse> | null = null;
|
||||
|
||||
function getApiVersion(): string {
|
||||
return 'v0alpha1';
|
||||
}
|
||||
|
||||
async function loadPluginMetas(): Promise<PluginMetasResponse> {
|
||||
if (!evaluateBooleanFlag('useMTPlugins', false)) {
|
||||
const result = { items: [] };
|
||||
return result;
|
||||
}
|
||||
|
||||
const metas = await fetch(`/apis/plugins.grafana.app/${getApiVersion()}/namespaces/${config.namespace}/metas`);
|
||||
if (!metas.ok) {
|
||||
throw new Error(`Failed to load plugin metas ${metas.status}:${metas.statusText}`);
|
||||
}
|
||||
|
||||
const result = await metas.json();
|
||||
return result;
|
||||
}
|
||||
|
||||
export function initPluginMetas(): Promise<PluginMetasResponse> {
|
||||
if (!initPromise) {
|
||||
initPromise = loadPluginMetas();
|
||||
}
|
||||
|
||||
return initPromise;
|
||||
}
|
||||
|
||||
export function clearCache() {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
throw new Error('clearCache() function can only be called from tests.');
|
||||
}
|
||||
|
||||
initPromise = null;
|
||||
}
|
||||
|
|
@ -0,0 +1,303 @@
|
|||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import { AngularMeta, AppPluginConfig, PluginLoadingStrategy } from '@grafana/data';
|
||||
|
||||
import { AppPluginMetas } from '../types';
|
||||
|
||||
export const app: AppPluginConfig = cloneDeep({
|
||||
id: 'myorg-someplugin-app',
|
||||
path: 'public/plugins/myorg-someplugin-app/module.js',
|
||||
version: '1.0.0',
|
||||
preload: false,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
angular: { detected: false } as AngularMeta,
|
||||
loadingStrategy: PluginLoadingStrategy.script,
|
||||
extensions: {
|
||||
addedLinks: [],
|
||||
addedComponents: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
addedFunctions: [],
|
||||
},
|
||||
dependencies: {
|
||||
grafanaDependency: '>=10.4.0',
|
||||
grafanaVersion: '*',
|
||||
plugins: [],
|
||||
extensions: {
|
||||
exposedComponents: [],
|
||||
},
|
||||
},
|
||||
buildMode: 'production',
|
||||
});
|
||||
|
||||
export const apps: AppPluginMetas = cloneDeep({
|
||||
'grafana-exploretraces-app': {
|
||||
id: 'grafana-exploretraces-app',
|
||||
path: 'public/plugins/grafana-exploretraces-app/module.js',
|
||||
version: '1.2.2',
|
||||
preload: true,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
angular: { detected: false } as AngularMeta,
|
||||
loadingStrategy: PluginLoadingStrategy.script,
|
||||
extensions: {
|
||||
addedLinks: [
|
||||
{
|
||||
targets: ['grafana/dashboard/panel/menu'],
|
||||
title: 'Open in Traces Drilldown',
|
||||
description: 'Open current query in the Traces Drilldown app',
|
||||
},
|
||||
{
|
||||
targets: ['grafana/explore/toolbar/action'],
|
||||
title: 'Open in Grafana Traces Drilldown',
|
||||
description: 'Try our new queryless experience for traces',
|
||||
},
|
||||
],
|
||||
addedComponents: [
|
||||
{
|
||||
targets: ['grafana-asserts-app/entity-assertions-widget/v1'],
|
||||
title: 'Asserts widget',
|
||||
description: 'A block with assertions for a given service',
|
||||
},
|
||||
{
|
||||
targets: ['grafana-asserts-app/insights-timeline-widget/v1'],
|
||||
title: 'Insights Timeline Widget',
|
||||
description: 'Widget for displaying insights timeline in other apps',
|
||||
},
|
||||
],
|
||||
exposedComponents: [
|
||||
{
|
||||
id: 'grafana-exploretraces-app/open-in-explore-traces-button/v1',
|
||||
title: 'Open in Traces Drilldown button',
|
||||
description: 'A button that opens a traces view in the Traces Drilldown app.',
|
||||
},
|
||||
{
|
||||
id: 'grafana-exploretraces-app/embedded-trace-exploration/v1',
|
||||
title: 'Embedded Trace Exploration',
|
||||
description:
|
||||
'A component that renders a trace exploration view that can be embedded in other parts of Grafana.',
|
||||
},
|
||||
],
|
||||
extensionPoints: [
|
||||
{
|
||||
id: 'grafana-exploretraces-app/investigation/v1',
|
||||
title: '',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
id: 'grafana-exploretraces-app/get-logs-drilldown-link/v1',
|
||||
title: '',
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
addedFunctions: [],
|
||||
},
|
||||
dependencies: {
|
||||
grafanaDependency: '>=11.5.0',
|
||||
grafanaVersion: '*',
|
||||
plugins: [],
|
||||
extensions: {
|
||||
exposedComponents: [
|
||||
'grafana-asserts-app/entity-assertions-widget/v1',
|
||||
'grafana-asserts-app/insights-timeline-widget/v1',
|
||||
],
|
||||
},
|
||||
},
|
||||
buildMode: 'production',
|
||||
},
|
||||
'grafana-lokiexplore-app': {
|
||||
id: 'grafana-lokiexplore-app',
|
||||
path: 'public/plugins/grafana-lokiexplore-app/module.js',
|
||||
version: '1.0.32',
|
||||
preload: true,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
angular: { detected: false } as AngularMeta,
|
||||
loadingStrategy: PluginLoadingStrategy.script,
|
||||
extensions: {
|
||||
addedLinks: [
|
||||
{
|
||||
targets: [
|
||||
'grafana/dashboard/panel/menu',
|
||||
'grafana/explore/toolbar/action',
|
||||
'grafana-metricsdrilldown-app/open-in-logs-drilldown/v1',
|
||||
'grafana-assistant-app/navigateToDrilldown/v1',
|
||||
],
|
||||
title: 'Open in Grafana Logs Drilldown',
|
||||
description: 'Open current query in the Grafana Logs Drilldown view',
|
||||
},
|
||||
],
|
||||
addedComponents: [
|
||||
{
|
||||
targets: ['grafana-asserts-app/insights-timeline-widget/v1'],
|
||||
title: 'Insights Timeline Widget',
|
||||
description: 'Widget for displaying insights timeline in other apps',
|
||||
},
|
||||
],
|
||||
exposedComponents: [
|
||||
{
|
||||
id: 'grafana-lokiexplore-app/open-in-explore-logs-button/v1',
|
||||
title: 'Open in Logs Drilldown button',
|
||||
description: 'A button that opens a logs view in the Logs Drilldown app.',
|
||||
},
|
||||
{
|
||||
id: 'grafana-lokiexplore-app/embedded-logs-exploration/v1',
|
||||
title: 'Embedded Logs Exploration',
|
||||
description:
|
||||
'A component that renders a logs exploration view that can be embedded in other parts of Grafana.',
|
||||
},
|
||||
],
|
||||
extensionPoints: [
|
||||
{
|
||||
id: 'grafana-lokiexplore-app/investigation/v1',
|
||||
title: '',
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
addedFunctions: [
|
||||
{
|
||||
targets: ['grafana-exploretraces-app/get-logs-drilldown-link/v1'],
|
||||
title: 'Open Logs Drilldown',
|
||||
description: 'Returns url to logs drilldown app',
|
||||
},
|
||||
],
|
||||
},
|
||||
dependencies: {
|
||||
grafanaDependency: '>=11.6.0',
|
||||
grafanaVersion: '*',
|
||||
plugins: [],
|
||||
extensions: {
|
||||
exposedComponents: [
|
||||
'grafana-adaptivelogs-app/temporary-exemptions/v1',
|
||||
'grafana-lokiexplore-app/embedded-logs-exploration/v1',
|
||||
'grafana-asserts-app/insights-timeline-widget/v1',
|
||||
'grafana/add-to-dashboard-form/v1',
|
||||
],
|
||||
},
|
||||
},
|
||||
buildMode: 'production',
|
||||
},
|
||||
'grafana-metricsdrilldown-app': {
|
||||
id: 'grafana-metricsdrilldown-app',
|
||||
path: 'public/plugins/grafana-metricsdrilldown-app/module.js',
|
||||
version: '1.0.26',
|
||||
preload: true,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
angular: { detected: false } as AngularMeta,
|
||||
loadingStrategy: PluginLoadingStrategy.script,
|
||||
extensions: {
|
||||
addedLinks: [
|
||||
{
|
||||
targets: [
|
||||
'grafana/dashboard/panel/menu',
|
||||
'grafana/explore/toolbar/action',
|
||||
'grafana-assistant-app/navigateToDrilldown/v1',
|
||||
'grafana/alerting/alertingrule/queryeditor',
|
||||
],
|
||||
title: 'Open in Grafana Metrics Drilldown',
|
||||
description: 'Open current query in the Grafana Metrics Drilldown view',
|
||||
},
|
||||
{
|
||||
targets: ['grafana-metricsdrilldown-app/grafana-assistant-app/navigateToDrilldown/v0-alpha'],
|
||||
title: 'Navigate to metrics drilldown',
|
||||
description: 'Build a url path to the metrics drilldown',
|
||||
},
|
||||
{
|
||||
targets: ['grafana/datasources/config/actions', 'grafana/datasources/config/status'],
|
||||
title: 'Open in Metrics Drilldown',
|
||||
description: 'Browse metrics in Grafana Metrics Drilldown',
|
||||
},
|
||||
],
|
||||
addedComponents: [],
|
||||
exposedComponents: [
|
||||
{
|
||||
id: 'grafana-metricsdrilldown-app/label-breakdown-component/v1',
|
||||
title: 'Label Breakdown',
|
||||
description: 'A metrics label breakdown view from the Metrics Drilldown app.',
|
||||
},
|
||||
{
|
||||
id: 'grafana-metricsdrilldown-app/knowledge-graph-insight-metrics/v1',
|
||||
title: 'Knowledge Graph Source Metrics',
|
||||
description: 'Explore the underlying metrics related to a Knowledge Graph insight',
|
||||
},
|
||||
],
|
||||
extensionPoints: [
|
||||
{
|
||||
id: 'grafana-exploremetrics-app/investigation/v1',
|
||||
title: '',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
id: 'grafana-metricsdrilldown-app/open-in-logs-drilldown/v1',
|
||||
title: '',
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
addedFunctions: [],
|
||||
},
|
||||
dependencies: {
|
||||
grafanaDependency: '>=11.6.0',
|
||||
grafanaVersion: '*',
|
||||
plugins: [],
|
||||
extensions: {
|
||||
exposedComponents: ['grafana/add-to-dashboard-form/v1'],
|
||||
},
|
||||
},
|
||||
buildMode: 'production',
|
||||
},
|
||||
'grafana-pyroscope-app': {
|
||||
id: 'grafana-pyroscope-app',
|
||||
path: 'public/plugins/grafana-pyroscope-app/module.js',
|
||||
version: '1.14.2',
|
||||
preload: true,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
angular: { detected: false } as AngularMeta,
|
||||
loadingStrategy: PluginLoadingStrategy.script,
|
||||
extensions: {
|
||||
addedLinks: [
|
||||
{
|
||||
targets: [
|
||||
'grafana/explore/toolbar/action',
|
||||
'grafana/traceview/details',
|
||||
'grafana-assistant-app/navigateToDrilldown/v1',
|
||||
],
|
||||
title: 'Open in Grafana Profiles Drilldown',
|
||||
description: 'Try our new queryless experience for profiles',
|
||||
},
|
||||
],
|
||||
addedComponents: [],
|
||||
exposedComponents: [
|
||||
{
|
||||
id: 'grafana-pyroscope-app/embedded-profiles-exploration/v1',
|
||||
title: 'Embedded Profiles Exploration',
|
||||
description:
|
||||
'A component that renders a profiles exploration view that can be embedded in other parts of Grafana.',
|
||||
},
|
||||
],
|
||||
extensionPoints: [
|
||||
{
|
||||
id: 'grafana-pyroscope-app/investigation/v1',
|
||||
title: '',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
id: 'grafana-pyroscope-app/settings/v1',
|
||||
title: '',
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
addedFunctions: [],
|
||||
},
|
||||
dependencies: {
|
||||
grafanaDependency: '>=11.5.0',
|
||||
grafanaVersion: '*',
|
||||
plugins: [],
|
||||
extensions: {
|
||||
exposedComponents: [
|
||||
'grafana-o11yinsights-app/insights-launcher/v1',
|
||||
'grafana-adaptiveprofiles-app/resolution-boost/v1',
|
||||
],
|
||||
},
|
||||
},
|
||||
buildMode: 'production',
|
||||
},
|
||||
[app.id]: app,
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
10
packages/grafana-runtime/src/services/pluginMeta/types.ts
Normal file
10
packages/grafana-runtime/src/services/pluginMeta/types.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import type { AppPluginConfig } from '@grafana/data';
|
||||
|
||||
import type { Meta } from './types/meta_object_gen';
|
||||
|
||||
export type AppPluginMetas = Record<string, AppPluginConfig>;
|
||||
|
||||
export type AppPluginMetasMapper<T> = (response: T) => AppPluginMetas;
|
||||
export interface PluginMetasResponse {
|
||||
items: Meta[];
|
||||
}
|
||||
49
packages/grafana-runtime/src/services/pluginMeta/types/meta_object_gen.ts
generated
Normal file
49
packages/grafana-runtime/src/services/pluginMeta/types/meta_object_gen.ts
generated
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* This file was generated by grafana-app-sdk. DO NOT EDIT.
|
||||
*/
|
||||
import { Spec } from './types.spec.gen';
|
||||
import { Status } from './types.status.gen';
|
||||
|
||||
export interface Metadata {
|
||||
name: string;
|
||||
namespace: string;
|
||||
generateName?: string;
|
||||
selfLink?: string;
|
||||
uid?: string;
|
||||
resourceVersion?: string;
|
||||
generation?: number;
|
||||
creationTimestamp?: string;
|
||||
deletionTimestamp?: string;
|
||||
deletionGracePeriodSeconds?: number;
|
||||
labels?: Record<string, string>;
|
||||
annotations?: Record<string, string>;
|
||||
ownerReferences?: OwnerReference[];
|
||||
finalizers?: string[];
|
||||
managedFields?: ManagedFieldsEntry[];
|
||||
}
|
||||
|
||||
export interface OwnerReference {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
uid: string;
|
||||
controller?: boolean;
|
||||
blockOwnerDeletion?: boolean;
|
||||
}
|
||||
|
||||
export interface ManagedFieldsEntry {
|
||||
manager?: string;
|
||||
operation?: string;
|
||||
apiVersion?: string;
|
||||
time?: string;
|
||||
fieldsType?: string;
|
||||
subresource?: string;
|
||||
}
|
||||
|
||||
export interface Meta {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
metadata: Metadata;
|
||||
spec: Spec;
|
||||
status: Status;
|
||||
}
|
||||
278
packages/grafana-runtime/src/services/pluginMeta/types/types.spec.gen.ts
generated
Normal file
278
packages/grafana-runtime/src/services/pluginMeta/types/types.spec.gen.ts
generated
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
// JSON configuration schema for Grafana plugins
|
||||
// Converted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json
|
||||
export interface JSONData {
|
||||
// Unique name of the plugin
|
||||
id: string;
|
||||
// Plugin type
|
||||
type: "app" | "datasource" | "panel" | "renderer";
|
||||
// Human-readable name of the plugin
|
||||
name: string;
|
||||
// Metadata for the plugin
|
||||
info: Info;
|
||||
// Dependency information
|
||||
dependencies: Dependencies;
|
||||
// Optional fields
|
||||
alerting?: boolean;
|
||||
annotations?: boolean;
|
||||
autoEnabled?: boolean;
|
||||
backend?: boolean;
|
||||
buildMode?: string;
|
||||
builtIn?: boolean;
|
||||
category?: "tsdb" | "logging" | "cloud" | "tracing" | "profiling" | "sql" | "enterprise" | "iot" | "other";
|
||||
enterpriseFeatures?: EnterpriseFeatures;
|
||||
executable?: string;
|
||||
hideFromList?: boolean;
|
||||
// +listType=atomic
|
||||
includes?: Include[];
|
||||
logs?: boolean;
|
||||
metrics?: boolean;
|
||||
multiValueFilterOperators?: boolean;
|
||||
pascalName?: string;
|
||||
preload?: boolean;
|
||||
queryOptions?: QueryOptions;
|
||||
// +listType=atomic
|
||||
routes?: Route[];
|
||||
skipDataQuery?: boolean;
|
||||
state?: "alpha" | "beta";
|
||||
streaming?: boolean;
|
||||
suggestions?: boolean;
|
||||
tracing?: boolean;
|
||||
iam?: IAM;
|
||||
// +listType=atomic
|
||||
roles?: Role[];
|
||||
extensions?: Extensions;
|
||||
}
|
||||
|
||||
export const defaultJSONData = (): JSONData => ({
|
||||
id: "",
|
||||
type: "app",
|
||||
name: "",
|
||||
info: defaultInfo(),
|
||||
dependencies: defaultDependencies(),
|
||||
});
|
||||
|
||||
export interface Info {
|
||||
// Required fields
|
||||
// +listType=set
|
||||
keywords: string[];
|
||||
logos: {
|
||||
small: string;
|
||||
large: string;
|
||||
};
|
||||
updated: string;
|
||||
version: string;
|
||||
// Optional fields
|
||||
author?: {
|
||||
name?: string;
|
||||
email?: string;
|
||||
url?: string;
|
||||
};
|
||||
description?: string;
|
||||
// +listType=atomic
|
||||
links?: {
|
||||
name?: string;
|
||||
url?: string;
|
||||
}[];
|
||||
// +listType=atomic
|
||||
screenshots?: {
|
||||
name?: string;
|
||||
path?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const defaultInfo = (): Info => ({
|
||||
keywords: [],
|
||||
logos: {
|
||||
small: "",
|
||||
large: "",
|
||||
},
|
||||
updated: "",
|
||||
version: "",
|
||||
});
|
||||
|
||||
export interface Dependencies {
|
||||
// Required field
|
||||
grafanaDependency: string;
|
||||
// Optional fields
|
||||
grafanaVersion?: string;
|
||||
// +listType=set
|
||||
// +listMapKey=id
|
||||
plugins?: {
|
||||
id: string;
|
||||
type: "app" | "datasource" | "panel";
|
||||
name: string;
|
||||
}[];
|
||||
extensions?: {
|
||||
// +listType=set
|
||||
exposedComponents?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultDependencies = (): Dependencies => ({
|
||||
grafanaDependency: "",
|
||||
});
|
||||
|
||||
export interface EnterpriseFeatures {
|
||||
// Allow additional properties
|
||||
healthDiagnosticsErrors?: boolean;
|
||||
}
|
||||
|
||||
export const defaultEnterpriseFeatures = (): EnterpriseFeatures => ({
|
||||
healthDiagnosticsErrors: false,
|
||||
});
|
||||
|
||||
export interface Include {
|
||||
uid?: string;
|
||||
type?: "dashboard" | "page" | "panel" | "datasource";
|
||||
name?: string;
|
||||
component?: string;
|
||||
role?: "Admin" | "Editor" | "Viewer" | "None";
|
||||
action?: string;
|
||||
path?: string;
|
||||
addToNav?: boolean;
|
||||
defaultNav?: boolean;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export const defaultInclude = (): Include => ({
|
||||
});
|
||||
|
||||
export interface QueryOptions {
|
||||
maxDataPoints?: boolean;
|
||||
minInterval?: boolean;
|
||||
cacheTimeout?: boolean;
|
||||
}
|
||||
|
||||
export const defaultQueryOptions = (): QueryOptions => ({
|
||||
});
|
||||
|
||||
export interface Route {
|
||||
path?: string;
|
||||
method?: string;
|
||||
url?: string;
|
||||
reqSignedIn?: boolean;
|
||||
reqRole?: string;
|
||||
reqAction?: string;
|
||||
// +listType=atomic
|
||||
headers?: string[];
|
||||
body?: Record<string, any>;
|
||||
tokenAuth?: {
|
||||
url?: string;
|
||||
// +listType=set
|
||||
scopes?: string[];
|
||||
params?: Record<string, any>;
|
||||
};
|
||||
jwtTokenAuth?: {
|
||||
url?: string;
|
||||
// +listType=set
|
||||
scopes?: string[];
|
||||
params?: Record<string, any>;
|
||||
};
|
||||
// +listType=atomic
|
||||
urlParams?: {
|
||||
name?: string;
|
||||
content?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const defaultRoute = (): Route => ({
|
||||
});
|
||||
|
||||
export interface IAM {
|
||||
// +listType=atomic
|
||||
permissions?: {
|
||||
action?: string;
|
||||
scope?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const defaultIAM = (): IAM => ({
|
||||
});
|
||||
|
||||
export interface Role {
|
||||
role?: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
// +listType=atomic
|
||||
permissions?: {
|
||||
action?: string;
|
||||
scope?: string;
|
||||
}[];
|
||||
};
|
||||
// +listType=set
|
||||
grants?: string[];
|
||||
}
|
||||
|
||||
export const defaultRole = (): Role => ({
|
||||
});
|
||||
|
||||
export interface Extensions {
|
||||
// +listType=atomic
|
||||
addedComponents?: {
|
||||
// +listType=set
|
||||
targets: string[];
|
||||
title: string;
|
||||
description?: string;
|
||||
}[];
|
||||
// +listType=atomic
|
||||
addedLinks?: {
|
||||
// +listType=set
|
||||
targets: string[];
|
||||
title: string;
|
||||
description?: string;
|
||||
}[];
|
||||
// +listType=atomic
|
||||
addedFunctions?: {
|
||||
// +listType=set
|
||||
targets: string[];
|
||||
title: string;
|
||||
description?: string;
|
||||
}[];
|
||||
// +listType=set
|
||||
// +listMapKey=id
|
||||
exposedComponents?: {
|
||||
id: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}[];
|
||||
// +listType=set
|
||||
// +listMapKey=id
|
||||
extensionPoints?: {
|
||||
id: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const defaultExtensions = (): Extensions => ({
|
||||
});
|
||||
|
||||
export interface Spec {
|
||||
pluginJson: JSONData;
|
||||
class: "core" | "external";
|
||||
module?: {
|
||||
path: string;
|
||||
hash?: string;
|
||||
loadingStrategy?: "fetch" | "script";
|
||||
};
|
||||
baseURL?: string;
|
||||
signature?: {
|
||||
status: "internal" | "valid" | "invalid" | "modified" | "unsigned";
|
||||
type?: "grafana" | "commercial" | "community" | "private" | "private-glob";
|
||||
org?: string;
|
||||
};
|
||||
angular?: {
|
||||
detected: boolean;
|
||||
};
|
||||
translations?: Record<string, string>;
|
||||
// +listType=atomic
|
||||
children?: string[];
|
||||
}
|
||||
|
||||
export const defaultSpec = (): Spec => ({
|
||||
pluginJson: defaultJSONData(),
|
||||
class: "core",
|
||||
});
|
||||
|
||||
30
packages/grafana-runtime/src/services/pluginMeta/types/types.status.gen.ts
generated
Normal file
30
packages/grafana-runtime/src/services/pluginMeta/types/types.status.gen.ts
generated
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
export interface OperatorState {
|
||||
// lastEvaluation is the ResourceVersion last evaluated
|
||||
lastEvaluation: string;
|
||||
// state describes the state of the lastEvaluation.
|
||||
// It is limited to three possible states for machine evaluation.
|
||||
state: "success" | "in_progress" | "failed";
|
||||
// descriptiveState is an optional more descriptive state field which has no requirements on format
|
||||
descriptiveState?: string;
|
||||
// details contains any extra information that is operator-specific
|
||||
details?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const defaultOperatorState = (): OperatorState => ({
|
||||
lastEvaluation: "",
|
||||
state: "success",
|
||||
});
|
||||
|
||||
export interface Status {
|
||||
// operatorStates is a map of operator ID to operator state evaluations.
|
||||
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
|
||||
operatorStates?: Record<string, OperatorState>;
|
||||
// additionalFields is reserved for future use
|
||||
additionalFields?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const defaultStatus = (): Status => ({
|
||||
});
|
||||
|
||||
Loading…
Reference in a new issue