Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7bd7f4f72 | |||
| c47c360fd9 | |||
| 62cab8bd63 | |||
| 1e031db607 | |||
| 172f1fb974 | |||
| a716549f36 | |||
| e5c1de390d | |||
| 20f17d72c3 | |||
| a3d7bd8dca | |||
| 074e8ce128 | |||
| 4149767391 | |||
| 0c49337205 | |||
| c5345498b1 | |||
| 1bcccd5e61 | |||
| 12b38d1b7a | |||
| 359d097154 | |||
| cfc5d96c34 |
@@ -211,6 +211,12 @@ type ScopeNavigationSpec struct {
|
||||
Scope string `json:"scope"`
|
||||
// Used to navigate to a sub-scope of the main scope. URL will not be used if this is set.
|
||||
SubScope string `json:"subScope,omitempty"`
|
||||
// Preload the subscope children, as soon as the ScopeNavigation is loaded.
|
||||
PreLoadSubScopeChildren bool `json:"preLoadSubScopeChildren,omitempty"`
|
||||
// Expands to display the subscope children when the ScopeNavigation is loaded.
|
||||
ExpandOnLoad bool `json:"expandOnLoad,omitempty"`
|
||||
// Makes the subscope not selectable, only serving as a way to build the tree.
|
||||
DisableSubScopeSelection bool `json:"disableSubScopeSelection,omitempty"`
|
||||
}
|
||||
|
||||
// Type of the item.
|
||||
|
||||
@@ -642,6 +642,27 @@ func schema_pkg_apis_scope_v0alpha1_ScopeNavigationSpec(ref common.ReferenceCall
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"preLoadSubScopeChildren": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Preload the subscope children, as soon as the ScopeNavigation is loaded.",
|
||||
Type: []string{"boolean"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"expandOnLoad": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Expands to display the subscope children when the ScopeNavigation is loaded.",
|
||||
Type: []string{"boolean"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"disableSubScopeSelection": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Makes the subscope not selectable, only serving as a way to build the tree.",
|
||||
Type: []string{"boolean"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"url", "scope"},
|
||||
},
|
||||
|
||||
@@ -107,8 +107,8 @@ Here is an overview of version support through 2026:
|
||||
| 12.0.x | May 5, 2025 | February 5, 2026 | Patch Support |
|
||||
| 12.1.x | July 22, 2025 | April 22, 2026 | Patch Support |
|
||||
| 12.2.x | September 23, 2025 | June 23, 2026 | Patch Support |
|
||||
| 12.3.x | November 18, 2025 | August 18, 2026 | Yet to be released |
|
||||
| 12.4.x (Last minor of 12) | February 24, 2026 | November 24, 2026 | Yet to be released |
|
||||
| 12.3.x | November 19, 2025 | August 19, 2026 | Patch Support |
|
||||
| 12.4.x (Last minor of 12) | February 24, 2026 | May 24, 2027 | Yet to be released |
|
||||
| 13.0.0 | TBD | TBD | Yet to be released |
|
||||
|
||||
## How are these versions supported?
|
||||
|
||||
@@ -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
|
||||
@@ -1835,11 +1830,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/pages/DashboardScenePage.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 2
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -16,17 +16,22 @@ interface Props {
|
||||
title?: string;
|
||||
offset?: number;
|
||||
dragClass?: string;
|
||||
onDragStart?: (event: React.PointerEvent<HTMLDivElement>) => void;
|
||||
onOpenMenu?: () => void;
|
||||
}
|
||||
|
||||
export function HoverWidget({ menu, title, dragClass, children, offset = -32, onOpenMenu }: Props) {
|
||||
export function HoverWidget({ menu, title, dragClass, children, offset = -32, onOpenMenu, onDragStart }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const draggableRef = useRef<HTMLDivElement>(null);
|
||||
const selectors = e2eSelectors.components.Panels.Panel.HoverWidget;
|
||||
// Capture the pointer to keep the widget visible while dragging
|
||||
const onPointerDown = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
||||
draggableRef.current?.setPointerCapture(e.pointerId);
|
||||
}, []);
|
||||
const onPointerDown = useCallback(
|
||||
(e: React.PointerEvent<HTMLDivElement>) => {
|
||||
draggableRef.current?.setPointerCapture(e.pointerId);
|
||||
onDragStart?.(e);
|
||||
},
|
||||
[onDragStart]
|
||||
);
|
||||
|
||||
const onPointerUp = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
||||
draggableRef.current?.releasePointerCapture(e.pointerId);
|
||||
|
||||
@@ -384,6 +384,7 @@ export function PanelChrome({
|
||||
menu={menu}
|
||||
title={typeof title === 'string' ? title : undefined}
|
||||
dragClass={dragClass}
|
||||
onDragStart={onDragStart}
|
||||
offset={hoverHeaderOffset}
|
||||
onOpenMenu={onOpenMenu}
|
||||
>
|
||||
|
||||
@@ -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 ---------------------------- */
|
||||
|
||||
@@ -154,9 +154,12 @@ func TestJobProgressRecorderWarningStatus(t *testing.T) {
|
||||
// Verify the final status includes warnings
|
||||
require.NotNil(t, finalStatus.Warnings)
|
||||
assert.Len(t, finalStatus.Warnings, 3)
|
||||
assert.Contains(t, finalStatus.Warnings[0], "deprecated API used")
|
||||
assert.Contains(t, finalStatus.Warnings[1], "missing optional field")
|
||||
assert.Contains(t, finalStatus.Warnings[2], "validation warning")
|
||||
expectedWarnings := []string{
|
||||
"deprecated API used (file: dashboards/test.json, name: test-resource, action: updated)",
|
||||
"missing optional field (file: dashboards/test2.json, name: test-resource-2, action: created)",
|
||||
"validation warning (file: datasources/test.yaml, name: test-resource-3, action: created)",
|
||||
}
|
||||
assert.ElementsMatch(t, finalStatus.Warnings, expectedWarnings)
|
||||
|
||||
// Verify the state is set to Warning
|
||||
assert.Equal(t, provisioning.JobStateWarning, finalStatus.State)
|
||||
|
||||
@@ -224,4 +224,46 @@ func testCheck(t *testing.T, server *Server) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, true, res.GetAllowed())
|
||||
})
|
||||
|
||||
t.Run("user:19 should be able to view folder 4 and all subfolders", func(t *testing.T) {
|
||||
res, err := server.Check(newContextWithNamespace(), newReq("user:19", utils.VerbGet, folderGroup, folderResource, "", "", "4"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
|
||||
res, err = server.Check(newContextWithNamespace(), newReq("user:19", utils.VerbGet, folderGroup, folderResource, "", "", "5"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
|
||||
res, err = server.Check(newContextWithNamespace(), newReq("user:19", utils.VerbGet, folderGroup, folderResource, "", "", "6"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
})
|
||||
|
||||
t.Run("user:19 should be able to edit folder 4 and all subfolders", func(t *testing.T) {
|
||||
res, err := server.Check(newContextWithNamespace(), newReq("user:19", utils.VerbDelete, folderGroup, folderResource, "", "", "4"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
|
||||
res, err = server.Check(newContextWithNamespace(), newReq("user:19", utils.VerbDelete, folderGroup, folderResource, "", "", "5"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
|
||||
res, err = server.Check(newContextWithNamespace(), newReq("user:19", utils.VerbDelete, folderGroup, folderResource, "", "", "6"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
})
|
||||
|
||||
t.Run("user:20 should be able to view folder 4 and all subfolders", func(t *testing.T) {
|
||||
res, err := server.Check(newContextWithNamespace(), newReq("user:20", utils.VerbGet, folderGroup, folderResource, "", "", "4"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
|
||||
res, err = server.Check(newContextWithNamespace(), newReq("user:20", utils.VerbGet, folderGroup, folderResource, "", "", "5"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
|
||||
res, err = server.Check(newContextWithNamespace(), newReq("user:20", utils.VerbGet, folderGroup, folderResource, "", "", "6"))
|
||||
require.NoError(t, err)
|
||||
assert.True(t, res.GetAllowed())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -73,6 +73,8 @@ func setup(t *testing.T, srv *Server) *Server {
|
||||
common.NewFolderTuple("user:17", common.RelationSetView, "4"),
|
||||
common.NewFolderTuple("user:18", common.RelationCreate, "general"),
|
||||
common.NewFolderResourceTuple("user:18", common.RelationCreate, dashboardGroup, dashboardResource, "", "general"),
|
||||
common.NewFolderTuple("user:19", common.RelationSetAdmin, "4"),
|
||||
common.NewFolderTuple("user:20", common.RelationSetEdit, "4"),
|
||||
}
|
||||
|
||||
return setupOpenFGADatabase(t, srv, tuples)
|
||||
|
||||
@@ -274,6 +274,11 @@ func (s *Service) listDashboardVersionsThroughK8s(
|
||||
continueToken = tempOut.GetContinue()
|
||||
}
|
||||
|
||||
// Update the continue token on the response to reflect the actual position after all fetched items.
|
||||
// Without this, the response would return the token from the first fetch, causing duplicate items
|
||||
// on subsequent pages when multiple fetches were needed to fill the requested limit.
|
||||
out.SetContinue(continueToken)
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -268,6 +268,58 @@ func TestListDashboardVersions(t *testing.T) {
|
||||
}}}, res)
|
||||
})
|
||||
|
||||
t.Run("List returns continue token when first fetch satisfies limit with more pages", func(t *testing.T) {
|
||||
dashboardService := dashboards.NewFakeDashboardService(t)
|
||||
dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()}
|
||||
mockCli := new(client.MockK8sHandler)
|
||||
dashboardVersionService.k8sclient = mockCli
|
||||
dashboardVersionService.features = featuremgmt.WithFeatures()
|
||||
|
||||
dashboardService.On("GetDashboardUIDByID", mock.Anything,
|
||||
mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).
|
||||
Return(&dashboards.DashboardRef{UID: "uid"}, nil)
|
||||
query := dashver.ListDashboardVersionsQuery{DashboardID: 42, Limit: 2}
|
||||
mockCli.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
|
||||
|
||||
firstPage := &unstructured.UnstructuredList{
|
||||
Items: []unstructured.Unstructured{
|
||||
{Object: map[string]any{
|
||||
"metadata": map[string]any{
|
||||
"name": "uid",
|
||||
"resourceVersion": "11",
|
||||
"generation": int64(4),
|
||||
"labels": map[string]any{
|
||||
utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck
|
||||
},
|
||||
},
|
||||
"spec": map[string]any{},
|
||||
}},
|
||||
{Object: map[string]any{
|
||||
"metadata": map[string]any{
|
||||
"name": "uid",
|
||||
"resourceVersion": "12",
|
||||
"generation": int64(5),
|
||||
"labels": map[string]any{
|
||||
utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck
|
||||
},
|
||||
},
|
||||
"spec": map[string]any{},
|
||||
}},
|
||||
},
|
||||
}
|
||||
firstMeta, err := meta.ListAccessor(firstPage)
|
||||
require.NoError(t, err)
|
||||
firstMeta.SetContinue("t1") // More pages exist
|
||||
|
||||
mockCli.On("List", mock.Anything, mock.Anything, mock.Anything).Return(firstPage, nil).Once()
|
||||
|
||||
res, err := dashboardVersionService.List(context.Background(), &query)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 2, len(res.Versions))
|
||||
require.Equal(t, "t1", res.ContinueToken) // Token from first fetch when limit is satisfied
|
||||
mockCli.AssertNumberOfCalls(t, "List", 1) // Only one fetch needed
|
||||
})
|
||||
|
||||
t.Run("List returns correct continue token across multiple pages", func(t *testing.T) {
|
||||
dashboardService := dashboards.NewFakeDashboardService(t)
|
||||
dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()}
|
||||
@@ -333,7 +385,79 @@ func TestListDashboardVersions(t *testing.T) {
|
||||
res, err := dashboardVersionService.List(context.Background(), &query)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 3, len(res.Versions))
|
||||
require.Equal(t, "t1", res.ContinueToken) // Implementation returns continue token from first page
|
||||
require.Equal(t, "", res.ContinueToken) // Should return token from last fetch (empty = no more pages)
|
||||
mockCli.AssertNumberOfCalls(t, "List", 2)
|
||||
})
|
||||
|
||||
t.Run("List returns continue token from last fetch when more pages exist", func(t *testing.T) {
|
||||
dashboardService := dashboards.NewFakeDashboardService(t)
|
||||
dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()}
|
||||
mockCli := new(client.MockK8sHandler)
|
||||
dashboardVersionService.k8sclient = mockCli
|
||||
dashboardVersionService.features = featuremgmt.WithFeatures()
|
||||
|
||||
dashboardService.On("GetDashboardUIDByID", mock.Anything,
|
||||
mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).
|
||||
Return(&dashboards.DashboardRef{UID: "uid"}, nil)
|
||||
query := dashver.ListDashboardVersionsQuery{DashboardID: 42, Limit: 3}
|
||||
mockCli.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
|
||||
|
||||
firstPage := &unstructured.UnstructuredList{
|
||||
Items: []unstructured.Unstructured{
|
||||
{Object: map[string]any{
|
||||
"metadata": map[string]any{
|
||||
"name": "uid",
|
||||
"resourceVersion": "11",
|
||||
"generation": int64(4),
|
||||
"labels": map[string]any{
|
||||
utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck
|
||||
},
|
||||
},
|
||||
"spec": map[string]any{},
|
||||
}},
|
||||
{Object: map[string]any{
|
||||
"metadata": map[string]any{
|
||||
"name": "uid",
|
||||
"resourceVersion": "12",
|
||||
"generation": int64(5),
|
||||
"labels": map[string]any{
|
||||
utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck
|
||||
},
|
||||
},
|
||||
"spec": map[string]any{},
|
||||
}},
|
||||
},
|
||||
}
|
||||
firstMeta, err := meta.ListAccessor(firstPage)
|
||||
require.NoError(t, err)
|
||||
firstMeta.SetContinue("t1")
|
||||
|
||||
secondPage := &unstructured.UnstructuredList{
|
||||
Items: []unstructured.Unstructured{
|
||||
{Object: map[string]any{
|
||||
"metadata": map[string]any{
|
||||
"name": "uid",
|
||||
"resourceVersion": "13",
|
||||
"generation": int64(6),
|
||||
"labels": map[string]any{
|
||||
utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck
|
||||
},
|
||||
},
|
||||
"spec": map[string]any{},
|
||||
}},
|
||||
},
|
||||
}
|
||||
secondMeta, err := meta.ListAccessor(secondPage)
|
||||
require.NoError(t, err)
|
||||
secondMeta.SetContinue("t2") // More pages exist
|
||||
|
||||
mockCli.On("List", mock.Anything, mock.Anything, mock.Anything).Return(firstPage, nil).Once()
|
||||
mockCli.On("List", mock.Anything, mock.Anything, mock.Anything).Return(secondPage, nil).Once()
|
||||
|
||||
res, err := dashboardVersionService.List(context.Background(), &query)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 3, len(res.Versions))
|
||||
require.Equal(t, "t2", res.ContinueToken) // Must return token from LAST fetch, not first
|
||||
mockCli.AssertNumberOfCalls(t, "List", 2)
|
||||
})
|
||||
|
||||
|
||||
@@ -280,7 +280,15 @@ func (s *Service) handleTagValues(rw http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
tempoPath := fmt.Sprintf("api/v2/search/tag/%s/values", encodedTag)
|
||||
// escape tag
|
||||
tag, err := url.PathUnescape(encodedTag)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to unescape", "error", err, "tag", encodedTag)
|
||||
http.Error(rw, "Invalid 'tag' parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
tempoPath := fmt.Sprintf("api/v2/search/tag/%s/values", tag)
|
||||
s.proxyToTempo(rw, req, tempoPath)
|
||||
}
|
||||
|
||||
|
||||
@@ -51,11 +51,16 @@ function DashboardOutlineNode({ sceneObject, editPane, isEditing, depth, index }
|
||||
|
||||
const noTitleText = t('dashboard.outline.tree-item.no-title', '<no title>');
|
||||
|
||||
const children = editableElement.getOutlineChildren?.(isEditing) ?? [];
|
||||
const elementInfo = editableElement.getEditableElementInfo();
|
||||
const instanceName = elementInfo.instanceName === '' ? noTitleText : elementInfo.instanceName;
|
||||
const outlineRename = useOutlineRename(editableElement, isEditing);
|
||||
const isContainer = editableElement.getOutlineChildren ? true : false;
|
||||
const visibleChildren = useMemo(() => {
|
||||
const children = editableElement.getOutlineChildren?.(isEditing) ?? [];
|
||||
return isEditing
|
||||
? children
|
||||
: children.filter((child) => !getEditableElementFor(child)?.getEditableElementInfo().isHidden);
|
||||
}, [editableElement, isEditing]);
|
||||
|
||||
const onNodeClicked = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
@@ -74,6 +79,10 @@ function DashboardOutlineNode({ sceneObject, editPane, isEditing, depth, index }
|
||||
setIsCollapsed(!isCollapsed);
|
||||
};
|
||||
|
||||
if (elementInfo.isHidden && !isEditing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
// todo: add proper keyboard navigation
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
||||
@@ -130,8 +139,8 @@ function DashboardOutlineNode({ sceneObject, editPane, isEditing, depth, index }
|
||||
|
||||
{isContainer && !isCollapsed && (
|
||||
<ul className={styles.nodeChildren} role="group">
|
||||
{children.length > 0 ? (
|
||||
children.map((child, i) => (
|
||||
{visibleChildren.length > 0 ? (
|
||||
visibleChildren.map((child, i) => (
|
||||
<DashboardOutlineNode
|
||||
key={child.state.key}
|
||||
sceneObject={child}
|
||||
|
||||
@@ -190,7 +190,7 @@ describe('InspectJsonTab', () => {
|
||||
expect(obj.kind).toEqual('Panel');
|
||||
expect(obj.spec.id).toEqual(12);
|
||||
expect(obj.spec.data.kind).toEqual('QueryGroup');
|
||||
expect(tab.isEditable()).toBe(false);
|
||||
expect(tab.isEditable()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { LibraryPanel } from '@grafana/schema/';
|
||||
import { Button, CodeEditor, Field, Select, useStyles2 } from '@grafana/ui';
|
||||
import { Alert, Button, CodeEditor, Field, Select, useStyles2 } from '@grafana/ui';
|
||||
import { isDashboardV2Spec } from 'app/features/dashboard/api/utils';
|
||||
import { getPanelDataFrames } from 'app/features/dashboard/components/HelpWizard/utils';
|
||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||
@@ -27,6 +27,7 @@ import { getPrettyJSON } from 'app/features/inspector/utils/utils';
|
||||
import { reportPanelInspectInteraction } from 'app/features/search/page/reporting';
|
||||
|
||||
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
|
||||
import { buildVizPanel } from '../serialization/layoutSerializers/utils';
|
||||
import { buildGridItemForPanel } from '../serialization/transformSaveModelToScene';
|
||||
import { gridItemToPanel, vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
|
||||
import { vizPanelToSchemaV2 } from '../serialization/transformSceneToSaveModelSchemaV2';
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
getQueryRunnerFor,
|
||||
isLibraryPanel,
|
||||
} from '../utils/utils';
|
||||
import { isPanelKindV2 } from '../v2schema/validation';
|
||||
|
||||
export type ShowContent = 'panel-json' | 'panel-data' | 'data-frames';
|
||||
|
||||
@@ -45,6 +47,7 @@ export interface InspectJsonTabState extends SceneObjectState {
|
||||
source: ShowContent;
|
||||
jsonText: string;
|
||||
onClose: () => void;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class InspectJsonTab extends SceneObjectBase<InspectJsonTabState> {
|
||||
@@ -102,38 +105,77 @@ export class InspectJsonTab extends SceneObjectBase<InspectJsonTabState> {
|
||||
}
|
||||
|
||||
public onChangeSource = (value: SelectableValue<ShowContent>) => {
|
||||
this.setState({ source: value.value!, jsonText: getJsonText(value.value!, this.state.panelRef.resolve()) });
|
||||
this.setState({
|
||||
source: value.value!,
|
||||
jsonText: getJsonText(value.value!, this.state.panelRef.resolve()),
|
||||
error: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
public onApplyChange = () => {
|
||||
const panel = this.state.panelRef.resolve();
|
||||
const dashboard = getDashboardSceneFor(panel);
|
||||
const jsonObj = JSON.parse(this.state.jsonText);
|
||||
|
||||
const panelModel = new PanelModel(jsonObj);
|
||||
const gridItem = buildGridItemForPanel(panelModel);
|
||||
const newState = sceneUtils.cloneSceneObjectState(gridItem.state);
|
||||
|
||||
if (!(panel.parent instanceof DashboardGridItem)) {
|
||||
console.error('Cannot update state of panel', panel, gridItem);
|
||||
let jsonObj: unknown;
|
||||
try {
|
||||
jsonObj = JSON.parse(this.state.jsonText);
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
error: t('dashboard-scene.inspect-json-tab.error-invalid-json', 'Invalid JSON'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.onClose();
|
||||
if (isDashboardV2Spec(dashboard.getSaveModel())) {
|
||||
if (!isPanelKindV2(jsonObj)) {
|
||||
this.setState({
|
||||
error: t(
|
||||
'dashboard-scene.inspect-json-tab.error-invalid-v2-panel',
|
||||
'Panel JSON did not pass validation. Please check the JSON and try again.'
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const vizPanel = buildVizPanel(jsonObj, jsonObj.spec.id);
|
||||
|
||||
if (!dashboard.state.isEditing) {
|
||||
dashboard.onEnterEditMode();
|
||||
if (!dashboard.state.isEditing) {
|
||||
dashboard.onEnterEditMode();
|
||||
}
|
||||
|
||||
reportPanelInspectInteraction(InspectTab.JSON, 'apply', {
|
||||
panel_type_changed: panel.state.pluginId !== jsonObj.spec.vizConfig.group,
|
||||
panel_id_changed: getPanelIdForVizPanel(panel) !== jsonObj.spec.id,
|
||||
panel_grid_pos_changed: false, // Grid cant be edited from inspect in v2 panels.
|
||||
panel_targets_changed: hasQueriesChanged(getQueryRunnerFor(panel), getQueryRunnerFor(vizPanel.state.$data)),
|
||||
});
|
||||
|
||||
panel.setState(vizPanel.state);
|
||||
this.state.onClose();
|
||||
} else {
|
||||
const panelModel = new PanelModel(jsonObj);
|
||||
const gridItem = buildGridItemForPanel(panelModel);
|
||||
const newState = sceneUtils.cloneSceneObjectState(gridItem.state);
|
||||
|
||||
if (!(panel.parent instanceof DashboardGridItem)) {
|
||||
console.error('Cannot update state of panel', panel, gridItem);
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.onClose();
|
||||
|
||||
if (!dashboard.state.isEditing) {
|
||||
dashboard.onEnterEditMode();
|
||||
}
|
||||
|
||||
panel.parent.setState(newState);
|
||||
|
||||
//Report relevant updates
|
||||
reportPanelInspectInteraction(InspectTab.JSON, 'apply', {
|
||||
panel_type_changed: panel.state.pluginId !== panelModel.type,
|
||||
panel_id_changed: getPanelIdForVizPanel(panel) !== panelModel.id,
|
||||
panel_grid_pos_changed: hasGridPosChanged(panel.parent.state, newState),
|
||||
panel_targets_changed: hasQueriesChanged(getQueryRunnerFor(panel), getQueryRunnerFor(newState.$data)),
|
||||
});
|
||||
}
|
||||
|
||||
panel.parent.setState(newState);
|
||||
|
||||
//Report relevant updates
|
||||
reportPanelInspectInteraction(InspectTab.JSON, 'apply', {
|
||||
panel_type_changed: panel.state.pluginId !== panelModel.type,
|
||||
panel_id_changed: getPanelIdForVizPanel(panel) !== panelModel.id,
|
||||
panel_grid_pos_changed: hasGridPosChanged(panel.parent.state, newState),
|
||||
panel_targets_changed: hasQueriesChanged(getQueryRunnerFor(panel), getQueryRunnerFor(newState.$data)),
|
||||
});
|
||||
};
|
||||
|
||||
public onCodeEditorBlur = (value: string) => {
|
||||
@@ -152,11 +194,6 @@ export class InspectJsonTab extends SceneObjectBase<InspectJsonTabState> {
|
||||
return false;
|
||||
}
|
||||
|
||||
// V2 dashboard panels are not editable from the inspect
|
||||
if (isDashboardV2Spec(getDashboardSceneFor(panel).getSaveModel())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only support normal grid items for now and not repeated items
|
||||
if (panel.parent instanceof DashboardGridItem && panel.parent.isRepeated()) {
|
||||
return false;
|
||||
@@ -170,14 +207,14 @@ export class InspectJsonTab extends SceneObjectBase<InspectJsonTabState> {
|
||||
}
|
||||
|
||||
function InspectJsonTabComponent({ model }: SceneComponentProps<InspectJsonTab>) {
|
||||
const { source: show, jsonText } = model.useState();
|
||||
const { source: show, jsonText, error } = model.useState();
|
||||
const styles = useStyles2(getPanelInspectorStyles2);
|
||||
const options = model.getOptions();
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<div className={styles.toolbar} data-testid={selectors.components.PanelInspector.Json.content}>
|
||||
<Field label={t('dashboard.inspect-json.select-source', 'Select source')} className="flex-grow-1">
|
||||
<Field label={t('dashboard.inspect-json.select-source', 'Select source')} className="flex-grow-1" noMargin>
|
||||
<Select
|
||||
inputId="select-source-dropdown"
|
||||
options={options}
|
||||
@@ -192,6 +229,12 @@ function InspectJsonTabComponent({ model }: SceneComponentProps<InspectJsonTab>)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" title={t('dashboard-scene.inspect-json-tab.validation-error', 'Validation error')}>
|
||||
<p>{error}</p>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className={styles.content}>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
|
||||
+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',
|
||||
};
|
||||
|
||||
@@ -5,7 +5,19 @@ import { CoreApp, GrafanaTheme2, PanelPlugin, PanelProps } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { sceneUtils } from '@grafana/scenes';
|
||||
import { Box, Button, ButtonGroup, Dropdown, Icon, Menu, Stack, Text, usePanelContext, useStyles2 } from '@grafana/ui';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Dropdown,
|
||||
EmptyState,
|
||||
Icon,
|
||||
Menu,
|
||||
Stack,
|
||||
Text,
|
||||
usePanelContext,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { NEW_PANEL_TITLE } from '../../dashboard/utils/dashboard';
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
@@ -92,20 +104,30 @@ function UnconfiguredPanelComp(props: PanelProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const { isEditing } = dashboard.state;
|
||||
|
||||
return (
|
||||
<Stack direction={'row'} alignItems={'center'} height={'100%'} justifyContent={'center'}>
|
||||
<Box paddingBottom={2}>
|
||||
<ButtonGroup>
|
||||
<Button icon="sliders-v-alt" onClick={onConfigure}>
|
||||
<Trans i18nKey="dashboard.new-panel.configure-button">Configure</Trans>
|
||||
</Button>
|
||||
<Dropdown overlay={MenuActions} placement="bottom-end" onVisibleChange={onMenuClick}>
|
||||
<Button
|
||||
aria-label={t('dashboard.new-panel.configure-button-menu', 'Toggle menu')}
|
||||
icon={isOpen ? 'angle-up' : 'angle-down'}
|
||||
/>
|
||||
</Dropdown>
|
||||
</ButtonGroup>
|
||||
{isEditing ? (
|
||||
<ButtonGroup>
|
||||
<Button icon="sliders-v-alt" onClick={onConfigure}>
|
||||
<Trans i18nKey="dashboard.new-panel.configure-button">Configure</Trans>
|
||||
</Button>
|
||||
<Dropdown overlay={MenuActions} placement="bottom-end" onVisibleChange={onMenuClick}>
|
||||
<Button
|
||||
aria-label={t('dashboard.new-panel.configure-button-menu', 'Toggle menu')}
|
||||
icon={isOpen ? 'angle-up' : 'angle-down'}
|
||||
/>
|
||||
</Dropdown>
|
||||
</ButtonGroup>
|
||||
) : (
|
||||
<EmptyState
|
||||
variant="call-to-action"
|
||||
message={t('dashboard.new-panel.missing-config', 'Missing panel configuration')}
|
||||
hideImage
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -91,10 +91,12 @@ export class RowItem
|
||||
}
|
||||
|
||||
public getEditableElementInfo(): EditableDashboardElementInfo {
|
||||
const isHidden = !this.state.conditionalRendering?.state.result;
|
||||
return {
|
||||
typeName: t('dashboard.edit-pane.elements.row', 'Row'),
|
||||
instanceName: sceneGraph.interpolate(this, this.state.title, undefined, 'text'),
|
||||
icon: 'list-ul',
|
||||
isHidden,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
@@ -237,6 +238,7 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
}),
|
||||
dragging: css({
|
||||
cursor: 'move',
|
||||
backgroundColor: theme.colors.background.canvas,
|
||||
}),
|
||||
wrapperGrow: css({
|
||||
flexGrow: 1,
|
||||
|
||||
@@ -89,10 +89,12 @@ export class TabItem
|
||||
}
|
||||
|
||||
public getEditableElementInfo(): EditableDashboardElementInfo {
|
||||
const isHidden = !this.state.conditionalRendering?.state.result;
|
||||
return {
|
||||
typeName: t('dashboard.edit-pane.elements.tab', 'Tab'),
|
||||
instanceName: sceneGraph.interpolate(this, this.state.title, undefined, 'text'),
|
||||
icon: 'layers',
|
||||
isHidden,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
defaultPanelKind,
|
||||
defaultQueryGroupKind,
|
||||
defaultPanelQueryKind,
|
||||
defaultVizConfigKind,
|
||||
} from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
|
||||
import { isPanelKindV2 } from './validation';
|
||||
|
||||
describe('v2schema validation', () => {
|
||||
it('isPanelKindV2 returns true for a minimal valid PanelKind', () => {
|
||||
const panel = defaultPanelKind();
|
||||
// Ensure minimal required properties exist (defaults should be fine)
|
||||
panel.spec.vizConfig = defaultVizConfigKind();
|
||||
panel.spec.data = defaultQueryGroupKind();
|
||||
|
||||
expect(isPanelKindV2(panel)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when kind is not "Panel"', () => {
|
||||
const panel = defaultPanelKind();
|
||||
// @ts-expect-error intentional invalid kind for test
|
||||
panel.kind = 'NotAPanel';
|
||||
expect(isPanelKindV2(panel)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when data kind is wrong', () => {
|
||||
const panel = defaultPanelKind();
|
||||
// @ts-expect-error intentional invalid kind for test
|
||||
panel.spec.data = { kind: 'Wrong', spec: {} };
|
||||
expect(isPanelKindV2(panel)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when queries contain invalid entries', () => {
|
||||
const panel = defaultPanelKind();
|
||||
panel.spec.data = defaultQueryGroupKind();
|
||||
// @ts-expect-error push an invalid query shape
|
||||
panel.spec.data.spec.queries = [{}];
|
||||
expect(isPanelKindV2(panel)).toBe(false);
|
||||
|
||||
// Ensure a valid query shape passes
|
||||
panel.spec.data.spec.queries = [defaultPanelQueryKind()];
|
||||
expect(isPanelKindV2(panel)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when vizConfig.group is not a string', () => {
|
||||
const panel = defaultPanelKind();
|
||||
panel.spec.vizConfig = defaultVizConfigKind();
|
||||
// @ts-expect-error force wrong type
|
||||
panel.spec.vizConfig.group = 42;
|
||||
expect(isPanelKindV2(panel)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when transparent is not a boolean', () => {
|
||||
const panel = defaultPanelKind();
|
||||
// @ts-expect-error wrong type
|
||||
panel.spec.transparent = 'yes';
|
||||
expect(isPanelKindV2(panel)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
import {
|
||||
PanelKind,
|
||||
QueryGroupKind,
|
||||
VizConfigKind,
|
||||
PanelQueryKind,
|
||||
TransformationKind,
|
||||
} from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isPanelQueryKind(value: unknown): value is PanelQueryKind {
|
||||
if (!isObject(value)) {
|
||||
return false;
|
||||
}
|
||||
if (value.kind !== 'PanelQuery' || !isObject(value.spec)) {
|
||||
return false;
|
||||
}
|
||||
// Minimal checks for query spec; accept additional properties
|
||||
if (typeof value.spec.refId !== 'string') {
|
||||
return false;
|
||||
}
|
||||
if (typeof value.spec.hidden !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
// value.spec.query is an opaque "DataQueryKind" which is { kind: string, spec: Record<string, any> }
|
||||
const q = value.spec.query;
|
||||
if (!isObject(q) || typeof q.kind !== 'string' || !isObject(q.spec)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isTransformationKind(value: unknown): value is TransformationKind {
|
||||
if (!isObject(value)) {
|
||||
return false;
|
||||
}
|
||||
if (typeof value.kind !== 'string') {
|
||||
return false;
|
||||
}
|
||||
if (!isObject(value.spec)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isQueryGroupKind(value: unknown): value is QueryGroupKind {
|
||||
if (!isObject(value)) {
|
||||
return false;
|
||||
}
|
||||
if (value.kind !== 'QueryGroup' || !isObject(value.spec)) {
|
||||
return false;
|
||||
}
|
||||
const spec = value.spec;
|
||||
if (!Array.isArray(spec.queries) || !spec.queries.every(isPanelQueryKind)) {
|
||||
return false;
|
||||
}
|
||||
if (!Array.isArray(spec.transformations) || !spec.transformations.every(isTransformationKind)) {
|
||||
return false;
|
||||
}
|
||||
if (!isObject(spec.queryOptions)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isVizConfigKind(value: unknown): value is VizConfigKind {
|
||||
if (!isObject(value)) {
|
||||
return false;
|
||||
}
|
||||
if (value.kind !== 'VizConfig') {
|
||||
return false;
|
||||
}
|
||||
if (typeof value.group !== 'string') {
|
||||
return false;
|
||||
}
|
||||
if (typeof value.version !== 'string') {
|
||||
return false;
|
||||
}
|
||||
if (!isObject(value.spec)) {
|
||||
return false;
|
||||
}
|
||||
const spec = value.spec;
|
||||
if (!isObject(spec.options)) {
|
||||
return false;
|
||||
}
|
||||
if (!isObject(spec.fieldConfig)) {
|
||||
return false;
|
||||
}
|
||||
// Minimal fieldConfig shape (defaults/overrides may be empty)
|
||||
if (!isObject(spec.fieldConfig)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isPanelKindV2(value: unknown): value is PanelKind {
|
||||
if (!isObject(value)) {
|
||||
return false;
|
||||
}
|
||||
if (value.kind !== 'Panel') {
|
||||
return false;
|
||||
}
|
||||
if (!isObject(value.spec)) {
|
||||
return false;
|
||||
}
|
||||
const spec = value.spec;
|
||||
if (typeof spec.id !== 'number') {
|
||||
return false;
|
||||
}
|
||||
if (typeof spec.title !== 'string') {
|
||||
return false;
|
||||
}
|
||||
if (typeof spec.description !== 'string') {
|
||||
return false;
|
||||
}
|
||||
if (!Array.isArray(spec.links)) {
|
||||
return false;
|
||||
}
|
||||
if (!isQueryGroupKind(spec.data)) {
|
||||
return false;
|
||||
}
|
||||
if (!isVizConfigKind(spec.vizConfig)) {
|
||||
return false;
|
||||
}
|
||||
if (spec.transparent !== undefined && typeof spec.transparent !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function validatePanelKindV2(value: unknown): asserts value is PanelKind {
|
||||
if (!isPanelKindV2(value)) {
|
||||
throw new Error('Provided JSON is not a valid v2 Panel spec');
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -190,9 +190,7 @@ export default class TempoLanguageProvider extends LanguageProvider {
|
||||
* @returns the encoded tag
|
||||
*/
|
||||
private encodeTag = (tag: string): string => {
|
||||
// If we call `encodeURIComponent` only once, we still get an error when issuing a request to the backend
|
||||
// Reference: https://stackoverflow.com/a/37456192
|
||||
return encodeURIComponent(encodeURIComponent(tag));
|
||||
return encodeURIComponent(tag);
|
||||
};
|
||||
|
||||
generateQueryFromFilters({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -5133,6 +5133,7 @@
|
||||
"empty-state-message": "Run a query to visualize it here or go to all visualizations to add other panel types",
|
||||
"menu-open-panel-editor": "Configure",
|
||||
"menu-use-library-panel": "Use library panel",
|
||||
"missing-config": "Missing panel configuration",
|
||||
"suggestions": {
|
||||
"empty-state-message": "Run a query to start seeing suggested visualizations"
|
||||
}
|
||||
@@ -6145,7 +6146,10 @@
|
||||
"no-data-found": "No data found"
|
||||
},
|
||||
"inspect-json-tab": {
|
||||
"apply": "Apply"
|
||||
"apply": "Apply",
|
||||
"error-invalid-json": "Invalid JSON",
|
||||
"error-invalid-v2-panel": "Panel JSON did not pass validation. Please check the JSON and try again.",
|
||||
"validation-error": "Validation error"
|
||||
},
|
||||
"interval-variable-form": {
|
||||
"description-auto-option": "Dynamically calculates interval by dividing time range by the count specified",
|
||||
|
||||
@@ -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