Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6024fbb363 | |||
| 512f4bc8dc | |||
| 0c49337205 | |||
| c5345498b1 | |||
| 1bcccd5e61 | |||
| 12b38d1b7a | |||
| 359d097154 | |||
| cfc5d96c34 | |||
| 88924ee9ac | |||
| 5e3c7ad0c1 | |||
| 75e08a20f6 | |||
| c8908c5100 | |||
| d8106adb63 | |||
| a4c1b51182 | |||
| 535c9be2f7 | |||
| 49f891a24d | |||
| 86018141d0 | |||
| 7fd2476a12 |
@@ -653,6 +653,7 @@ i18next.config.ts @grafana/grafana-frontend-platform
|
||||
/packages/grafana-runtime/src/components/QueryEditorWithMigration* @grafana/plugins-platform-frontend @grafana/plugins-platform-backend
|
||||
/packages/grafana-runtime/src/config.ts @grafana/grafana-frontend-platform
|
||||
/packages/grafana-runtime/src/services/ @grafana/grafana-frontend-platform
|
||||
/packages/grafana-runtime/src/services/plugins.ts @grafana/plugins-platform-frontend
|
||||
/packages/grafana-runtime/src/services/pluginExtensions @grafana/plugins-platform-frontend
|
||||
/packages/grafana-runtime/src/services/CorrelationsService.ts @grafana/datapro
|
||||
/packages/grafana-runtime/src/services/LocationService.test.tsx @grafana/grafana-search-navigate-organise
|
||||
|
||||
@@ -223,17 +223,25 @@ To export a dashboard in its current state as a PDF, follow these steps:
|
||||
|
||||
1. Click the **X** at the top-right corner to close the share drawer.
|
||||
|
||||
### Export a dashboard as JSON
|
||||
### Export a dashboard as code
|
||||
|
||||
Export a Grafana JSON file that contains everything you need, including layout, variables, styles, data sources, queries, and so on, so that you can later import the dashboard. To export a JSON file, follow these steps:
|
||||
|
||||
1. Click **Dashboards** in the main menu.
|
||||
1. Open the dashboard you want to export.
|
||||
1. Click the **Export** drop-down list in the top-right corner and select **Export as JSON**.
|
||||
1. Click the **Export** drop-down list in the top-right corner and select **Export as code**.
|
||||
|
||||
The **Export dashboard JSON** drawer opens.
|
||||
The **Export dashboard** drawer opens.
|
||||
|
||||
1. Select the dashboard JSON model that you to export:
|
||||
- **Classic** - Export dashboards created using the [current dashboard schema](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/visualizations/dashboards/build-dashboards/view-dashboard-json-model/).
|
||||
- **V1 Resource** - Export dashboards created using the [current dashboard schema](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/visualizations/dashboards/build-dashboards/view-dashboard-json-model/) wrapped in the `spec` property of the [V1 Kubernetes-style resource](https://play.grafana.org/swagger?api=dashboard.grafana.app-v2alpha1). Choose between **JSON** and **YAML** format.
|
||||
- **V2 Resource** - Export dashboards created using the [V2 Resource schema](https://play.grafana.org/swagger?api=dashboard.grafana.app-v2beta1). Choose between **JSON** and **YAML** format.
|
||||
|
||||
1. Do one of the following:
|
||||
- Toggle the **Export for sharing externally** switch to generate the JSON with a different data source UID.
|
||||
- Toggle the **Remove deployment details** switch to make the dashboard externally shareable.
|
||||
|
||||
1. Toggle the **Export the dashboard to use in another instance** switch to generate the JSON with a different data source UID.
|
||||
1. Click **Download file** or **Copy to clipboard**.
|
||||
1. Click the **X** at the top-right corner to close the share drawer.
|
||||
|
||||
|
||||
@@ -343,6 +343,33 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
|
||||
// TODO -- saving for another day.
|
||||
});
|
||||
|
||||
test('Tests nested table expansion', async ({ gotoDashboardPage, selectors, page }) => {
|
||||
const dashboardPage = await gotoDashboardPage({
|
||||
uid: DASHBOARD_UID,
|
||||
queryParams: new URLSearchParams({ editPanel: '4' }),
|
||||
});
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Nested tables'))
|
||||
).toBeVisible();
|
||||
|
||||
await waitForTableLoad(page);
|
||||
|
||||
await expect(page.locator('[role="row"]')).toHaveCount(3); // header + 2 rows
|
||||
|
||||
const firstRowExpander = dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.RowExpander)
|
||||
.first();
|
||||
|
||||
await firstRowExpander.click();
|
||||
await expect(page.locator('[role="row"]')).not.toHaveCount(3); // more rows are present now, it is dynamic tho.
|
||||
|
||||
// TODO: test sorting
|
||||
|
||||
await firstRowExpander.click();
|
||||
await expect(page.locator('[role="row"]')).toHaveCount(3); // back to original state
|
||||
});
|
||||
|
||||
test('Tests tooltip interactions', async ({ gotoDashboardPage, selectors }) => {
|
||||
const dashboardPage = await gotoDashboardPage({
|
||||
uid: DASHBOARD_UID,
|
||||
|
||||
@@ -804,11 +804,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/Table/TableNG/utils.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"packages/grafana-ui/src/components/Table/TableRT/Filter.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
|
||||
@@ -575,6 +575,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() 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() 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() from @grafana/runtime instead',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -499,6 +499,9 @@ export const versionedComponents = {
|
||||
},
|
||||
},
|
||||
TableNG: {
|
||||
RowExpander: {
|
||||
'12.4.0': 'data-testid tableng row expander',
|
||||
},
|
||||
Filters: {
|
||||
HeaderButton: {
|
||||
'12.1.0': 'data-testid tableng header filter',
|
||||
|
||||
@@ -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 getAppPluginMetas function or useAppPluginMetas hook instead */
|
||||
apps: Record<string, AppPluginConfigGrafanaData> = {};
|
||||
auth: AuthSettings = {};
|
||||
minRefreshInterval = '';
|
||||
|
||||
@@ -29,3 +29,4 @@ export {
|
||||
export { UserStorage } from '../utils/userStorage';
|
||||
|
||||
export { initOpenFeature, evaluateBooleanFlag } from './openFeature';
|
||||
export { setAppPluginMetas } from '../services/plugins';
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { AppPluginConfig } from '@grafana/data';
|
||||
|
||||
import { config } from '../config';
|
||||
|
||||
export type AppPluginMetas = Record<string, AppPluginConfig>;
|
||||
|
||||
let apps: AppPluginMetas = {};
|
||||
let appsPromise: Promise<void> | undefined = undefined;
|
||||
|
||||
function areAppsInitialized(): boolean {
|
||||
return Boolean(Object.keys(apps).length);
|
||||
}
|
||||
|
||||
async function initPluginMetas(): Promise<void> {
|
||||
if (appsPromise) {
|
||||
return appsPromise;
|
||||
}
|
||||
|
||||
appsPromise = new Promise((resolve) => {
|
||||
if (config.featureToggles.useMTPlugins) {
|
||||
// add loading app configs from MT API here
|
||||
apps = {};
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
apps = config.apps;
|
||||
resolve();
|
||||
return;
|
||||
});
|
||||
|
||||
return appsPromise;
|
||||
}
|
||||
|
||||
export async function getAppPluginMetas(): Promise<AppPluginConfig[]> {
|
||||
if (!areAppsInitialized()) {
|
||||
await initPluginMetas();
|
||||
}
|
||||
|
||||
return Object.values(cloneDeep(apps));
|
||||
}
|
||||
|
||||
export async function getAppPluginMeta(id: string): Promise<AppPluginConfig | undefined> {
|
||||
if (!areAppsInitialized()) {
|
||||
await initPluginMetas();
|
||||
}
|
||||
|
||||
if (!apps[id]) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return cloneDeep(apps[id]);
|
||||
}
|
||||
|
||||
export function setAppPluginMetas(override: AppPluginMetas) {
|
||||
// We allow overriding apps in tests
|
||||
if (override && process.env.NODE_ENV !== 'test') {
|
||||
throw new Error('setAppPluginMetas() function can only be called from tests.');
|
||||
}
|
||||
|
||||
apps = { ...override };
|
||||
}
|
||||
|
||||
export interface UseAppPluginMetasResult {
|
||||
isAppPluginMetasLoading: boolean;
|
||||
error: Error | undefined;
|
||||
apps: AppPluginConfig[];
|
||||
}
|
||||
|
||||
export function useAppPluginMetas(filterByIds: string[] = []): UseAppPluginMetasResult {
|
||||
const { loading, error, value: apps = [] } = useAsync(getAppPluginMetas);
|
||||
const filtered = apps.filter((app) => filterByIds.includes(app.id));
|
||||
|
||||
return { isAppPluginMetasLoading: loading, error, apps: filtered };
|
||||
}
|
||||
|
||||
export interface UseAppPluginMetaResult {
|
||||
isAppPluginMetaLoading: boolean;
|
||||
error: Error | undefined;
|
||||
app: AppPluginConfig | undefined;
|
||||
}
|
||||
|
||||
export function useAppPluginMeta(filterById: string): UseAppPluginMetaResult {
|
||||
const { loading, error, value: app } = useAsync(() => getAppPluginMeta(filterById));
|
||||
return { isAppPluginMetaLoading: loading, error, app };
|
||||
}
|
||||
@@ -11,3 +11,13 @@
|
||||
|
||||
// This is a dummy export so typescript doesn't error importing an "empty module"
|
||||
export const unstable = {};
|
||||
|
||||
export {
|
||||
type AppPluginMetas,
|
||||
type UseAppPluginMetaResult,
|
||||
type UseAppPluginMetasResult,
|
||||
getAppPluginMeta,
|
||||
getAppPluginMetas,
|
||||
useAppPluginMeta,
|
||||
useAppPluginMetas,
|
||||
} from './services/plugins';
|
||||
|
||||
@@ -154,8 +154,18 @@ export function TableNG(props: TableNGProps) {
|
||||
|
||||
const resizeHandler = useColumnResize(onColumnResize);
|
||||
|
||||
const rows = useMemo(() => frameToRecords(data), [data]);
|
||||
const hasNestedFrames = useMemo(() => getIsNestedTable(data.fields), [data]);
|
||||
const nestedFramesFieldName = useMemo(() => {
|
||||
if (!hasNestedFrames) {
|
||||
return;
|
||||
}
|
||||
const firstNestedField = data.fields.find((f) => f.type === FieldType.nestedFrames);
|
||||
if (!firstNestedField) {
|
||||
return;
|
||||
}
|
||||
return getDisplayName(firstNestedField);
|
||||
}, [data, hasNestedFrames]);
|
||||
const rows = useMemo(() => frameToRecords(data, nestedFramesFieldName), [data, nestedFramesFieldName]);
|
||||
const getTextColorForBackground = useMemo(() => memoize(_getTextColorForBackground, { maxSize: 1000 }), []);
|
||||
|
||||
const {
|
||||
@@ -374,7 +384,11 @@ export function TableNG(props: TableNGProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const expandedRecords = applySort(frameToRecords(nestedData), nestedData.fields, sortColumns);
|
||||
const expandedRecords = applySort(
|
||||
frameToRecords(nestedData, nestedFramesFieldName),
|
||||
nestedData.fields,
|
||||
sortColumns
|
||||
);
|
||||
if (!expandedRecords.length) {
|
||||
return (
|
||||
<div className={styles.noDataNested}>
|
||||
@@ -398,7 +412,7 @@ export function TableNG(props: TableNGProps) {
|
||||
width: COLUMN.EXPANDER_WIDTH,
|
||||
minWidth: COLUMN.EXPANDER_WIDTH,
|
||||
}),
|
||||
[commonDataGridProps, data.fields.length, expandedRows, sortColumns, styles]
|
||||
[commonDataGridProps, data.fields.length, expandedRows, sortColumns, styles, nestedFramesFieldName]
|
||||
);
|
||||
|
||||
const fromFields = useCallback(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../../../themes/ThemeContext';
|
||||
@@ -16,13 +17,21 @@ export function RowExpander({ onCellExpand, isExpanded }: RowExpanderNGProps) {
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div role="button" tabIndex={0} className={styles.expanderCell} onClick={onCellExpand} onKeyDown={handleKeyDown}>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={styles.expanderCell}
|
||||
onClick={onCellExpand}
|
||||
onKeyDown={handleKeyDown}
|
||||
data-testid={selectors.components.Panels.Visualization.TableNG.RowExpander}
|
||||
>
|
||||
<Icon
|
||||
aria-label={
|
||||
isExpanded
|
||||
? t('grafana-ui.row-expander-ng.aria-label-collapse', 'Collapse row')
|
||||
: t('grafana-ui.row-expander.aria-label-expand', 'Expand row')
|
||||
}
|
||||
aria-expanded={isExpanded}
|
||||
name={isExpanded ? 'angle-down' : 'angle-right'}
|
||||
size="lg"
|
||||
/>
|
||||
|
||||
@@ -79,7 +79,6 @@ export interface TableRow {
|
||||
|
||||
// Nested table properties
|
||||
data?: DataFrame;
|
||||
__nestedFrames?: DataFrame[];
|
||||
__expanded?: boolean; // For row expansion state
|
||||
|
||||
// Generic typing for column values
|
||||
@@ -262,7 +261,7 @@ export type TableCellStyles = (theme: GrafanaTheme2, options: TableCellStyleOpti
|
||||
export type Comparator = (a: TableCellValue, b: TableCellValue) => number;
|
||||
|
||||
// Type for converting a DataFrame into an array of TableRows
|
||||
export type FrameToRowsConverter = (frame: DataFrame) => TableRow[];
|
||||
export type FrameToRowsConverter = (frame: DataFrame, nestedFramesFieldName?: string) => TableRow[];
|
||||
|
||||
// Type for mapping column names to their field types
|
||||
export type ColumnTypes = Record<string, FieldType>;
|
||||
|
||||
@@ -675,10 +675,12 @@ export function applySort(
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const frameToRecords = (frame: DataFrame): TableRow[] => {
|
||||
export const frameToRecords = (frame: DataFrame, nestedFramesFieldName?: string): TableRow[] => {
|
||||
const fnBody = `
|
||||
const rows = Array(frame.length);
|
||||
const values = frame.fields.map(f => f.values);
|
||||
const hasNestedFrames = '${nestedFramesFieldName ?? ''}'.length > 0;
|
||||
|
||||
let rowCount = 0;
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
rows[rowCount] = {
|
||||
@@ -686,11 +688,14 @@ export const frameToRecords = (frame: DataFrame): TableRow[] => {
|
||||
__index: i,
|
||||
${frame.fields.map((field, fieldIdx) => `${JSON.stringify(getDisplayName(field))}: values[${fieldIdx}][i]`).join(',')}
|
||||
};
|
||||
rowCount += 1;
|
||||
if (rows[rowCount-1]['__nestedFrames']){
|
||||
const childFrame = rows[rowCount-1]['__nestedFrames'];
|
||||
rows[rowCount] = {__depth: 1, __index: i, data: childFrame[0]}
|
||||
rowCount += 1;
|
||||
rowCount++;
|
||||
|
||||
if (hasNestedFrames) {
|
||||
const childFrame = rows[rowCount-1][${JSON.stringify(nestedFramesFieldName)}];
|
||||
if (childFrame){
|
||||
rows[rowCount] = {__depth: 1, __index: i, data: childFrame[0]}
|
||||
rowCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
@@ -698,8 +703,9 @@ export const frameToRecords = (frame: DataFrame): TableRow[] => {
|
||||
|
||||
// Creates a function that converts a DataFrame into an array of TableRows
|
||||
// Uses new Function() for performance as it's faster than creating rows using loops
|
||||
const convert = new Function('frame', fnBody) as FrameToRowsConverter;
|
||||
return convert(frame);
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const convert = new Function('frame', 'nestedFramesFieldName', fnBody) as FrameToRowsConverter;
|
||||
return convert(frame, nestedFramesFieldName);
|
||||
};
|
||||
|
||||
/* ----------------------------- Data grid comparator ---------------------------- */
|
||||
|
||||
+3
-7
@@ -99,10 +99,9 @@ import { usePluginComponent } from './features/plugins/extensions/usePluginCompo
|
||||
import { usePluginComponents } from './features/plugins/extensions/usePluginComponents';
|
||||
import { usePluginFunctions } from './features/plugins/extensions/usePluginFunctions';
|
||||
import { usePluginLinks } from './features/plugins/extensions/usePluginLinks';
|
||||
import { getAppPluginsToAwait, getAppPluginsToPreload } from './features/plugins/extensions/utils';
|
||||
import { importPanelPlugin, syncGetPanelPlugin } from './features/plugins/importPanelPlugin';
|
||||
import { initSystemJSHooks } from './features/plugins/loader/systemjsHooks';
|
||||
import { preloadPlugins } from './features/plugins/pluginPreloader';
|
||||
import { preloadPluginsToBeAwaited, preloadPluginsToBePreloaded } from './features/plugins/pluginPreloader';
|
||||
import { QueryRunner } from './features/query/state/QueryRunner';
|
||||
import { runRequest } from './features/query/state/runRequest';
|
||||
import { initWindowRuntime } from './features/runtime/init';
|
||||
@@ -257,11 +256,8 @@ export class GrafanaApp {
|
||||
const skipAppPluginsPreload =
|
||||
config.featureToggles.rendererDisableAppPluginsPreload && contextSrv.user.authenticatedBy === 'render';
|
||||
if (contextSrv.user.orgRole !== '' && !skipAppPluginsPreload) {
|
||||
const appPluginsToAwait = getAppPluginsToAwait();
|
||||
const appPluginsToPreload = getAppPluginsToPreload();
|
||||
|
||||
preloadPlugins(appPluginsToPreload);
|
||||
await preloadPlugins(appPluginsToAwait);
|
||||
preloadPluginsToBePreloaded();
|
||||
await preloadPluginsToBeAwaited();
|
||||
}
|
||||
|
||||
setHelpNavItemHook(useHelpNode);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useLocalStorage } from 'react-use';
|
||||
|
||||
import { PluginExtensionPoints, store } from '@grafana/data';
|
||||
import { getAppEvents, reportInteraction, usePluginLinks, locationService } from '@grafana/runtime';
|
||||
import { useAppPluginMetas } from '@grafana/runtime/unstable';
|
||||
import { ExtensionPointPluginMeta, getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils';
|
||||
import { CloseExtensionSidebarEvent, OpenExtensionSidebarEvent, ToggleExtensionSidebarEvent } from 'app/types/events';
|
||||
|
||||
@@ -90,19 +91,21 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo
|
||||
// that means, a plugin would need to register both, a link and a component to
|
||||
// `grafana/extension-sidebar/v0-alpha` and the link's `configure` method would control
|
||||
// whether the component is rendered or not
|
||||
const { links, isLoading } = usePluginLinks({
|
||||
const { links, isLoading: isPluginLinksLoading } = usePluginLinks({
|
||||
extensionPointId: PluginExtensionPoints.ExtensionSidebar,
|
||||
context: {
|
||||
path: currentPath,
|
||||
},
|
||||
});
|
||||
|
||||
const { apps, isAppPluginMetasLoading: isAppPluginConfigsLoading } = useAppPluginMetas();
|
||||
const isLoading = isPluginLinksLoading || isAppPluginConfigsLoading;
|
||||
// get all components for this extension point, but only for the permitted plugins
|
||||
// if the extension sidebar is not enabled, we will return an empty map
|
||||
const availableComponents = useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
Array.from(getExtensionPointPluginMeta(PluginExtensionPoints.ExtensionSidebar).entries()).filter(
|
||||
Array.from(getExtensionPointPluginMeta(apps, PluginExtensionPoints.ExtensionSidebar).entries()).filter(
|
||||
([pluginId, pluginMeta]) =>
|
||||
PERMITTED_EXTENSION_SIDEBAR_PLUGINS.includes(pluginId) &&
|
||||
links.some(
|
||||
@@ -112,7 +115,7 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo
|
||||
)
|
||||
)
|
||||
),
|
||||
[links]
|
||||
[links, apps]
|
||||
);
|
||||
|
||||
// check if the stored docked component is still available
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getProxyApiUrl } from './onCallApi';
|
||||
|
||||
describe('getProxyApiUrl', () => {
|
||||
it('should return URL with IRM plugin ID when IRM plugin is present', () => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
|
||||
|
||||
expect(getProxyApiUrl('/alert_receive_channels/')).toBe(
|
||||
@@ -15,6 +16,7 @@ describe('getProxyApiUrl', () => {
|
||||
});
|
||||
|
||||
it('should return URL with OnCall plugin ID when IRM plugin is not present', () => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
config.apps = {
|
||||
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
||||
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
||||
|
||||
+1
@@ -67,6 +67,7 @@ describe('filterRulerRulesConfig', () => {
|
||||
};
|
||||
|
||||
it('should filter by namespace', () => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
config.apps = { [SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]) };
|
||||
const { filteredConfig, someRulesAreSkipped } = filterRulerRulesConfig(mockRulesConfig, 'namespace1');
|
||||
|
||||
|
||||
@@ -214,6 +214,7 @@ export function setGrafanaPromRules(groups: GrafanaPromRuleGroupDTO[]) {
|
||||
|
||||
/** Make a given plugin ID respond with a 404, as if it isn't installed at all */
|
||||
export const removePlugin = (pluginId: string) => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
delete config.apps[pluginId];
|
||||
server.use(getPluginMissingHandler(pluginId));
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ const PLUGIN_NOT_FOUND_RESPONSE = { message: 'Plugin not found, no installed plu
|
||||
*/
|
||||
export const getPluginsHandler = (pluginsArray: PluginMeta[] = plugins) => {
|
||||
plugins.forEach(({ id, baseUrl, info, angular }) => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
config.apps[id] = {
|
||||
id,
|
||||
path: baseUrl,
|
||||
|
||||
@@ -137,6 +137,7 @@ describe('cloneRuleDefinition', () => {
|
||||
|
||||
it('Should remove the origin label when cloning data source plugin-provided rules', () => {
|
||||
// Mock the plugin as installed
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
config.apps = {
|
||||
[SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]),
|
||||
};
|
||||
@@ -174,6 +175,7 @@ describe('cloneRuleDefinition', () => {
|
||||
});
|
||||
|
||||
it('Should remove the origin label when cloning Grafana-managed plugin-provided rules', () => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
config.apps = {
|
||||
[SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]),
|
||||
};
|
||||
|
||||
@@ -62,11 +62,13 @@ describe('checkEvaluationIntervalGlobalLimit', () => {
|
||||
|
||||
describe('getIsIrmPluginPresent', () => {
|
||||
it('should return true when IRM plugin is present in config.apps', () => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
|
||||
expect(getIsIrmPluginPresent()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when IRM plugin is not present in config.apps', () => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
config.apps = {
|
||||
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
||||
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
||||
@@ -77,11 +79,13 @@ describe('getIsIrmPluginPresent', () => {
|
||||
|
||||
describe('getIrmIfPresentOrIncidentPluginId', () => {
|
||||
it('should return IRM plugin ID when IRM plugin is present', () => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
|
||||
expect(getIrmIfPresentOrIncidentPluginId()).toBe(SupportedPlugin.Irm);
|
||||
});
|
||||
|
||||
it('should return Incident plugin ID when IRM plugin is not present', () => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
config.apps = {
|
||||
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
||||
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
||||
@@ -92,11 +96,13 @@ describe('getIrmIfPresentOrIncidentPluginId', () => {
|
||||
|
||||
describe('getIrmIfPresentOrOnCallPluginId', () => {
|
||||
it('should return IRM plugin ID when IRM plugin is present', () => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
|
||||
expect(getIrmIfPresentOrOnCallPluginId()).toBe(SupportedPlugin.Irm);
|
||||
});
|
||||
|
||||
it('should return OnCall plugin ID when IRM plugin is not present', () => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
config.apps = {
|
||||
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
||||
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
||||
|
||||
@@ -30,6 +30,7 @@ export function checkEvaluationIntervalGlobalLimit(alertGroupEvaluateEvery?: str
|
||||
}
|
||||
|
||||
export function getIsIrmPluginPresent() {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return SupportedPlugin.Irm in config.apps;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ describe('getRuleOrigin', () => {
|
||||
});
|
||||
|
||||
it('returns pluginId when origin label matches expected format and plugin is installed', () => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
config.apps = {
|
||||
installed_plugin: {
|
||||
id: 'installed_plugin',
|
||||
|
||||
@@ -273,6 +273,7 @@ export function getRulePluginOrigin(rule?: Rule | PromRuleDTO | RulerRuleDTO): R
|
||||
}
|
||||
|
||||
function isPluginInstalled(pluginId: string) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return Boolean(config.apps[pluginId]);
|
||||
}
|
||||
|
||||
|
||||
+25
-21
@@ -3,12 +3,14 @@ import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { PluginLoadingStrategy } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { setAppPluginMetas } from '@grafana/runtime/internal';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
|
||||
import { AdvisorRedirectNotice } from './AdvisorRedirectNotice';
|
||||
|
||||
const originalFeatureToggleValue = config.featureToggles.grafanaAdvisor;
|
||||
jest.mock('@grafana/runtime/internal', () => ({
|
||||
...jest.requireActual('@grafana/runtime/internal'),
|
||||
UserStorage: jest.fn().mockImplementation(() => ({
|
||||
getItem: jest.fn().mockResolvedValue('true'),
|
||||
setItem: jest.fn().mockResolvedValue(undefined),
|
||||
@@ -24,27 +26,29 @@ describe('AdvisorRedirectNotice', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
config.featureToggles.grafanaAdvisor = originalFeatureToggleValue;
|
||||
config.apps['grafana-advisor-app'] = {
|
||||
id: 'grafana-advisor-app',
|
||||
path: '/a/grafana-advisor-app',
|
||||
version: '1.0.0',
|
||||
preload: false,
|
||||
angular: { detected: false, hideDeprecation: false },
|
||||
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||
dependencies: {
|
||||
grafanaDependency: '*',
|
||||
grafanaVersion: '*',
|
||||
plugins: [],
|
||||
extensions: { exposedComponents: [] },
|
||||
setAppPluginMetas({
|
||||
'grafana-advisor-app': {
|
||||
id: 'grafana-advisor-app',
|
||||
path: '/a/grafana-advisor-app',
|
||||
version: '1.0.0',
|
||||
preload: false,
|
||||
angular: { detected: false, hideDeprecation: false },
|
||||
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||
dependencies: {
|
||||
grafanaDependency: '*',
|
||||
grafanaVersion: '*',
|
||||
plugins: [],
|
||||
extensions: { exposedComponents: [] },
|
||||
},
|
||||
extensions: {
|
||||
addedLinks: [],
|
||||
addedComponents: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
addedFunctions: [],
|
||||
},
|
||||
},
|
||||
extensions: {
|
||||
addedLinks: [],
|
||||
addedComponents: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
addedFunctions: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render when user is not admin', async () => {
|
||||
@@ -60,7 +64,7 @@ describe('AdvisorRedirectNotice', () => {
|
||||
});
|
||||
|
||||
it('should not render when app is not installed', async () => {
|
||||
delete config.apps['grafana-advisor-app'];
|
||||
setAppPluginMetas({});
|
||||
render(<AdvisorRedirectNotice />);
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
+3
-1
@@ -5,6 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { UserStorage } from '@grafana/runtime/internal';
|
||||
import { useAppPluginMeta } from '@grafana/runtime/unstable';
|
||||
import { Alert, LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
|
||||
@@ -27,8 +28,9 @@ export function AdvisorRedirectNotice() {
|
||||
const styles = useStyles2(getStyles);
|
||||
const hasAdminRights = contextSrv.hasRole('Admin') || contextSrv.isGrafanaAdmin;
|
||||
const [showNotice, setShowNotice] = useState(false);
|
||||
const { app } = useAppPluginMeta('grafana-advisor-app');
|
||||
|
||||
const canUseAdvisor = hasAdminRights && config.featureToggles.grafanaAdvisor && !!config.apps['grafana-advisor-app'];
|
||||
const canUseAdvisor = hasAdminRights && config.featureToggles.grafanaAdvisor && !!app;
|
||||
|
||||
useEffect(() => {
|
||||
if (canUseAdvisor) {
|
||||
|
||||
+101
-1
@@ -25,10 +25,17 @@ import { DashboardDataDTO } from 'app/types/dashboard';
|
||||
|
||||
import { PanelInspectDrawer } from '../../inspect/PanelInspectDrawer';
|
||||
import { PanelTimeRange, PanelTimeRangeState } from '../../scene/panel-timerange/PanelTimeRange';
|
||||
import { DashboardLayoutManager } from '../../scene/types/DashboardLayoutManager';
|
||||
import { transformSaveModelSchemaV2ToScene } from '../../serialization/transformSaveModelSchemaV2ToScene';
|
||||
import { transformSaveModelToScene } from '../../serialization/transformSaveModelToScene';
|
||||
import { findVizPanelByKey } from '../../utils/utils';
|
||||
import { buildPanelEditScene } from '../PanelEditor';
|
||||
import { testDashboard, panelWithTransformations, panelWithQueriesOnly } from '../testfiles/testDashboard';
|
||||
import {
|
||||
testDashboard,
|
||||
panelWithTransformations,
|
||||
panelWithQueriesOnly,
|
||||
testDashboardV2,
|
||||
} from '../testfiles/testDashboard';
|
||||
|
||||
import { PanelDataQueriesTab, PanelDataQueriesTabRendered } from './PanelDataQueriesTab';
|
||||
|
||||
@@ -824,6 +831,78 @@ describe('PanelDataQueriesTab', () => {
|
||||
expect(queriesTab.state.dsSettings?.uid).toBe('gdev-testdata');
|
||||
});
|
||||
});
|
||||
|
||||
describe('V2 schema behavior - panel datasource undefined but queries have datasource', () => {
|
||||
it('should load datasource from first query for V2 panel with prometheus datasource', async () => {
|
||||
// panel-1 has a query with prometheus datasource
|
||||
const { queriesTab } = await setupV2Scene('panel-1');
|
||||
|
||||
// V2 panels have undefined panel-level datasource for non-mixed panels
|
||||
expect(queriesTab.queryRunner.state.datasource).toBeUndefined();
|
||||
|
||||
// But the query has its own datasource
|
||||
expect(queriesTab.queryRunner.state.queries[0].datasource).toEqual({
|
||||
type: 'grafana-prometheus-datasource',
|
||||
uid: 'gdev-prometheus',
|
||||
});
|
||||
|
||||
// Should load the datasource from the first query
|
||||
expect(queriesTab.state.datasource?.uid).toBe('gdev-prometheus');
|
||||
expect(queriesTab.state.dsSettings?.uid).toBe('gdev-prometheus');
|
||||
});
|
||||
|
||||
it('should load datasource from first query for V2 panel with testdata datasource', async () => {
|
||||
// panel-2 has a query with testdata datasource
|
||||
const { queriesTab } = await setupV2Scene('panel-2');
|
||||
|
||||
// V2 panels have undefined panel-level datasource for non-mixed panels
|
||||
expect(queriesTab.queryRunner.state.datasource).toBeUndefined();
|
||||
|
||||
// But the query has its own datasource
|
||||
expect(queriesTab.queryRunner.state.queries[0].datasource).toEqual({
|
||||
type: 'grafana-testdata-datasource',
|
||||
uid: 'gdev-testdata',
|
||||
});
|
||||
|
||||
// Should load the datasource from the first query
|
||||
expect(queriesTab.state.datasource?.uid).toBe('gdev-testdata');
|
||||
expect(queriesTab.state.dsSettings?.uid).toBe('gdev-testdata');
|
||||
});
|
||||
|
||||
it('should fall back to last used datasource when V2 query has no explicit datasource', async () => {
|
||||
store.exists.mockReturnValue(true);
|
||||
store.getObject.mockImplementation((key: string, def: unknown) => {
|
||||
if (key === PANEL_EDIT_LAST_USED_DATASOURCE) {
|
||||
return {
|
||||
dashboardUid: 'v2-dashboard-uid',
|
||||
datasourceUid: 'gdev-testdata',
|
||||
};
|
||||
}
|
||||
return def;
|
||||
});
|
||||
|
||||
// panel-3 has a query with NO explicit datasource (datasource.name is undefined)
|
||||
const { queriesTab } = await setupV2Scene('panel-3');
|
||||
|
||||
// V2 panel with no explicit datasource on query should fall back to last used
|
||||
expect(queriesTab.state.datasource?.uid).toBe('gdev-testdata');
|
||||
expect(queriesTab.state.dsSettings?.uid).toBe('gdev-testdata');
|
||||
});
|
||||
|
||||
it('should use panel-level datasource when available (V1 behavior preserved)', async () => {
|
||||
const { queriesTab } = await setupScene('panel-1');
|
||||
|
||||
// V1 panels have panel-level datasource set
|
||||
expect(queriesTab.queryRunner.state.datasource).toEqual({
|
||||
uid: 'gdev-testdata',
|
||||
type: 'grafana-testdata-datasource',
|
||||
});
|
||||
|
||||
// Should use the panel-level datasource
|
||||
expect(queriesTab.state.datasource?.uid).toBe('gdev-testdata');
|
||||
expect(queriesTab.state.dsSettings?.uid).toBe('gdev-testdata');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -844,3 +923,24 @@ async function setupScene(panelId: string) {
|
||||
|
||||
return { panel, scene: dashboard, queriesTab };
|
||||
}
|
||||
|
||||
// Setup V2 scene - uses transformSaveModelSchemaV2ToScene
|
||||
async function setupV2Scene(panelKey: string) {
|
||||
const dashboard = transformSaveModelSchemaV2ToScene(testDashboardV2);
|
||||
|
||||
const vizPanels = (dashboard.state.body as DashboardLayoutManager).getVizPanels();
|
||||
const panel = vizPanels.find((p) => p.state.key === panelKey)!;
|
||||
|
||||
const panelEditor = buildPanelEditScene(panel);
|
||||
dashboard.setState({ editPanel: panelEditor });
|
||||
|
||||
deactivators.push(dashboard.activate());
|
||||
deactivators.push(panelEditor.activate());
|
||||
|
||||
const queriesTab = panelEditor.state.dataPane!.state.tabs[0] as PanelDataQueriesTab;
|
||||
deactivators.push(queriesTab.activate());
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
return { panel, scene: dashboard, queriesTab };
|
||||
}
|
||||
|
||||
@@ -86,6 +86,17 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
|
||||
let datasource: DataSourceApi | undefined;
|
||||
let dsSettings: DataSourceInstanceSettings | undefined;
|
||||
|
||||
// If no panel-level datasource (V2 schema non-mixed case), infer from first query
|
||||
// This also improves the V1 behavior because it doesn't make sense to rely on last used
|
||||
// if underlying queries have different datasources
|
||||
if (!datasourceToLoad) {
|
||||
const queries = this.queryRunner.state.queries;
|
||||
const firstQueryDs = queries[0]?.datasource;
|
||||
if (firstQueryDs) {
|
||||
datasourceToLoad = firstQueryDs;
|
||||
}
|
||||
}
|
||||
|
||||
if (!datasourceToLoad) {
|
||||
const dashboardScene = getDashboardSceneFor(this);
|
||||
const dashboardUid = dashboardScene.state.uid ?? '';
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { Spec as DashboardV2Spec, defaultDataQueryKind } from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
|
||||
|
||||
export const panelWithQueriesOnly = {
|
||||
datasource: {
|
||||
type: 'grafana-testdata-datasource',
|
||||
@@ -751,3 +754,223 @@ export const testDashboard = {
|
||||
version: 6,
|
||||
weekStart: '',
|
||||
};
|
||||
|
||||
// V2 Dashboard fixture - panels have queries with datasources but NO panel-level datasource
|
||||
export const testDashboardV2: DashboardWithAccessInfo<DashboardV2Spec> = {
|
||||
kind: 'DashboardWithAccessInfo',
|
||||
metadata: {
|
||||
name: 'v2-dashboard-uid',
|
||||
namespace: 'default',
|
||||
labels: {},
|
||||
generation: 1,
|
||||
resourceVersion: '1',
|
||||
creationTimestamp: new Date().toISOString(),
|
||||
},
|
||||
spec: {
|
||||
title: 'V2 Test Dashboard',
|
||||
description: 'Test dashboard for V2 schema',
|
||||
tags: [],
|
||||
cursorSync: 'Off',
|
||||
liveNow: false,
|
||||
editable: true,
|
||||
preload: false,
|
||||
links: [],
|
||||
variables: [],
|
||||
annotations: [],
|
||||
timeSettings: {
|
||||
from: 'now-6h',
|
||||
to: 'now',
|
||||
autoRefresh: '',
|
||||
autoRefreshIntervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'],
|
||||
fiscalYearStartMonth: 0,
|
||||
hideTimepicker: false,
|
||||
timezone: '',
|
||||
weekStart: undefined,
|
||||
quickRanges: [],
|
||||
},
|
||||
elements: {
|
||||
'panel-1': {
|
||||
kind: 'Panel',
|
||||
spec: {
|
||||
id: 1,
|
||||
title: 'Panel with Prometheus datasource',
|
||||
description: '',
|
||||
links: [],
|
||||
data: {
|
||||
kind: 'QueryGroup',
|
||||
spec: {
|
||||
queries: [
|
||||
{
|
||||
kind: 'PanelQuery',
|
||||
spec: {
|
||||
refId: 'A',
|
||||
hidden: false,
|
||||
query: {
|
||||
kind: 'DataQuery',
|
||||
version: defaultDataQueryKind().version,
|
||||
group: 'grafana-prometheus-datasource',
|
||||
datasource: {
|
||||
name: 'gdev-prometheus',
|
||||
},
|
||||
spec: {
|
||||
expr: 'up',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
transformations: [],
|
||||
queryOptions: {},
|
||||
},
|
||||
},
|
||||
vizConfig: {
|
||||
kind: 'VizConfig',
|
||||
group: 'timeseries',
|
||||
version: '1.0.0',
|
||||
spec: {
|
||||
options: {},
|
||||
fieldConfig: {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'panel-2': {
|
||||
kind: 'Panel',
|
||||
spec: {
|
||||
id: 2,
|
||||
title: 'Panel with TestData datasource',
|
||||
description: '',
|
||||
links: [],
|
||||
data: {
|
||||
kind: 'QueryGroup',
|
||||
spec: {
|
||||
queries: [
|
||||
{
|
||||
kind: 'PanelQuery',
|
||||
spec: {
|
||||
refId: 'A',
|
||||
hidden: false,
|
||||
query: {
|
||||
kind: 'DataQuery',
|
||||
version: defaultDataQueryKind().version,
|
||||
group: 'grafana-testdata-datasource',
|
||||
datasource: {
|
||||
name: 'gdev-testdata',
|
||||
},
|
||||
spec: {
|
||||
scenarioId: 'random_walk',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
transformations: [],
|
||||
queryOptions: {},
|
||||
},
|
||||
},
|
||||
vizConfig: {
|
||||
kind: 'VizConfig',
|
||||
group: 'timeseries',
|
||||
version: '1.0.0',
|
||||
spec: {
|
||||
options: {},
|
||||
fieldConfig: {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'panel-3': {
|
||||
kind: 'Panel',
|
||||
spec: {
|
||||
id: 3,
|
||||
title: 'Panel with no datasource on query',
|
||||
description: '',
|
||||
links: [],
|
||||
data: {
|
||||
kind: 'QueryGroup',
|
||||
spec: {
|
||||
queries: [
|
||||
{
|
||||
kind: 'PanelQuery',
|
||||
spec: {
|
||||
refId: 'A',
|
||||
hidden: false,
|
||||
query: {
|
||||
kind: 'DataQuery',
|
||||
version: defaultDataQueryKind().version,
|
||||
group: 'grafana-testdata-datasource',
|
||||
// No datasource.name - simulates panel with no explicit datasource
|
||||
spec: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
transformations: [],
|
||||
queryOptions: {},
|
||||
},
|
||||
},
|
||||
vizConfig: {
|
||||
kind: 'VizConfig',
|
||||
group: 'timeseries',
|
||||
version: '1.0.0',
|
||||
spec: {
|
||||
options: {},
|
||||
fieldConfig: {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
kind: 'GridLayout',
|
||||
spec: {
|
||||
items: [
|
||||
{
|
||||
kind: 'GridLayoutItem',
|
||||
spec: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 12,
|
||||
height: 8,
|
||||
element: { kind: 'ElementReference', name: 'panel-1' },
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'GridLayoutItem',
|
||||
spec: {
|
||||
x: 12,
|
||||
y: 0,
|
||||
width: 12,
|
||||
height: 8,
|
||||
element: { kind: 'ElementReference', name: 'panel-2' },
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'GridLayoutItem',
|
||||
spec: {
|
||||
x: 0,
|
||||
y: 8,
|
||||
width: 12,
|
||||
height: 8,
|
||||
element: { kind: 'ElementReference', name: 'panel-3' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
access: {
|
||||
url: '/d/v2-dashboard-uid',
|
||||
slug: 'v2-test-dashboard',
|
||||
},
|
||||
apiVersion: 'v2',
|
||||
};
|
||||
|
||||
@@ -18,7 +18,8 @@ import { isDashboardLayoutGrid } from '../types/DashboardLayoutGrid';
|
||||
import { RowItem } from './RowItem';
|
||||
|
||||
export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
|
||||
const { layout, collapse: isCollapsed, fillScreen, hideHeader: isHeaderHidden, isDropTarget, key } = model.useState();
|
||||
const { layout, collapse, fillScreen, hideHeader: isHeaderHidden, isDropTarget, key } = model.useState();
|
||||
const isCollapsed = collapse && !isHeaderHidden; // never allow a row without a header to be collapsed
|
||||
const isClone = isRepeatCloneOrChildOf(model);
|
||||
const { isEditing } = useDashboardState(model);
|
||||
const [isConditionallyHidden, conditionalRenderingClass, conditionalRenderingOverlay] = useIsConditionallyHidden(
|
||||
|
||||
@@ -17,14 +17,9 @@ jest.mock('@grafana/llm', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
apps: {
|
||||
'grafana-llm-app': true,
|
||||
},
|
||||
},
|
||||
jest.mock('@grafana/runtime/unstable', () => ({
|
||||
...jest.requireActual('@grafana/runtime/unstable'),
|
||||
getAppPluginMeta: () => Promise.resolve({}),
|
||||
}));
|
||||
|
||||
describe('getDashboardChanges', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import { llm } from '@grafana/llm';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||
import { Panel } from '@grafana/schema';
|
||||
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
@@ -70,7 +70,8 @@ let llmHealthCheck: Promise<boolean> | undefined;
|
||||
* @returns true if the LLM plugin is enabled.
|
||||
*/
|
||||
export async function isLLMPluginEnabled(): Promise<boolean> {
|
||||
if (!config.apps['grafana-llm-app']) {
|
||||
const app = await getAppPluginMeta('grafana-llm-app');
|
||||
if (!app) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import React from 'react';
|
||||
import { firstValueFrom, take } from 'rxjs';
|
||||
|
||||
import { PluginLoadingStrategy } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { setAppPluginMetas } from '@grafana/runtime/internal';
|
||||
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||
|
||||
import { log } from '../logs/log';
|
||||
import { resetLogMock } from '../logs/testUtils';
|
||||
@@ -30,7 +31,6 @@ jest.mock('../logs/log', () => {
|
||||
});
|
||||
|
||||
describe('AddedComponentsRegistry', () => {
|
||||
const originalApps = config.apps;
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const appPluginConfig = {
|
||||
id: pluginId,
|
||||
@@ -61,13 +61,11 @@ describe('AddedComponentsRegistry', () => {
|
||||
beforeEach(() => {
|
||||
resetLogMock(log);
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
config.apps = {
|
||||
[pluginId]: appPluginConfig,
|
||||
};
|
||||
setAppPluginMetas({ [pluginId]: appPluginConfig });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.apps = originalApps;
|
||||
setAppPluginMetas({});
|
||||
});
|
||||
|
||||
it('should return empty registry when no extensions registered', async () => {
|
||||
@@ -450,7 +448,11 @@ describe('AddedComponentsRegistry', () => {
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedComponents = [];
|
||||
const meta = await getAppPluginMeta(pluginId);
|
||||
expect(meta).toBeDefined();
|
||||
|
||||
const app = { ...meta!, extensions: { ...meta!.extensions, addedComponents: [] } };
|
||||
setAppPluginMetas({ [pluginId]: app });
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
@@ -499,7 +501,11 @@ describe('AddedComponentsRegistry', () => {
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedComponents = [];
|
||||
const meta = await getAppPluginMeta(pluginId);
|
||||
expect(meta).toBeDefined();
|
||||
|
||||
const app = { ...meta!, extensions: { ...meta!.extensions, addedComponents: [] } };
|
||||
setAppPluginMetas({ [pluginId]: app });
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
@@ -525,7 +531,11 @@ describe('AddedComponentsRegistry', () => {
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedComponents = [componentConfig];
|
||||
const meta = await getAppPluginMeta(pluginId);
|
||||
expect(meta).toBeDefined();
|
||||
|
||||
const app = { ...meta!, extensions: { ...meta!.extensions, addedComponents: [componentConfig] } };
|
||||
setAppPluginMetas({ [pluginId]: app });
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
|
||||
@@ -30,10 +30,10 @@ export class AddedComponentsRegistry extends Registry<
|
||||
super(options);
|
||||
}
|
||||
|
||||
mapToRegistry(
|
||||
async mapToRegistry(
|
||||
registry: RegistryType<AddedComponentRegistryItem[]>,
|
||||
item: PluginExtensionConfigs<PluginExtensionAddedComponentConfig>
|
||||
): RegistryType<AddedComponentRegistryItem[]> {
|
||||
): Promise<RegistryType<AddedComponentRegistryItem[]>> {
|
||||
const { pluginId, configs } = item;
|
||||
|
||||
for (const config of configs) {
|
||||
@@ -51,7 +51,7 @@ export class AddedComponentsRegistry extends Registry<
|
||||
if (
|
||||
pluginId !== 'grafana' &&
|
||||
isGrafanaDevMode() &&
|
||||
isAddedComponentMetaInfoMissing(pluginId, config, configLog)
|
||||
(await isAddedComponentMetaInfoMissing(pluginId, config, configLog))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { firstValueFrom, take } from 'rxjs';
|
||||
|
||||
import { PluginLoadingStrategy } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { setAppPluginMetas } from '@grafana/runtime/internal';
|
||||
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||
|
||||
import { log } from '../logs/log';
|
||||
import { resetLogMock } from '../logs/testUtils';
|
||||
@@ -29,7 +30,6 @@ jest.mock('../logs/log', () => {
|
||||
});
|
||||
|
||||
describe('addedFunctionsRegistry', () => {
|
||||
const originalApps = config.apps;
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const appPluginConfig = {
|
||||
id: pluginId,
|
||||
@@ -60,13 +60,11 @@ describe('addedFunctionsRegistry', () => {
|
||||
beforeEach(() => {
|
||||
resetLogMock(log);
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
config.apps = {
|
||||
[pluginId]: appPluginConfig,
|
||||
};
|
||||
setAppPluginMetas({ [pluginId]: appPluginConfig });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.apps = originalApps;
|
||||
setAppPluginMetas({});
|
||||
});
|
||||
|
||||
it('should return empty registry when no extensions registered', async () => {
|
||||
@@ -642,7 +640,11 @@ describe('addedFunctionsRegistry', () => {
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedFunctions = [];
|
||||
const meta = await getAppPluginMeta(pluginId);
|
||||
expect(meta).toBeDefined();
|
||||
|
||||
const app = { ...meta!, extensions: { ...meta!.extensions, addedFunctions: [] } };
|
||||
setAppPluginMetas({ [pluginId]: app });
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
@@ -691,7 +693,11 @@ describe('addedFunctionsRegistry', () => {
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedFunctions = [];
|
||||
const meta = await getAppPluginMeta(pluginId);
|
||||
expect(meta).toBeDefined();
|
||||
|
||||
const app = { ...meta!, extensions: { ...meta!.extensions, addedFunctions: [] } };
|
||||
setAppPluginMetas({ [pluginId]: app });
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
@@ -717,7 +723,11 @@ describe('addedFunctionsRegistry', () => {
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedFunctions = [fnConfig];
|
||||
const meta = await getAppPluginMeta(pluginId);
|
||||
expect(meta).toBeDefined();
|
||||
|
||||
const app = { ...meta!, extensions: { ...meta!.extensions, addedFunctions: [fnConfig] } };
|
||||
setAppPluginMetas({ [pluginId]: app });
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
|
||||
@@ -28,11 +28,12 @@ export class AddedFunctionsRegistry extends Registry<AddedFunctionsRegistryItem[
|
||||
super(options);
|
||||
}
|
||||
|
||||
mapToRegistry(
|
||||
async mapToRegistry(
|
||||
registry: RegistryType<AddedFunctionsRegistryItem[]>,
|
||||
item: PluginExtensionConfigs<PluginExtensionAddedFunctionConfig>
|
||||
): RegistryType<AddedFunctionsRegistryItem[]> {
|
||||
): Promise<RegistryType<AddedFunctionsRegistryItem[]>> {
|
||||
const { pluginId, configs } = item;
|
||||
|
||||
for (const config of configs) {
|
||||
const configLog = this.logger.child({
|
||||
title: config.title,
|
||||
@@ -49,7 +50,11 @@ export class AddedFunctionsRegistry extends Registry<AddedFunctionsRegistryItem[
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pluginId !== 'grafana' && isGrafanaDevMode() && isAddedFunctionMetaInfoMissing(pluginId, config, configLog)) {
|
||||
if (
|
||||
pluginId !== 'grafana' &&
|
||||
isGrafanaDevMode() &&
|
||||
(await isAddedFunctionMetaInfoMissing(pluginId, config, configLog))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { firstValueFrom, take } from 'rxjs';
|
||||
|
||||
import { PluginLoadingStrategy } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { setAppPluginMetas } from '@grafana/runtime/internal';
|
||||
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||
|
||||
import { log } from '../logs/log';
|
||||
import { resetLogMock } from '../logs/testUtils';
|
||||
@@ -29,7 +30,6 @@ jest.mock('../logs/log', () => {
|
||||
});
|
||||
|
||||
describe('AddedLinksRegistry', () => {
|
||||
const originalApps = config.apps;
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const appPluginConfig = {
|
||||
id: pluginId,
|
||||
@@ -60,13 +60,11 @@ describe('AddedLinksRegistry', () => {
|
||||
beforeEach(() => {
|
||||
resetLogMock(log);
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
config.apps = {
|
||||
[pluginId]: appPluginConfig,
|
||||
};
|
||||
setAppPluginMetas({ [pluginId]: appPluginConfig });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.apps = originalApps;
|
||||
setAppPluginMetas({});
|
||||
});
|
||||
|
||||
it('should return empty registry when no extensions registered', async () => {
|
||||
@@ -626,7 +624,11 @@ describe('AddedLinksRegistry', () => {
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedLinks = [];
|
||||
const meta = await getAppPluginMeta(pluginId);
|
||||
expect(meta).toBeDefined();
|
||||
|
||||
const app = { ...meta!, extensions: { ...meta!.extensions, addedLinks: [] } };
|
||||
setAppPluginMetas({ [pluginId]: app });
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
@@ -677,7 +679,11 @@ describe('AddedLinksRegistry', () => {
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedLinks = [];
|
||||
const meta = await getAppPluginMeta(pluginId);
|
||||
expect(meta).toBeDefined();
|
||||
|
||||
const app = { ...meta!, extensions: { ...meta!.extensions, addedLinks: [] } };
|
||||
setAppPluginMetas({ [pluginId]: app });
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
@@ -704,7 +710,11 @@ describe('AddedLinksRegistry', () => {
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.addedLinks = [linkConfig];
|
||||
const meta = await getAppPluginMeta(pluginId);
|
||||
expect(meta).toBeDefined();
|
||||
|
||||
const app = { ...meta!, extensions: { ...meta!.extensions, addedLinks: [linkConfig] } };
|
||||
setAppPluginMetas({ [pluginId]: app });
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
|
||||
@@ -34,10 +34,10 @@ export class AddedLinksRegistry extends Registry<AddedLinkRegistryItem[], Plugin
|
||||
super(options);
|
||||
}
|
||||
|
||||
mapToRegistry(
|
||||
async mapToRegistry(
|
||||
registry: RegistryType<AddedLinkRegistryItem[]>,
|
||||
item: PluginExtensionConfigs<PluginExtensionAddedLinkConfig>
|
||||
): RegistryType<AddedLinkRegistryItem[]> {
|
||||
): Promise<RegistryType<AddedLinkRegistryItem[]>> {
|
||||
const { pluginId, configs } = item;
|
||||
|
||||
for (const config of configs) {
|
||||
@@ -66,7 +66,11 @@ export class AddedLinksRegistry extends Registry<AddedLinkRegistryItem[], Plugin
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pluginId !== 'grafana' && isGrafanaDevMode() && isAddedLinkMetaInfoMissing(pluginId, config, configLog)) {
|
||||
if (
|
||||
pluginId !== 'grafana' &&
|
||||
isGrafanaDevMode() &&
|
||||
(await isAddedLinkMetaInfoMissing(pluginId, config, configLog))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import React from 'react';
|
||||
import { firstValueFrom, take } from 'rxjs';
|
||||
|
||||
import { PluginLoadingStrategy } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { setAppPluginMetas } from '@grafana/runtime/internal';
|
||||
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||
|
||||
import { log } from '../logs/log';
|
||||
import { resetLogMock } from '../logs/testUtils';
|
||||
@@ -30,7 +31,6 @@ jest.mock('../logs/log', () => {
|
||||
});
|
||||
|
||||
describe('ExposedComponentsRegistry', () => {
|
||||
const originalApps = config.apps;
|
||||
const pluginId = 'grafana-basic-app';
|
||||
const appPluginConfig = {
|
||||
id: pluginId,
|
||||
@@ -61,13 +61,11 @@ describe('ExposedComponentsRegistry', () => {
|
||||
beforeEach(() => {
|
||||
resetLogMock(log);
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
|
||||
config.apps = {
|
||||
[pluginId]: appPluginConfig,
|
||||
};
|
||||
setAppPluginMetas({ [pluginId]: appPluginConfig });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.apps = originalApps;
|
||||
setAppPluginMetas({});
|
||||
});
|
||||
|
||||
it('should return empty registry when no exposed components have been registered', async () => {
|
||||
@@ -423,7 +421,11 @@ describe('ExposedComponentsRegistry', () => {
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.exposedComponents = [];
|
||||
const meta = await getAppPluginMeta(pluginId);
|
||||
expect(meta).toBeDefined();
|
||||
|
||||
const app = { ...meta!, extensions: { ...meta!.extensions, exposedComponents: [] } };
|
||||
setAppPluginMetas({ [pluginId]: app });
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
@@ -472,7 +474,11 @@ describe('ExposedComponentsRegistry', () => {
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.exposedComponents = [];
|
||||
const meta = await getAppPluginMeta(pluginId);
|
||||
expect(meta).toBeDefined();
|
||||
|
||||
const app = { ...meta!, extensions: { ...meta!.extensions, exposedComponents: [] } };
|
||||
setAppPluginMetas({ [pluginId]: app });
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
@@ -497,8 +503,11 @@ describe('ExposedComponentsRegistry', () => {
|
||||
component: () => React.createElement('div', null, 'Hello World1'),
|
||||
};
|
||||
|
||||
// Make sure that the meta-info is empty
|
||||
config.apps[pluginId].extensions.exposedComponents = [componentConfig];
|
||||
const meta = await getAppPluginMeta(pluginId);
|
||||
expect(meta).toBeDefined();
|
||||
|
||||
const app = { ...meta!, extensions: { ...meta!.extensions, exposedComponents: [componentConfig] } };
|
||||
setAppPluginMetas({ [pluginId]: app });
|
||||
|
||||
registry.register({
|
||||
pluginId,
|
||||
|
||||
@@ -30,10 +30,10 @@ export class ExposedComponentsRegistry extends Registry<
|
||||
super(options);
|
||||
}
|
||||
|
||||
mapToRegistry(
|
||||
async mapToRegistry(
|
||||
registry: RegistryType<ExposedComponentRegistryItem>,
|
||||
{ pluginId, configs }: PluginExtensionConfigs<PluginExtensionExposedComponentConfig>
|
||||
): RegistryType<ExposedComponentRegistryItem> {
|
||||
): Promise<RegistryType<ExposedComponentRegistryItem>> {
|
||||
if (!configs) {
|
||||
return registry;
|
||||
}
|
||||
@@ -65,7 +65,7 @@ export class ExposedComponentsRegistry extends Registry<
|
||||
if (
|
||||
pluginId !== 'grafana' &&
|
||||
isGrafanaDevMode() &&
|
||||
isExposedComponentMetaInfoMissing(pluginId, config, pointIdLog)
|
||||
(await isExposedComponentMetaInfoMissing(pluginId, config, pointIdLog))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { Observable, ReplaySubject, Subject, distinctUntilChanged, firstValueFrom, map, scan, startWith } from 'rxjs';
|
||||
import {
|
||||
Observable,
|
||||
ReplaySubject,
|
||||
Subject,
|
||||
distinctUntilChanged,
|
||||
firstValueFrom,
|
||||
map,
|
||||
mergeScan,
|
||||
startWith,
|
||||
} from 'rxjs';
|
||||
|
||||
import { ExtensionsLog, log } from '../logs/log';
|
||||
import { deepFreeze } from '../utils';
|
||||
@@ -44,7 +53,7 @@ export abstract class Registry<TRegistryValue extends object | unknown[] | Recor
|
||||
this.registrySubject = new ReplaySubject<RegistryType<TRegistryValue>>(1);
|
||||
this.resultSubject
|
||||
.pipe(
|
||||
scan(this.mapToRegistry.bind(this), options.initialState ?? {}),
|
||||
mergeScan(this.mapToRegistry.bind(this), options.initialState ?? {}),
|
||||
// Emit an empty registry to start the stream (it is only going to do it once during construction, and then just passes down the values)
|
||||
startWith(options.initialState ?? {})
|
||||
)
|
||||
@@ -55,7 +64,7 @@ export abstract class Registry<TRegistryValue extends object | unknown[] | Recor
|
||||
abstract mapToRegistry(
|
||||
registry: RegistryType<TRegistryValue>,
|
||||
item: PluginExtensionConfigs<TMapType>
|
||||
): RegistryType<TRegistryValue>;
|
||||
): Promise<RegistryType<TRegistryValue>>;
|
||||
|
||||
register(result: PluginExtensionConfigs<TMapType>): void {
|
||||
if (this.isReadOnly) {
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { preloadPlugins } from '../pluginPreloader';
|
||||
import { PreloadAppPluginsPredicate, preloadPluginsWithPredicate } from '../pluginPreloader';
|
||||
|
||||
import { getAppPluginConfigs } from './utils';
|
||||
export function useLoadAppPlugins(extensionId: string, predicate: PreloadAppPluginsPredicate): { isLoading: boolean } {
|
||||
const { loading: isLoading } = useAsync(
|
||||
() => preloadPluginsWithPredicate(extensionId, predicate),
|
||||
[extensionId, predicate]
|
||||
);
|
||||
|
||||
export function useLoadAppPlugins(pluginIds: string[] = []): { isLoading: boolean } {
|
||||
const { loading: isLoading } = useAsync(async () => {
|
||||
const appConfigs = getAppPluginConfigs(pluginIds);
|
||||
|
||||
if (!appConfigs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
await preloadPlugins(appConfigs);
|
||||
});
|
||||
|
||||
return { isLoading };
|
||||
return { isLoading: isLoading };
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { JSX } from 'react';
|
||||
|
||||
import { PluginContextProvider, PluginLoadingStrategy, PluginMeta, PluginType } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { setAppPluginMetas } from '@grafana/runtime/internal';
|
||||
|
||||
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
||||
import { log } from './logs/log';
|
||||
@@ -51,7 +52,6 @@ describe('usePluginComponent()', () => {
|
||||
let registries: PluginExtensionRegistries;
|
||||
let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element;
|
||||
let pluginMeta: PluginMeta;
|
||||
const originalApps = config.apps;
|
||||
const pluginId = 'myorg-extensions-app';
|
||||
const exposedComponentId = `${pluginId}/exposed-component/v1`;
|
||||
const exposedComponentConfig = {
|
||||
@@ -135,9 +135,7 @@ describe('usePluginComponent()', () => {
|
||||
},
|
||||
};
|
||||
|
||||
config.apps = {
|
||||
[pluginId]: appPluginConfig,
|
||||
};
|
||||
setAppPluginMetas({ [pluginId]: appPluginConfig });
|
||||
|
||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<ExtensionRegistriesProvider registries={registries}>{children}</ExtensionRegistriesProvider>
|
||||
@@ -145,7 +143,7 @@ describe('usePluginComponent()', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.apps = originalApps;
|
||||
setAppPluginMetas({});
|
||||
});
|
||||
|
||||
it('should return null if there are no component exposed for the id', () => {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { isExposedComponentDependencyMissing } from './validators';
|
||||
export function usePluginComponent<Props extends object = {}>(id: string): UsePluginComponentResult<Props> {
|
||||
const registryItem = useExposedComponentRegistrySlice<Props>(id);
|
||||
const pluginContext = usePluginContext();
|
||||
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExposedComponentPluginDependencies(id));
|
||||
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(id, getExposedComponentPluginDependencies);
|
||||
|
||||
return useMemo(() => {
|
||||
// For backwards compatibility we don't enable restrictions in production or when the hook is used in core Grafana.
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
PluginType,
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { setAppPluginMetas } from '@grafana/runtime/internal';
|
||||
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||
|
||||
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
||||
import * as errors from './errors';
|
||||
@@ -115,31 +117,33 @@ describe('usePluginComponents()', () => {
|
||||
},
|
||||
};
|
||||
|
||||
config.apps[pluginId] = {
|
||||
id: pluginId,
|
||||
path: '',
|
||||
version: '',
|
||||
preload: false,
|
||||
angular: {
|
||||
detected: false,
|
||||
hideDeprecation: false,
|
||||
},
|
||||
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||
dependencies: {
|
||||
grafanaVersion: '8.0.0',
|
||||
plugins: [],
|
||||
setAppPluginMetas({
|
||||
[pluginId]: {
|
||||
id: pluginId,
|
||||
path: '',
|
||||
version: '',
|
||||
preload: false,
|
||||
angular: {
|
||||
detected: false,
|
||||
hideDeprecation: false,
|
||||
},
|
||||
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||
dependencies: {
|
||||
grafanaVersion: '8.0.0',
|
||||
plugins: [],
|
||||
extensions: {
|
||||
exposedComponents: [],
|
||||
},
|
||||
},
|
||||
extensions: {
|
||||
addedLinks: [],
|
||||
addedComponents: [],
|
||||
addedFunctions: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
},
|
||||
extensions: {
|
||||
addedLinks: [],
|
||||
addedComponents: [],
|
||||
addedFunctions: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<PluginContextProvider meta={pluginMeta}>
|
||||
@@ -496,7 +500,7 @@ describe('usePluginComponents()', () => {
|
||||
});
|
||||
|
||||
// It can happen that core Grafana plugins (e.g. traces) reuse core components which implement extension points.
|
||||
it('should not validate the extension point meta-info for core plugins', () => {
|
||||
it('should not validate the extension point meta-info for core plugins', async () => {
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const componentConfig = {
|
||||
@@ -506,8 +510,12 @@ describe('usePluginComponents()', () => {
|
||||
component: () => <div>Component</div>,
|
||||
};
|
||||
|
||||
// The `AddedComponentsRegistry` is validating if the link is registered in the plugin metadata (config.apps).
|
||||
config.apps[pluginId].extensions.addedComponents = [componentConfig];
|
||||
// The `AddedComponentsRegistry` is validating if the link is registered in the plugin metadata.
|
||||
const meta = await getAppPluginMeta(pluginId);
|
||||
expect(meta).toBeDefined();
|
||||
|
||||
const app = { ...meta!, extensions: { ...meta!.extensions, addedComponents: [componentConfig] } };
|
||||
setAppPluginMetas({ [pluginId]: app });
|
||||
|
||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<PluginContextProvider
|
||||
|
||||
@@ -21,7 +21,7 @@ export function usePluginComponents<Props extends object = {}>({
|
||||
}: UsePluginComponentsOptions): UsePluginComponentsResult<Props> {
|
||||
const registryItems = useAddedComponentsRegistrySlice<Props>(extensionPointId);
|
||||
const pluginContext = usePluginContext();
|
||||
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExtensionPointPluginDependencies(extensionPointId));
|
||||
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(extensionPointId, getExtensionPointPluginDependencies);
|
||||
|
||||
return useMemo(() => {
|
||||
const { result } = validateExtensionPoint({ extensionPointId, pluginContext, isLoadingAppPlugins });
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
PluginMeta,
|
||||
PluginType,
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { setAppPluginMetas } from '@grafana/runtime/internal';
|
||||
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||
|
||||
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
||||
import * as errors from './errors';
|
||||
@@ -107,31 +108,33 @@ describe('usePluginFunctions()', () => {
|
||||
},
|
||||
};
|
||||
|
||||
config.apps[pluginId] = {
|
||||
id: pluginId,
|
||||
path: '',
|
||||
version: '',
|
||||
preload: false,
|
||||
angular: {
|
||||
detected: false,
|
||||
hideDeprecation: false,
|
||||
},
|
||||
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||
dependencies: {
|
||||
grafanaVersion: '8.0.0',
|
||||
plugins: [],
|
||||
setAppPluginMetas({
|
||||
[pluginId]: {
|
||||
id: pluginId,
|
||||
path: '',
|
||||
version: '',
|
||||
preload: false,
|
||||
angular: {
|
||||
detected: false,
|
||||
hideDeprecation: false,
|
||||
},
|
||||
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||
dependencies: {
|
||||
grafanaVersion: '8.0.0',
|
||||
plugins: [],
|
||||
extensions: {
|
||||
exposedComponents: [],
|
||||
},
|
||||
},
|
||||
extensions: {
|
||||
addedLinks: [],
|
||||
addedComponents: [],
|
||||
addedFunctions: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
},
|
||||
extensions: {
|
||||
addedLinks: [],
|
||||
addedComponents: [],
|
||||
addedFunctions: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<PluginContextProvider meta={pluginMeta}>
|
||||
@@ -318,7 +321,7 @@ describe('usePluginFunctions()', () => {
|
||||
});
|
||||
|
||||
// It can happen that core Grafana plugins (e.g. traces) reuse core components which implement extension points.
|
||||
it('should not validate the extension point meta-info for core plugins', () => {
|
||||
it('should not validate the extension point meta-info for core plugins', async () => {
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const functionConfig = {
|
||||
@@ -328,8 +331,12 @@ describe('usePluginFunctions()', () => {
|
||||
fn: () => 'function1',
|
||||
};
|
||||
|
||||
// The `AddedFunctionsRegistry` is validating if the function is registered in the plugin metadata (config.apps).
|
||||
config.apps[pluginId].extensions.addedFunctions = [functionConfig];
|
||||
// The `AddedFunctionsRegistry` is validating if the function is registered in the plugin metadata.
|
||||
const meta = await getAppPluginMeta(pluginId);
|
||||
expect(meta).toBeDefined();
|
||||
|
||||
const app = { ...meta!, extensions: { ...meta!.extensions, addedFunctions: [functionConfig] } };
|
||||
setAppPluginMetas({ [pluginId]: app });
|
||||
|
||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<PluginContextProvider
|
||||
|
||||
@@ -15,8 +15,7 @@ export function usePluginFunctions<Signature>({
|
||||
}: UsePluginFunctionsOptions): UsePluginFunctionsResult<Signature> {
|
||||
const registryItems = useAddedFunctionsRegistrySlice<Signature>(extensionPointId);
|
||||
const pluginContext = usePluginContext();
|
||||
const deps = getExtensionPointPluginDependencies(extensionPointId);
|
||||
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(deps);
|
||||
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(extensionPointId, getExtensionPointPluginDependencies);
|
||||
|
||||
return useMemo(() => {
|
||||
const { result } = validateExtensionPoint({ extensionPointId, pluginContext, isLoadingAppPlugins });
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
PluginMeta,
|
||||
PluginType,
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { setAppPluginMetas } from '@grafana/runtime/internal';
|
||||
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||
|
||||
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
|
||||
import * as errors from './errors';
|
||||
@@ -107,31 +108,33 @@ describe('usePluginLinks()', () => {
|
||||
},
|
||||
};
|
||||
|
||||
config.apps[pluginId] = {
|
||||
id: pluginId,
|
||||
path: '',
|
||||
version: '',
|
||||
preload: false,
|
||||
angular: {
|
||||
detected: false,
|
||||
hideDeprecation: false,
|
||||
},
|
||||
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||
dependencies: {
|
||||
grafanaVersion: '8.0.0',
|
||||
plugins: [],
|
||||
setAppPluginMetas({
|
||||
[pluginId]: {
|
||||
id: pluginId,
|
||||
path: '',
|
||||
version: '',
|
||||
preload: false,
|
||||
angular: {
|
||||
detected: false,
|
||||
hideDeprecation: false,
|
||||
},
|
||||
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||
dependencies: {
|
||||
grafanaVersion: '8.0.0',
|
||||
plugins: [],
|
||||
extensions: {
|
||||
exposedComponents: [],
|
||||
},
|
||||
},
|
||||
extensions: {
|
||||
addedLinks: [],
|
||||
addedComponents: [],
|
||||
addedFunctions: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
},
|
||||
extensions: {
|
||||
addedLinks: [],
|
||||
addedComponents: [],
|
||||
addedFunctions: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<PluginContextProvider meta={pluginMeta}>
|
||||
@@ -258,7 +261,7 @@ describe('usePluginLinks()', () => {
|
||||
});
|
||||
|
||||
// It can happen that core Grafana plugins (e.g. traces) reuse core components which implement extension points.
|
||||
it('should not validate the extension point meta-info for core plugins', () => {
|
||||
it('should not validate the extension point meta-info for core plugins', async () => {
|
||||
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
|
||||
|
||||
const linkConfig = {
|
||||
@@ -269,7 +272,11 @@ describe('usePluginLinks()', () => {
|
||||
};
|
||||
|
||||
// The `AddedLinksRegistry` is validating if the link is registered in the plugin metadata (config.apps).
|
||||
config.apps[pluginId].extensions.addedLinks = [linkConfig];
|
||||
const meta = await getAppPluginMeta(pluginId);
|
||||
expect(meta).toBeDefined();
|
||||
|
||||
const app = { ...meta!, extensions: { ...meta!.extensions, addedLinks: [linkConfig] } };
|
||||
setAppPluginMetas({ [pluginId]: app });
|
||||
|
||||
wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<PluginContextProvider
|
||||
|
||||
@@ -24,7 +24,7 @@ export function usePluginLinks({
|
||||
}: UsePluginLinksOptions): UsePluginLinksResult {
|
||||
const registryItems = useAddedLinksRegistrySlice(extensionPointId);
|
||||
const pluginContext = usePluginContext();
|
||||
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExtensionPointPluginDependencies(extensionPointId));
|
||||
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(extensionPointId, getExtensionPointPluginDependencies);
|
||||
|
||||
return useMemo(() => {
|
||||
const { result, pointLog } = validateExtensionPoint({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import * as React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import {
|
||||
type AppPluginConfig,
|
||||
type PluginExtensionEventHelpers,
|
||||
type PluginExtensionOpenModalOptions,
|
||||
isDateTime,
|
||||
@@ -13,10 +14,9 @@ import {
|
||||
PanelMenuItem,
|
||||
PluginExtensionAddedLinkConfig,
|
||||
urlUtil,
|
||||
PluginExtensionPoints,
|
||||
ExtensionInfo,
|
||||
} from '@grafana/data';
|
||||
import { reportInteraction, config, AppPluginConfig } from '@grafana/runtime';
|
||||
import { reportInteraction, config } from '@grafana/runtime';
|
||||
import { Modal } from '@grafana/ui';
|
||||
import { appEvents } from 'app/core/app_events';
|
||||
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from 'app/types/events';
|
||||
|
||||
import { RestrictedGrafanaApisProvider } from '../components/restrictedGrafanaApis/RestrictedGrafanaApisProvider';
|
||||
import { PreloadAppPluginsPredicate } from '../pluginPreloader';
|
||||
|
||||
import { ExtensionErrorBoundary } from './ExtensionErrorBoundary';
|
||||
import { ExtensionsLog, log as baseLog } from './logs/log';
|
||||
@@ -609,9 +610,6 @@ export function getLinkExtensionPathWithTracking(pluginId: string, path: string,
|
||||
// Can be set with the `GF_DEFAULT_APP_MODE` environment variable
|
||||
export const isGrafanaDevMode = () => config.buildInfo.env === 'development';
|
||||
|
||||
export const getAppPluginConfigs = (pluginIds: string[] = []) =>
|
||||
Object.values(config.apps).filter((app) => pluginIds.includes(app.id));
|
||||
|
||||
export const getAppPluginIdFromExposedComponentId = (exposedComponentId: string) => {
|
||||
return exposedComponentId.split('/')[0];
|
||||
};
|
||||
@@ -619,8 +617,11 @@ export const getAppPluginIdFromExposedComponentId = (exposedComponentId: string)
|
||||
// Returns a list of app plugin ids that are registering extensions to this extension point.
|
||||
// (These plugins are necessary to be loaded to use the extension point.)
|
||||
// (The function also returns the plugin ids that the plugins - that extend the extension point - depend on.)
|
||||
export const getExtensionPointPluginDependencies = (extensionPointId: string): string[] => {
|
||||
return Object.values(config.apps)
|
||||
export const getExtensionPointPluginDependencies: PreloadAppPluginsPredicate = (
|
||||
apps: AppPluginConfig[],
|
||||
extensionPointId: string
|
||||
): string[] => {
|
||||
return apps
|
||||
.filter(
|
||||
(app) =>
|
||||
app.extensions.addedLinks.some((link) => link.targets.includes(extensionPointId)) ||
|
||||
@@ -628,7 +629,7 @@ export const getExtensionPointPluginDependencies = (extensionPointId: string): s
|
||||
)
|
||||
.map((app) => app.id)
|
||||
.reduce((acc: string[], id: string) => {
|
||||
return [...acc, id, ...getAppPluginDependencies(id)];
|
||||
return [...acc, id, ...getAppPluginDependencies(apps, id)];
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -645,11 +646,14 @@ export type ExtensionPointPluginMeta = Map<
|
||||
* @param extensionPointId - The id of the extension point.
|
||||
* @returns A map of plugin ids and their addedComponents and addedLinks to the extension point.
|
||||
*/
|
||||
export const getExtensionPointPluginMeta = (extensionPointId: string): ExtensionPointPluginMeta => {
|
||||
export const getExtensionPointPluginMeta = (
|
||||
apps: AppPluginConfig[],
|
||||
extensionPointId: string
|
||||
): ExtensionPointPluginMeta => {
|
||||
return new Map(
|
||||
getExtensionPointPluginDependencies(extensionPointId)
|
||||
getExtensionPointPluginDependencies(apps, extensionPointId)
|
||||
.map((pluginId) => {
|
||||
const app = config.apps[pluginId];
|
||||
const app = apps.find((a) => a.id === pluginId);
|
||||
// if the plugin does not exist or does not expose any components or links to the extension point, return undefined
|
||||
if (
|
||||
!app ||
|
||||
@@ -674,19 +678,27 @@ export const getExtensionPointPluginMeta = (extensionPointId: string): Extension
|
||||
|
||||
// Returns a list of app plugin ids that are necessary to be loaded to use the exposed component.
|
||||
// (It is first the plugin that exposes the component, and then the ones that it depends on.)
|
||||
export const getExposedComponentPluginDependencies = (exposedComponentId: string) => {
|
||||
export const getExposedComponentPluginDependencies: PreloadAppPluginsPredicate = (
|
||||
apps: AppPluginConfig[],
|
||||
exposedComponentId: string
|
||||
) => {
|
||||
const pluginId = getAppPluginIdFromExposedComponentId(exposedComponentId);
|
||||
|
||||
return [pluginId].reduce((acc: string[], pluginId: string) => {
|
||||
return [...acc, pluginId, ...getAppPluginDependencies(pluginId)];
|
||||
return [...acc, pluginId, ...getAppPluginDependencies(apps, pluginId)];
|
||||
}, []);
|
||||
};
|
||||
|
||||
// Returns a list of app plugin ids that are necessary to be loaded, based on the `dependencies.extensions`
|
||||
// metadata field. (For example the plugins that expose components that the app depends on.)
|
||||
// Heads up! This is a recursive function.
|
||||
export const getAppPluginDependencies = (pluginId: string, visited: string[] = []): string[] => {
|
||||
if (!config.apps[pluginId]) {
|
||||
export const getAppPluginDependencies = (
|
||||
apps: AppPluginConfig[],
|
||||
pluginId: string,
|
||||
visited: string[] = []
|
||||
): string[] => {
|
||||
const app = apps.find((a) => a.id === pluginId);
|
||||
if (!app) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -695,38 +707,14 @@ export const getAppPluginDependencies = (pluginId: string, visited: string[] = [
|
||||
return [];
|
||||
}
|
||||
|
||||
const pluginIdDependencies = config.apps[pluginId].dependencies.extensions.exposedComponents.map(
|
||||
getAppPluginIdFromExposedComponentId
|
||||
);
|
||||
const pluginIdDependencies = app.dependencies.extensions.exposedComponents.map(getAppPluginIdFromExposedComponentId);
|
||||
|
||||
return (
|
||||
pluginIdDependencies
|
||||
.reduce((acc, _pluginId) => {
|
||||
return [...acc, ...getAppPluginDependencies(_pluginId, [...visited, pluginId])];
|
||||
return [...acc, ...getAppPluginDependencies(apps, _pluginId, [...visited, pluginId])];
|
||||
}, pluginIdDependencies)
|
||||
// We don't want the plugin to "depend on itself"
|
||||
.filter((id) => id !== pluginId)
|
||||
);
|
||||
};
|
||||
|
||||
// Returns a list of app plugins that has to be loaded before core Grafana could finish the initialization.
|
||||
export const getAppPluginsToAwait = () => {
|
||||
const pluginIds = [
|
||||
// The "cloud-home-app" is registering banners once it's loaded, and this can cause a rerender in the AppChrome if it's loaded after the Grafana app init.
|
||||
'cloud-home-app',
|
||||
];
|
||||
|
||||
return Object.values(config.apps).filter((app) => pluginIds.includes(app.id));
|
||||
};
|
||||
|
||||
// Returns a list of app plugins that has to be preloaded in parallel with the core Grafana initialization.
|
||||
export const getAppPluginsToPreload = () => {
|
||||
// The DashboardPanelMenu extension point is using the `getPluginExtensions()` API in scenes at the moment, which means that it cannot yet benefit from dynamic plugin loading.
|
||||
const dashboardPanelMenuPluginIds = getExtensionPointPluginDependencies(PluginExtensionPoints.DashboardPanelMenu);
|
||||
const awaitedPluginIds = getAppPluginsToAwait().map((app) => app.id);
|
||||
const isNotAwaited = (app: AppPluginConfig) => !awaitedPluginIds.includes(app.id);
|
||||
|
||||
return Object.values(config.apps).filter((app) => {
|
||||
return isNotAwaited(app) && (app.preload || dashboardPanelMenuPluginIds.includes(app.id));
|
||||
});
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,8 @@ import {
|
||||
PluginExtensionPointPatterns,
|
||||
} from '@grafana/data';
|
||||
import { PluginAddedLinksConfigureFunc } from '@grafana/data/internal';
|
||||
import { config, isPluginExtensionLink } from '@grafana/runtime';
|
||||
import { isPluginExtensionLink } from '@grafana/runtime';
|
||||
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||
|
||||
import * as errors from './errors';
|
||||
import { ExtensionsLog } from './logs/log';
|
||||
@@ -145,13 +146,13 @@ export const isExposedComponentDependencyMissing = (id: string, pluginContext: P
|
||||
return !exposedComponentsDependencies || !exposedComponentsDependencies.includes(id);
|
||||
};
|
||||
|
||||
export const isAddedLinkMetaInfoMissing = (
|
||||
export const isAddedLinkMetaInfoMissing = async (
|
||||
pluginId: string,
|
||||
metaInfo: PluginExtensionAddedLinkConfig,
|
||||
log: ExtensionsLog
|
||||
) => {
|
||||
const logPrefix = 'Could not register link extension. Reason:';
|
||||
const app = config.apps[pluginId];
|
||||
const app = await getAppPluginMeta(pluginId);
|
||||
const pluginJsonMetaInfo = app ? app.extensions.addedLinks.filter(({ title }) => title === metaInfo.title) : null;
|
||||
|
||||
if (!app) {
|
||||
@@ -177,13 +178,13 @@ export const isAddedLinkMetaInfoMissing = (
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isAddedFunctionMetaInfoMissing = (
|
||||
export const isAddedFunctionMetaInfoMissing = async (
|
||||
pluginId: string,
|
||||
metaInfo: PluginExtensionAddedFunctionConfig,
|
||||
log: ExtensionsLog
|
||||
) => {
|
||||
const logPrefix = 'Could not register function extension. Reason:';
|
||||
const app = config.apps[pluginId];
|
||||
const app = await getAppPluginMeta(pluginId);
|
||||
const pluginJsonMetaInfo = app ? app.extensions.addedFunctions.filter(({ title }) => title === metaInfo.title) : null;
|
||||
|
||||
if (!app) {
|
||||
@@ -209,13 +210,13 @@ export const isAddedFunctionMetaInfoMissing = (
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isAddedComponentMetaInfoMissing = (
|
||||
export const isAddedComponentMetaInfoMissing = async (
|
||||
pluginId: string,
|
||||
metaInfo: PluginExtensionAddedComponentConfig,
|
||||
log: ExtensionsLog
|
||||
) => {
|
||||
const logPrefix = 'Could not register component extension. Reason:';
|
||||
const app = config.apps[pluginId];
|
||||
const app = await getAppPluginMeta(pluginId);
|
||||
const pluginJsonMetaInfo = app
|
||||
? app.extensions.addedComponents.filter(({ title }) => title === metaInfo.title)
|
||||
: null;
|
||||
@@ -243,13 +244,13 @@ export const isAddedComponentMetaInfoMissing = (
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isExposedComponentMetaInfoMissing = (
|
||||
export const isExposedComponentMetaInfoMissing = async (
|
||||
pluginId: string,
|
||||
metaInfo: PluginExtensionExposedComponentConfig,
|
||||
log: ExtensionsLog
|
||||
) => {
|
||||
const logPrefix = 'Could not register exposed component extension. Reason:';
|
||||
const app = config.apps[pluginId];
|
||||
const app = await getAppPluginMeta(pluginId);
|
||||
const pluginJsonMetaInfo = app ? app.extensions.exposedComponents.filter(({ id }) => id === metaInfo.id) : null;
|
||||
|
||||
if (!app) {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import type {
|
||||
AppPluginConfig,
|
||||
PluginExtensionAddedLinkConfig,
|
||||
PluginExtensionExposedComponentConfig,
|
||||
PluginExtensionAddedComponentConfig,
|
||||
import {
|
||||
type AppPluginConfig,
|
||||
type PluginExtensionAddedLinkConfig,
|
||||
type PluginExtensionExposedComponentConfig,
|
||||
type PluginExtensionAddedComponentConfig,
|
||||
PluginExtensionPoints,
|
||||
} from '@grafana/data';
|
||||
import { getAppPluginMetas } from '@grafana/runtime/unstable';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
||||
|
||||
import { getExtensionPointPluginDependencies } from './extensions/utils';
|
||||
import { pluginImporter } from './importer/pluginImporter';
|
||||
|
||||
export type PluginPreloadResult = {
|
||||
@@ -23,6 +26,59 @@ export const clearPreloadedPluginsCache = () => {
|
||||
preloadPromises.clear();
|
||||
};
|
||||
|
||||
function getAppPluginIdsToAwait() {
|
||||
const pluginIds = [
|
||||
// The "cloud-home-app" is registering banners once it's loaded, and this can cause a rerender in the AppChrome if it's loaded after the Grafana app init.
|
||||
'cloud-home-app',
|
||||
];
|
||||
|
||||
return pluginIds;
|
||||
}
|
||||
|
||||
function isNotAwaited(app: AppPluginConfig) {
|
||||
return !getAppPluginIdsToAwait().includes(app.id);
|
||||
}
|
||||
|
||||
export async function preloadPluginsToBeAwaited() {
|
||||
const apps = await getAppPluginMetas();
|
||||
const awaited = getAppPluginIdsToAwait();
|
||||
const filtered = apps.filter((app) => awaited.includes(app.id));
|
||||
|
||||
preloadPlugins(filtered);
|
||||
}
|
||||
|
||||
export async function preloadPluginsToBePreloaded() {
|
||||
const apps = await getAppPluginMetas();
|
||||
|
||||
// The DashboardPanelMenu extension point is using the `getPluginExtensions()` API in scenes at the moment, which means that it cannot yet benefit from dynamic plugin loading.
|
||||
const dashboardPanelMenuPluginIds = getExtensionPointPluginDependencies(
|
||||
apps,
|
||||
PluginExtensionPoints.DashboardPanelMenu
|
||||
);
|
||||
|
||||
const filtered = apps.filter((app) => {
|
||||
return isNotAwaited(app) && (app.preload || dashboardPanelMenuPluginIds.includes(app.id));
|
||||
});
|
||||
|
||||
preloadPlugins(filtered);
|
||||
}
|
||||
|
||||
export type PreloadAppPluginsPredicate = (apps: AppPluginConfig[], extensionId: string) => string[];
|
||||
|
||||
const noop: PreloadAppPluginsPredicate = () => [];
|
||||
|
||||
export async function preloadPluginsWithPredicate(extensionId: string, predicate: PreloadAppPluginsPredicate = noop) {
|
||||
const apps = await getAppPluginMetas();
|
||||
const filteredIds = predicate(apps, extensionId);
|
||||
const filtered = apps.filter((app) => filteredIds.includes(app.id));
|
||||
|
||||
if (!filtered.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
preloadPlugins(filtered);
|
||||
}
|
||||
|
||||
export async function preloadPlugins(apps: AppPluginConfig[] = []) {
|
||||
// Create preload promises for each app, reusing existing promises if already loading
|
||||
const promises = apps.map((app) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PluginType, patchArrayVectorProrotypeMethods } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { getAppPluginMeta } from '@grafana/runtime/unstable';
|
||||
|
||||
import { transformPluginSourceForCDN } from '../cdn/utils';
|
||||
import { resolvePluginUrlWithCache } from '../loader/pluginInfoCache';
|
||||
@@ -121,7 +122,7 @@ export function patchSandboxEnvironmentPrototype(sandboxEnvironment: SandboxEnvi
|
||||
);
|
||||
}
|
||||
|
||||
export function getPluginLoadData(pluginId: string): SandboxPluginMeta {
|
||||
export async function getPluginLoadData(pluginId: string): Promise<SandboxPluginMeta> {
|
||||
// find it in datasources
|
||||
for (const datasource of Object.values(config.datasources)) {
|
||||
if (datasource.type === pluginId) {
|
||||
@@ -138,16 +139,15 @@ export function getPluginLoadData(pluginId: string): SandboxPluginMeta {
|
||||
|
||||
//find it in apps
|
||||
//the information inside the apps object is more limited
|
||||
for (const app of Object.values(config.apps)) {
|
||||
if (app.id === pluginId) {
|
||||
return {
|
||||
id: pluginId,
|
||||
type: PluginType.app,
|
||||
module: app.path,
|
||||
moduleHash: app.moduleHash,
|
||||
};
|
||||
}
|
||||
const app = await getAppPluginMeta(pluginId);
|
||||
if (!app) {
|
||||
throw new Error(`Could not find plugin ${pluginId}`);
|
||||
}
|
||||
|
||||
throw new Error(`Could not find plugin ${pluginId}`);
|
||||
return {
|
||||
id: pluginId,
|
||||
type: PluginType.app,
|
||||
module: app.path,
|
||||
moduleHash: app.moduleHash,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ const pluginLogCache: Record<string, boolean> = {};
|
||||
export async function importPluginModuleInSandbox({ pluginId }: { pluginId: string }): Promise<System.Module> {
|
||||
patchWebAPIs();
|
||||
try {
|
||||
const pluginMeta = getPluginLoadData(pluginId);
|
||||
const pluginMeta = await getPluginLoadData(pluginId);
|
||||
if (!pluginImportCache.has(pluginId)) {
|
||||
pluginImportCache.set(pluginId, doImportPluginModuleInSandbox(pluginMeta));
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ const getSummaryColumns = () => [
|
||||
header: 'Unchanged',
|
||||
cell: ({ row: { original: item } }: SummaryCell) => item.noop?.toString() || '-',
|
||||
},
|
||||
{
|
||||
id: 'warnings',
|
||||
header: 'Warnings',
|
||||
cell: ({ row: { original: item } }: SummaryCell) => item.warning?.toString() || '-',
|
||||
},
|
||||
{
|
||||
id: 'errors',
|
||||
header: 'Errors',
|
||||
|
||||
@@ -911,7 +911,7 @@ const traceSubFrame = (
|
||||
subFrame.add(transformSpanToTraceData(span, spanSet, trace));
|
||||
});
|
||||
|
||||
return subFrame;
|
||||
return toDataFrame(subFrame);
|
||||
};
|
||||
|
||||
interface TraceTableData {
|
||||
|
||||
@@ -3739,6 +3739,10 @@
|
||||
"clear": "Vymazat vyhledávání a filtry",
|
||||
"text": "Nebyly nalezeny žádné výsledky pro váš dotaz"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "",
|
||||
"title": ""
|
||||
},
|
||||
"restore": {
|
||||
"success": "",
|
||||
"all-failed_one": "",
|
||||
@@ -5992,13 +5996,25 @@
|
||||
"title-error-loading-dashboard": "Chyba při načítání nástěnky"
|
||||
},
|
||||
"dashboard-scene": {
|
||||
"modal": {
|
||||
"cancel": "",
|
||||
"discard": "",
|
||||
"save": "",
|
||||
"text": {
|
||||
"save-changes-question": ""
|
||||
},
|
||||
"title": {
|
||||
"unsaved-changes": ""
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"edit-panel": "Upravit panel",
|
||||
"view-panel": "Zobrazit panel"
|
||||
},
|
||||
"title": {
|
||||
"dashboard": "Nástěnka",
|
||||
"discard-changes-to-dashboard": "Zahodit změny nástěnky?"
|
||||
"discard-changes-to-dashboard": "Zahodit změny nástěnky?",
|
||||
"unsaved-changes-question": ""
|
||||
}
|
||||
},
|
||||
"dashboard-scene-page-state-manager": {
|
||||
@@ -10798,7 +10814,6 @@
|
||||
"title": "Nové"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "",
|
||||
"title": "Nová nástěnka"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -11958,7 +11973,6 @@
|
||||
"title-setting-connection-could-cause-temporary-outage": "Nastavení tohoto připojení může způsobit dočasný výpadek"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"header": "Zajišťování",
|
||||
"subtitle-provisioning-feature": "Zobrazujte a spravujte vazby zajištění"
|
||||
},
|
||||
"git": {
|
||||
@@ -12730,7 +12744,6 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "",
|
||||
"import": "Importovat",
|
||||
"new": "Nové",
|
||||
"new-dashboard": "Nová nástěnka",
|
||||
|
||||
@@ -3707,6 +3707,10 @@
|
||||
"clear": "Suche und Filter löschen",
|
||||
"text": "Keine Ergebnisse für deine Abfrage gefunden"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "",
|
||||
"title": ""
|
||||
},
|
||||
"restore": {
|
||||
"success": "",
|
||||
"all-failed_one": "",
|
||||
@@ -5950,13 +5954,25 @@
|
||||
"title-error-loading-dashboard": "Fehler beim Laden des Dashboards"
|
||||
},
|
||||
"dashboard-scene": {
|
||||
"modal": {
|
||||
"cancel": "",
|
||||
"discard": "",
|
||||
"save": "",
|
||||
"text": {
|
||||
"save-changes-question": ""
|
||||
},
|
||||
"title": {
|
||||
"unsaved-changes": ""
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"edit-panel": "Panel bearbeiten",
|
||||
"view-panel": "Panel anzeigen"
|
||||
},
|
||||
"title": {
|
||||
"dashboard": "Dashboard",
|
||||
"discard-changes-to-dashboard": "Änderungen am Dashboard verwerfen?"
|
||||
"discard-changes-to-dashboard": "Änderungen am Dashboard verwerfen?",
|
||||
"unsaved-changes-question": ""
|
||||
}
|
||||
},
|
||||
"dashboard-scene-page-state-manager": {
|
||||
@@ -10712,7 +10728,6 @@
|
||||
"title": "Neu"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "",
|
||||
"title": "Neues Dashboard"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -11856,7 +11871,6 @@
|
||||
"title-setting-connection-could-cause-temporary-outage": "Das Einrichten dieser Verbindung kann zu einem vorübergehenden Ausfall führen"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"header": "Bereitstellung",
|
||||
"subtitle-provisioning-feature": "Sehen und verwalten Sie Ihre Bereitstellungsverbindungen"
|
||||
},
|
||||
"git": {
|
||||
@@ -12622,7 +12636,6 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "",
|
||||
"import": "Importieren",
|
||||
"new": "Neu",
|
||||
"new-dashboard": "Neues Dashboard",
|
||||
|
||||
@@ -3707,6 +3707,10 @@
|
||||
"clear": "Borrar la búsqueda y los filtros",
|
||||
"text": "No se han encontrado resultados para tu consulta"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "",
|
||||
"title": ""
|
||||
},
|
||||
"restore": {
|
||||
"success": "",
|
||||
"all-failed_one": "",
|
||||
@@ -5950,13 +5954,25 @@
|
||||
"title-error-loading-dashboard": "Error al cargar el panel de control"
|
||||
},
|
||||
"dashboard-scene": {
|
||||
"modal": {
|
||||
"cancel": "",
|
||||
"discard": "",
|
||||
"save": "",
|
||||
"text": {
|
||||
"save-changes-question": ""
|
||||
},
|
||||
"title": {
|
||||
"unsaved-changes": ""
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"edit-panel": "Editar panel",
|
||||
"view-panel": "Ver panel"
|
||||
},
|
||||
"title": {
|
||||
"dashboard": "Panel de control",
|
||||
"discard-changes-to-dashboard": "¿Descartar los cambios en el dashboard?"
|
||||
"discard-changes-to-dashboard": "¿Descartar los cambios en el dashboard?",
|
||||
"unsaved-changes-question": ""
|
||||
}
|
||||
},
|
||||
"dashboard-scene-page-state-manager": {
|
||||
@@ -10712,7 +10728,6 @@
|
||||
"title": "Nuevo"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "",
|
||||
"title": "Nuevo panel de control"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -11856,7 +11871,6 @@
|
||||
"title-setting-connection-could-cause-temporary-outage": "Configurar esta conexión podría causar una interrupción temporal"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"header": "Aprovisionamiento",
|
||||
"subtitle-provisioning-feature": "Ver y gestionar tus conexiones de aprovisionamiento"
|
||||
},
|
||||
"git": {
|
||||
@@ -12622,7 +12636,6 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "",
|
||||
"import": "Importar",
|
||||
"new": "Nuevo",
|
||||
"new-dashboard": "Nuevo panel de control",
|
||||
|
||||
@@ -3707,6 +3707,10 @@
|
||||
"clear": "Effacer la recherche et les filtres",
|
||||
"text": "Aucun résultat n'a été trouvé pour votre requête"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "",
|
||||
"title": ""
|
||||
},
|
||||
"restore": {
|
||||
"success": "",
|
||||
"all-failed_one": "",
|
||||
@@ -5950,13 +5954,25 @@
|
||||
"title-error-loading-dashboard": "Erreur lors du chargement du tableau de bord"
|
||||
},
|
||||
"dashboard-scene": {
|
||||
"modal": {
|
||||
"cancel": "",
|
||||
"discard": "",
|
||||
"save": "",
|
||||
"text": {
|
||||
"save-changes-question": ""
|
||||
},
|
||||
"title": {
|
||||
"unsaved-changes": ""
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"edit-panel": "Modifier le panneau",
|
||||
"view-panel": "Afficher le panneau"
|
||||
},
|
||||
"title": {
|
||||
"dashboard": "Tableau de bord",
|
||||
"discard-changes-to-dashboard": "Abandonner les modifications apportées au tableau de bord ?"
|
||||
"discard-changes-to-dashboard": "Abandonner les modifications apportées au tableau de bord ?",
|
||||
"unsaved-changes-question": ""
|
||||
}
|
||||
},
|
||||
"dashboard-scene-page-state-manager": {
|
||||
@@ -10712,7 +10728,6 @@
|
||||
"title": "Nouveau"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "",
|
||||
"title": "Nouveau tableau de bord"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -11856,7 +11871,6 @@
|
||||
"title-setting-connection-could-cause-temporary-outage": "La configuration de cette connexion peut entraîner une interruption temporaire"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"header": "Mise en service",
|
||||
"subtitle-provisioning-feature": "Afficher et gérer vos connexions de mise en service"
|
||||
},
|
||||
"git": {
|
||||
@@ -12622,7 +12636,6 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "",
|
||||
"import": "Importer",
|
||||
"new": "Nouveau",
|
||||
"new-dashboard": "Nouveau tableau de bord",
|
||||
|
||||
@@ -3707,6 +3707,10 @@
|
||||
"clear": "Keresés és szűrők törlése",
|
||||
"text": "Nincs találat a lekérdezésre"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "",
|
||||
"title": ""
|
||||
},
|
||||
"restore": {
|
||||
"success": "",
|
||||
"all-failed_one": "",
|
||||
@@ -5950,13 +5954,25 @@
|
||||
"title-error-loading-dashboard": "Hiba történt az irányítópult betöltésekor"
|
||||
},
|
||||
"dashboard-scene": {
|
||||
"modal": {
|
||||
"cancel": "",
|
||||
"discard": "",
|
||||
"save": "",
|
||||
"text": {
|
||||
"save-changes-question": ""
|
||||
},
|
||||
"title": {
|
||||
"unsaved-changes": ""
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"edit-panel": "Panel szerkesztése",
|
||||
"view-panel": "Panel megtekintése"
|
||||
},
|
||||
"title": {
|
||||
"dashboard": "Irányítópult",
|
||||
"discard-changes-to-dashboard": "Elveti az irányítópult módosításait?"
|
||||
"discard-changes-to-dashboard": "Elveti az irányítópult módosításait?",
|
||||
"unsaved-changes-question": ""
|
||||
}
|
||||
},
|
||||
"dashboard-scene-page-state-manager": {
|
||||
@@ -10712,7 +10728,6 @@
|
||||
"title": "Új"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "",
|
||||
"title": "Új irányítópult"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -11856,7 +11871,6 @@
|
||||
"title-setting-connection-could-cause-temporary-outage": "A kapcsolat létrehozása ideiglenes üzemszünetet okozhat"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"header": "Kiépítés",
|
||||
"subtitle-provisioning-feature": "Kiépítési kapcsolatok megtekintése és kezelése"
|
||||
},
|
||||
"git": {
|
||||
@@ -12622,7 +12636,6 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "",
|
||||
"import": "Importálás",
|
||||
"new": "Új",
|
||||
"new-dashboard": "Új irányítópult",
|
||||
|
||||
@@ -3691,6 +3691,10 @@
|
||||
"clear": "Hapus pencarian dan filter",
|
||||
"text": "Hasil untuk kueri Anda tidak ditemukan"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "",
|
||||
"title": ""
|
||||
},
|
||||
"restore": {
|
||||
"success": "",
|
||||
"all-failed_other": "",
|
||||
@@ -5929,13 +5933,25 @@
|
||||
"title-error-loading-dashboard": "Kesalahan saat memuat dasbor"
|
||||
},
|
||||
"dashboard-scene": {
|
||||
"modal": {
|
||||
"cancel": "",
|
||||
"discard": "",
|
||||
"save": "",
|
||||
"text": {
|
||||
"save-changes-question": ""
|
||||
},
|
||||
"title": {
|
||||
"unsaved-changes": ""
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"edit-panel": "Edit panel",
|
||||
"view-panel": "Lihat panel"
|
||||
},
|
||||
"title": {
|
||||
"dashboard": "Dasbor",
|
||||
"discard-changes-to-dashboard": "Batalkan perubahan ke dasbor?"
|
||||
"discard-changes-to-dashboard": "Batalkan perubahan ke dasbor?",
|
||||
"unsaved-changes-question": ""
|
||||
}
|
||||
},
|
||||
"dashboard-scene-page-state-manager": {
|
||||
@@ -10669,7 +10685,6 @@
|
||||
"title": "Baru"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "",
|
||||
"title": "Dasbor baru"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -11805,7 +11820,6 @@
|
||||
"title-setting-connection-could-cause-temporary-outage": "Mengatur koneksi ini dapat menyebabkan pemadaman sementara"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"header": "Penyediaan",
|
||||
"subtitle-provisioning-feature": "Lihat dan kelola koneksi penyediaan Anda"
|
||||
},
|
||||
"git": {
|
||||
@@ -12568,7 +12582,6 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "",
|
||||
"import": "Impor",
|
||||
"new": "Baru",
|
||||
"new-dashboard": "Dasbor baru",
|
||||
|
||||
@@ -3707,6 +3707,10 @@
|
||||
"clear": "Cancella ricerca e filtri",
|
||||
"text": "Nessun risultato trovato per la ricerca"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "",
|
||||
"title": ""
|
||||
},
|
||||
"restore": {
|
||||
"success": "",
|
||||
"all-failed_one": "",
|
||||
@@ -5950,13 +5954,25 @@
|
||||
"title-error-loading-dashboard": "Errore durante il caricamento del dashboard"
|
||||
},
|
||||
"dashboard-scene": {
|
||||
"modal": {
|
||||
"cancel": "",
|
||||
"discard": "",
|
||||
"save": "",
|
||||
"text": {
|
||||
"save-changes-question": ""
|
||||
},
|
||||
"title": {
|
||||
"unsaved-changes": ""
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"edit-panel": "Modifica pannello",
|
||||
"view-panel": "Visualizza pannello"
|
||||
},
|
||||
"title": {
|
||||
"dashboard": "Dashboard",
|
||||
"discard-changes-to-dashboard": "Annullare le modifiche alla dashboard?"
|
||||
"discard-changes-to-dashboard": "Annullare le modifiche alla dashboard?",
|
||||
"unsaved-changes-question": ""
|
||||
}
|
||||
},
|
||||
"dashboard-scene-page-state-manager": {
|
||||
@@ -10712,7 +10728,6 @@
|
||||
"title": "Nuovo"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "",
|
||||
"title": "Nuovo dashboard"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -11856,7 +11871,6 @@
|
||||
"title-setting-connection-could-cause-temporary-outage": "La configurazione di questa connessione potrebbe causare un'interruzione temporanea"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"header": "Provisioning",
|
||||
"subtitle-provisioning-feature": "Visualizza e gestisci le connessioni di provisioning"
|
||||
},
|
||||
"git": {
|
||||
@@ -12622,7 +12636,6 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "",
|
||||
"import": "Importa",
|
||||
"new": "Nuovo",
|
||||
"new-dashboard": "Nuovo dashboard",
|
||||
|
||||
@@ -3691,6 +3691,10 @@
|
||||
"clear": "検索とフィルタをクリア",
|
||||
"text": "クエリに一致する結果が見つかりませんでした。"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "",
|
||||
"title": ""
|
||||
},
|
||||
"restore": {
|
||||
"success": "",
|
||||
"all-failed_other": "",
|
||||
@@ -5929,13 +5933,25 @@
|
||||
"title-error-loading-dashboard": "ダッシュボードの読み込み中にエラーが発生しました"
|
||||
},
|
||||
"dashboard-scene": {
|
||||
"modal": {
|
||||
"cancel": "",
|
||||
"discard": "",
|
||||
"save": "",
|
||||
"text": {
|
||||
"save-changes-question": ""
|
||||
},
|
||||
"title": {
|
||||
"unsaved-changes": ""
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"edit-panel": "パネルを編集",
|
||||
"view-panel": "パネルを表示"
|
||||
},
|
||||
"title": {
|
||||
"dashboard": "ダッシュボード",
|
||||
"discard-changes-to-dashboard": "ダッシュボードへの変更を破棄しますか?"
|
||||
"discard-changes-to-dashboard": "ダッシュボードへの変更を破棄しますか?",
|
||||
"unsaved-changes-question": ""
|
||||
}
|
||||
},
|
||||
"dashboard-scene-page-state-manager": {
|
||||
@@ -10669,7 +10685,6 @@
|
||||
"title": "新規"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "",
|
||||
"title": "新しいダッシュボード"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -11805,7 +11820,6 @@
|
||||
"title-setting-connection-could-cause-temporary-outage": "この接続設定を行うことで、一時的に停止する可能性があります"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"header": "プロビジョニング",
|
||||
"subtitle-provisioning-feature": "プロビジョニング接続を表示・管理"
|
||||
},
|
||||
"git": {
|
||||
@@ -12568,7 +12582,6 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "",
|
||||
"import": "インポート",
|
||||
"new": "新規",
|
||||
"new-dashboard": "新しいダッシュボード",
|
||||
|
||||
@@ -3691,6 +3691,10 @@
|
||||
"clear": "검색 및 필터 초기화",
|
||||
"text": "쿼리에 대해 찾은 결과 없음"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "",
|
||||
"title": ""
|
||||
},
|
||||
"restore": {
|
||||
"success": "",
|
||||
"all-failed_other": "",
|
||||
@@ -5929,13 +5933,25 @@
|
||||
"title-error-loading-dashboard": "대시보드 로딩 중 오류 발생"
|
||||
},
|
||||
"dashboard-scene": {
|
||||
"modal": {
|
||||
"cancel": "",
|
||||
"discard": "",
|
||||
"save": "",
|
||||
"text": {
|
||||
"save-changes-question": ""
|
||||
},
|
||||
"title": {
|
||||
"unsaved-changes": ""
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"edit-panel": "패널 편집",
|
||||
"view-panel": "패널 보기"
|
||||
},
|
||||
"title": {
|
||||
"dashboard": "대시보드",
|
||||
"discard-changes-to-dashboard": "대시보드 변경 사항을 취소하시겠어요?"
|
||||
"discard-changes-to-dashboard": "대시보드 변경 사항을 취소하시겠어요?",
|
||||
"unsaved-changes-question": ""
|
||||
}
|
||||
},
|
||||
"dashboard-scene-page-state-manager": {
|
||||
@@ -10669,7 +10685,6 @@
|
||||
"title": "신규"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "",
|
||||
"title": "새 대시보드"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -11805,7 +11820,6 @@
|
||||
"title-setting-connection-could-cause-temporary-outage": "이 연결을 설정하면 일시적인 중단이 발생할 수 있습니다"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"header": "프로비저닝",
|
||||
"subtitle-provisioning-feature": "프로비저닝 연결 보기 및 관리"
|
||||
},
|
||||
"git": {
|
||||
@@ -12568,7 +12582,6 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "",
|
||||
"import": "가져오기",
|
||||
"new": "신규",
|
||||
"new-dashboard": "새 대시보드",
|
||||
|
||||
@@ -3707,6 +3707,10 @@
|
||||
"clear": "Zoekopdracht en filters wissen",
|
||||
"text": "Geen resultaten gevonden voor je zoekopdracht"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "",
|
||||
"title": ""
|
||||
},
|
||||
"restore": {
|
||||
"success": "",
|
||||
"all-failed_one": "",
|
||||
@@ -5950,13 +5954,25 @@
|
||||
"title-error-loading-dashboard": "Er is een fout opgetreden bij het laden van het dashboard"
|
||||
},
|
||||
"dashboard-scene": {
|
||||
"modal": {
|
||||
"cancel": "",
|
||||
"discard": "",
|
||||
"save": "",
|
||||
"text": {
|
||||
"save-changes-question": ""
|
||||
},
|
||||
"title": {
|
||||
"unsaved-changes": ""
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"edit-panel": "Paneel bewerken",
|
||||
"view-panel": "Paneel bekijken"
|
||||
},
|
||||
"title": {
|
||||
"dashboard": "Dashboard",
|
||||
"discard-changes-to-dashboard": "Wijzigingen in dashboard verwerpen?"
|
||||
"discard-changes-to-dashboard": "Wijzigingen in dashboard verwerpen?",
|
||||
"unsaved-changes-question": ""
|
||||
}
|
||||
},
|
||||
"dashboard-scene-page-state-manager": {
|
||||
@@ -10712,7 +10728,6 @@
|
||||
"title": "Nieuw"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "",
|
||||
"title": "Nieuw dashboard"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -11856,7 +11871,6 @@
|
||||
"title-setting-connection-could-cause-temporary-outage": "Het opzetten van deze verbinding kan een tijdelijke storing veroorzaken"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"header": "Provisioning",
|
||||
"subtitle-provisioning-feature": "Je provisioningverbindingen bekijken en beheren"
|
||||
},
|
||||
"git": {
|
||||
@@ -12622,7 +12636,6 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "",
|
||||
"import": "Importeren",
|
||||
"new": "Nieuw",
|
||||
"new-dashboard": "Nieuw dashboard",
|
||||
|
||||
@@ -3739,6 +3739,10 @@
|
||||
"clear": "Wyczyść wyszukiwanie i filtry",
|
||||
"text": "Nie znaleziono wyników dla tego zapytania"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "",
|
||||
"title": ""
|
||||
},
|
||||
"restore": {
|
||||
"success": "",
|
||||
"all-failed_one": "",
|
||||
@@ -5992,13 +5996,25 @@
|
||||
"title-error-loading-dashboard": "Błąd wczytywania pulpitu"
|
||||
},
|
||||
"dashboard-scene": {
|
||||
"modal": {
|
||||
"cancel": "",
|
||||
"discard": "",
|
||||
"save": "",
|
||||
"text": {
|
||||
"save-changes-question": ""
|
||||
},
|
||||
"title": {
|
||||
"unsaved-changes": ""
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"edit-panel": "Edytuj panel",
|
||||
"view-panel": "Wyświetl panel"
|
||||
},
|
||||
"title": {
|
||||
"dashboard": "Pulpit",
|
||||
"discard-changes-to-dashboard": "Odrzucić zmiany dotyczące pulpitu?"
|
||||
"discard-changes-to-dashboard": "Odrzucić zmiany dotyczące pulpitu?",
|
||||
"unsaved-changes-question": ""
|
||||
}
|
||||
},
|
||||
"dashboard-scene-page-state-manager": {
|
||||
@@ -10798,7 +10814,6 @@
|
||||
"title": "Nowy"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "",
|
||||
"title": "Nowy pulpit"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -11958,7 +11973,6 @@
|
||||
"title-setting-connection-could-cause-temporary-outage": "Skonfigurowanie tego połączenia może spowodować tymczasową niedostępność"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"header": "Konfiguracja",
|
||||
"subtitle-provisioning-feature": "Wyświetlaj połączenia aprowizacyjne i nimi zarządzaj"
|
||||
},
|
||||
"git": {
|
||||
@@ -12730,7 +12744,6 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "",
|
||||
"import": "Importuj",
|
||||
"new": "Nowy",
|
||||
"new-dashboard": "Nowy pulpit",
|
||||
|
||||
@@ -3707,6 +3707,10 @@
|
||||
"clear": "Limpar busca e filtros",
|
||||
"text": "Nenhum resultado encontrado para sua consulta"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "",
|
||||
"title": ""
|
||||
},
|
||||
"restore": {
|
||||
"success": "",
|
||||
"all-failed_one": "",
|
||||
@@ -5950,13 +5954,25 @@
|
||||
"title-error-loading-dashboard": "Erro ao carregar o painel de controle"
|
||||
},
|
||||
"dashboard-scene": {
|
||||
"modal": {
|
||||
"cancel": "",
|
||||
"discard": "",
|
||||
"save": "",
|
||||
"text": {
|
||||
"save-changes-question": ""
|
||||
},
|
||||
"title": {
|
||||
"unsaved-changes": ""
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"edit-panel": "Editar painel",
|
||||
"view-panel": "Visualizar painel"
|
||||
},
|
||||
"title": {
|
||||
"dashboard": "Painel de controle",
|
||||
"discard-changes-to-dashboard": "Deseja descartar as alterações no painel?"
|
||||
"discard-changes-to-dashboard": "Deseja descartar as alterações no painel?",
|
||||
"unsaved-changes-question": ""
|
||||
}
|
||||
},
|
||||
"dashboard-scene-page-state-manager": {
|
||||
@@ -10712,7 +10728,6 @@
|
||||
"title": "Novo"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "",
|
||||
"title": "Novo painel de controle"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -11856,7 +11871,6 @@
|
||||
"title-setting-connection-could-cause-temporary-outage": "Estabelecer esta conexão pode causar uma interrupção temporária"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"header": "Aprovisionamento",
|
||||
"subtitle-provisioning-feature": "Visualize e gerencie suas conexões de provisionamento"
|
||||
},
|
||||
"git": {
|
||||
@@ -12622,7 +12636,6 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "",
|
||||
"import": "Importar",
|
||||
"new": "Novo",
|
||||
"new-dashboard": "Novo painel de controle",
|
||||
|
||||
@@ -3707,6 +3707,10 @@
|
||||
"clear": "Limpar a pesquisa e os filtros",
|
||||
"text": "Não foram encontrados resultados para a sua consulta"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "",
|
||||
"title": ""
|
||||
},
|
||||
"restore": {
|
||||
"success": "",
|
||||
"all-failed_one": "",
|
||||
@@ -5950,13 +5954,25 @@
|
||||
"title-error-loading-dashboard": "Erro ao carregar o painel de controlo"
|
||||
},
|
||||
"dashboard-scene": {
|
||||
"modal": {
|
||||
"cancel": "",
|
||||
"discard": "",
|
||||
"save": "",
|
||||
"text": {
|
||||
"save-changes-question": ""
|
||||
},
|
||||
"title": {
|
||||
"unsaved-changes": ""
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"edit-panel": "Editar painel",
|
||||
"view-panel": "Visualizar painel"
|
||||
},
|
||||
"title": {
|
||||
"dashboard": "Painel de controlo",
|
||||
"discard-changes-to-dashboard": "Rejeitar alterações no painel de controlo?"
|
||||
"discard-changes-to-dashboard": "Rejeitar alterações no painel de controlo?",
|
||||
"unsaved-changes-question": ""
|
||||
}
|
||||
},
|
||||
"dashboard-scene-page-state-manager": {
|
||||
@@ -10712,7 +10728,6 @@
|
||||
"title": "Novo"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "",
|
||||
"title": "Novo painel de controlo"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -11856,7 +11871,6 @@
|
||||
"title-setting-connection-could-cause-temporary-outage": "Configurar esta ligação pode causar uma interrupção temporária"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"header": "Provisionamento",
|
||||
"subtitle-provisioning-feature": "Ver e gerir as suas ligações de provisionamento"
|
||||
},
|
||||
"git": {
|
||||
@@ -12622,7 +12636,6 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "",
|
||||
"import": "Importar",
|
||||
"new": "Novo",
|
||||
"new-dashboard": "Novo painel de controlo",
|
||||
|
||||
@@ -3739,6 +3739,10 @@
|
||||
"clear": "Очистить поиск и фильтры",
|
||||
"text": "По вашему запросу ничего не найдено"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "",
|
||||
"title": ""
|
||||
},
|
||||
"restore": {
|
||||
"success": "",
|
||||
"all-failed_one": "",
|
||||
@@ -5992,13 +5996,25 @@
|
||||
"title-error-loading-dashboard": "Ошибка при загрузке дашборда"
|
||||
},
|
||||
"dashboard-scene": {
|
||||
"modal": {
|
||||
"cancel": "",
|
||||
"discard": "",
|
||||
"save": "",
|
||||
"text": {
|
||||
"save-changes-question": ""
|
||||
},
|
||||
"title": {
|
||||
"unsaved-changes": ""
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"edit-panel": "Редактировать панель",
|
||||
"view-panel": "Просмотр панели"
|
||||
},
|
||||
"title": {
|
||||
"dashboard": "Дашборд",
|
||||
"discard-changes-to-dashboard": "Отменить изменения на дашборде?"
|
||||
"discard-changes-to-dashboard": "Отменить изменения на дашборде?",
|
||||
"unsaved-changes-question": ""
|
||||
}
|
||||
},
|
||||
"dashboard-scene-page-state-manager": {
|
||||
@@ -10798,7 +10814,6 @@
|
||||
"title": "Новые элементы"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "",
|
||||
"title": "Новый дашборд"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -11958,7 +11973,6 @@
|
||||
"title-setting-connection-could-cause-temporary-outage": "Настройка этого подключения может привести к временному сбою"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"header": "Подготовка к работе",
|
||||
"subtitle-provisioning-feature": "Просмотр подключений для подготовки и управлением ими"
|
||||
},
|
||||
"git": {
|
||||
@@ -12730,7 +12744,6 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "",
|
||||
"import": "Импорт",
|
||||
"new": "Новые элементы",
|
||||
"new-dashboard": "Новый дашборд",
|
||||
|
||||
@@ -3707,6 +3707,10 @@
|
||||
"clear": "Rensa sökning och filter",
|
||||
"text": "Inga resultat hittades för din fråga"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "",
|
||||
"title": ""
|
||||
},
|
||||
"restore": {
|
||||
"success": "",
|
||||
"all-failed_one": "",
|
||||
@@ -5950,13 +5954,25 @@
|
||||
"title-error-loading-dashboard": "Fel vid laddning av instrumentpanel"
|
||||
},
|
||||
"dashboard-scene": {
|
||||
"modal": {
|
||||
"cancel": "",
|
||||
"discard": "",
|
||||
"save": "",
|
||||
"text": {
|
||||
"save-changes-question": ""
|
||||
},
|
||||
"title": {
|
||||
"unsaved-changes": ""
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"edit-panel": "Redigera panel",
|
||||
"view-panel": "Visa panel"
|
||||
},
|
||||
"title": {
|
||||
"dashboard": "Instrumentpanel",
|
||||
"discard-changes-to-dashboard": "Kassera ändringar i instrumentpanelen?"
|
||||
"discard-changes-to-dashboard": "Kassera ändringar i instrumentpanelen?",
|
||||
"unsaved-changes-question": ""
|
||||
}
|
||||
},
|
||||
"dashboard-scene-page-state-manager": {
|
||||
@@ -10712,7 +10728,6 @@
|
||||
"title": "Nyhet"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "",
|
||||
"title": "Ny instrumentpanel"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -11856,7 +11871,6 @@
|
||||
"title-setting-connection-could-cause-temporary-outage": "Konfiguration av den här anslutningen kan orsaka ett tillfälligt avbrott"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"header": "Provisionering",
|
||||
"subtitle-provisioning-feature": "Visa och hantera dina provisioneringsanslutningar"
|
||||
},
|
||||
"git": {
|
||||
@@ -12622,7 +12636,6 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "",
|
||||
"import": "Importera",
|
||||
"new": "Nyhet",
|
||||
"new-dashboard": "Ny instrumentpanel",
|
||||
|
||||
@@ -3707,6 +3707,10 @@
|
||||
"clear": "Aramayı ve filtreleri temizle",
|
||||
"text": "Sorgunuz için sonuç bulunamadı"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "",
|
||||
"title": ""
|
||||
},
|
||||
"restore": {
|
||||
"success": "",
|
||||
"all-failed_one": "",
|
||||
@@ -5950,13 +5954,25 @@
|
||||
"title-error-loading-dashboard": "Pano yüklenirken hata oluştu"
|
||||
},
|
||||
"dashboard-scene": {
|
||||
"modal": {
|
||||
"cancel": "",
|
||||
"discard": "",
|
||||
"save": "",
|
||||
"text": {
|
||||
"save-changes-question": ""
|
||||
},
|
||||
"title": {
|
||||
"unsaved-changes": ""
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"edit-panel": "Paneli düzenle",
|
||||
"view-panel": "Paneli görüntüle"
|
||||
},
|
||||
"title": {
|
||||
"dashboard": "Pano",
|
||||
"discard-changes-to-dashboard": "Panodaki değişiklikler silinsin mi?"
|
||||
"discard-changes-to-dashboard": "Panodaki değişiklikler silinsin mi?",
|
||||
"unsaved-changes-question": ""
|
||||
}
|
||||
},
|
||||
"dashboard-scene-page-state-manager": {
|
||||
@@ -10712,7 +10728,6 @@
|
||||
"title": "Yeni"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "",
|
||||
"title": "Yeni pano"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -11856,7 +11871,6 @@
|
||||
"title-setting-connection-could-cause-temporary-outage": "Bu bağlantıyı kurmak geçici bir kesintiye neden olabilir"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"header": "Sağlama",
|
||||
"subtitle-provisioning-feature": "Sağlama bağlantılarınızı görüntüleyin ve yönetin"
|
||||
},
|
||||
"git": {
|
||||
@@ -12622,7 +12636,6 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "",
|
||||
"import": "İçe aktar",
|
||||
"new": "Yeni",
|
||||
"new-dashboard": "Yeni pano",
|
||||
|
||||
@@ -3691,6 +3691,10 @@
|
||||
"clear": "清除搜索和筛选条件",
|
||||
"text": "未找到与您的查询相关的结果"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "",
|
||||
"title": ""
|
||||
},
|
||||
"restore": {
|
||||
"success": "",
|
||||
"all-failed_other": "",
|
||||
@@ -5929,13 +5933,25 @@
|
||||
"title-error-loading-dashboard": "加载数据面板时出错"
|
||||
},
|
||||
"dashboard-scene": {
|
||||
"modal": {
|
||||
"cancel": "",
|
||||
"discard": "",
|
||||
"save": "",
|
||||
"text": {
|
||||
"save-changes-question": ""
|
||||
},
|
||||
"title": {
|
||||
"unsaved-changes": ""
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"edit-panel": "编辑面板",
|
||||
"view-panel": "查看面板"
|
||||
},
|
||||
"title": {
|
||||
"dashboard": "仪表板",
|
||||
"discard-changes-to-dashboard": "放弃对数据面板的更改?"
|
||||
"discard-changes-to-dashboard": "放弃对数据面板的更改?",
|
||||
"unsaved-changes-question": ""
|
||||
}
|
||||
},
|
||||
"dashboard-scene-page-state-manager": {
|
||||
@@ -10669,7 +10685,6 @@
|
||||
"title": "新建"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "",
|
||||
"title": "新建仪表板"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -11805,7 +11820,6 @@
|
||||
"title-setting-connection-could-cause-temporary-outage": "设置此连接可能会导致暂时中断"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"header": "配置",
|
||||
"subtitle-provisioning-feature": "查看和管理您的预配连接"
|
||||
},
|
||||
"git": {
|
||||
@@ -12568,7 +12582,6 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "",
|
||||
"import": "导入",
|
||||
"new": "新建",
|
||||
"new-dashboard": "新建仪表板",
|
||||
|
||||
@@ -3691,6 +3691,10 @@
|
||||
"clear": "清除搜尋和篩選條件",
|
||||
"text": "未找到您的查詢結果"
|
||||
},
|
||||
"recently-viewed": {
|
||||
"empty": "",
|
||||
"title": ""
|
||||
},
|
||||
"restore": {
|
||||
"success": "",
|
||||
"all-failed_other": "",
|
||||
@@ -5929,13 +5933,25 @@
|
||||
"title-error-loading-dashboard": "載入控制面板發生錯誤"
|
||||
},
|
||||
"dashboard-scene": {
|
||||
"modal": {
|
||||
"cancel": "",
|
||||
"discard": "",
|
||||
"save": "",
|
||||
"text": {
|
||||
"save-changes-question": ""
|
||||
},
|
||||
"title": {
|
||||
"unsaved-changes": ""
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"edit-panel": "編輯面板",
|
||||
"view-panel": "檢視面板"
|
||||
},
|
||||
"title": {
|
||||
"dashboard": "儀表板",
|
||||
"discard-changes-to-dashboard": "要捨棄儀表板的變更嗎?"
|
||||
"discard-changes-to-dashboard": "要捨棄儀表板的變更嗎?",
|
||||
"unsaved-changes-question": ""
|
||||
}
|
||||
},
|
||||
"dashboard-scene-page-state-manager": {
|
||||
@@ -10669,7 +10685,6 @@
|
||||
"title": "新"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "",
|
||||
"title": "新儀表板"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -11805,7 +11820,6 @@
|
||||
"title-setting-connection-could-cause-temporary-outage": "設定此連線可能會導致暫時中斷"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"header": "佈建",
|
||||
"subtitle-provisioning-feature": "檢視及管理您的佈建連線"
|
||||
},
|
||||
"git": {
|
||||
@@ -12568,7 +12582,6 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "",
|
||||
"import": "匯入",
|
||||
"new": "新",
|
||||
"new-dashboard": "新儀表板",
|
||||
|
||||
Reference in New Issue
Block a user