diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ae245b0b797..a2cdf0d15b8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -949,7 +949,6 @@ playwright.storybook.config.ts @grafana/grafana-frontend-platform /public/app/plugins/panel/heatmap/ @grafana/dataviz-squad /public/app/plugins/panel/histogram/ @grafana/dataviz-squad /public/app/plugins/panel/logs/ @grafana/observability-logs -/public/app/plugins/panel/logs-new/ @grafana/observability-logs /public/app/plugins/panel/nodeGraph/ @grafana/observability-traces-and-profiling @grafana/app-o11y-visualizations /public/app/plugins/panel/traces/ @grafana/observability-traces-and-profiling /public/app/plugins/panel/flamegraph/ @grafana/observability-traces-and-profiling diff --git a/devenv/frontend-service/provisioning/dashboards/dashboards/instance.json b/devenv/frontend-service/provisioning/dashboards/dashboards/instance.json index e5b18aa04bc..4fa1d258d21 100644 --- a/devenv/frontend-service/provisioning/dashboards/dashboards/instance.json +++ b/devenv/frontend-service/provisioning/dashboards/dashboards/instance.json @@ -919,7 +919,7 @@ } ], "title": "Requests", - "type": "logs-new" + "type": "logs" }, { "datasource": { @@ -962,7 +962,7 @@ } ], "title": "grafana-api requests", - "type": "logs-new" + "type": "logs" }, { "datasource": { @@ -1317,7 +1317,7 @@ } ], "title": "frontend-service errors", - "type": "logs-new" + "type": "logs" }, { "datasource": { @@ -1360,7 +1360,7 @@ } ], "title": "grafana-api errors", - "type": "logs-new" + "type": "logs" }, { "datasource": { @@ -1403,7 +1403,7 @@ } ], "title": "All frontend-service logs", - "type": "logs-new" + "type": "logs" }, { "datasource": { @@ -1446,7 +1446,7 @@ } ], "title": "All grafana-api logs", - "type": "logs-new" + "type": "logs" } ], "preload": false, diff --git a/pkg/registry/schemas/composable_kind.go b/pkg/registry/schemas/composable_kind.go index 8d6126e8d08..e17a71e15a9 100644 --- a/pkg/registry/schemas/composable_kind.go +++ b/pkg/registry/schemas/composable_kind.go @@ -248,16 +248,6 @@ func GetComposableKinds() ([]ComposableKind, error) { CueFile: logsCue, }) - logsnewCue, err := loadCueFileWithCommon(root, filepath.Join(root, "./public/app/plugins/panel/logs-new/panelcfg.cue")) - if err != nil { - return nil, err - } - kinds = append(kinds, ComposableKind{ - Name: "logsnew", - Filename: "panelcfg.cue", - CueFile: logsnewCue, - }) - newsCue, err := loadCueFileWithCommon(root, filepath.Join(root, "./public/app/plugins/panel/news/panelcfg.cue")) if err != nil { return nil, err diff --git a/pkg/services/pluginsintegration/plugintest/plugins_test.go b/pkg/services/pluginsintegration/plugintest/plugins_test.go index d22dd7ab7d5..01b7d0d7315 100644 --- a/pkg/services/pluginsintegration/plugintest/plugins_test.go +++ b/pkg/services/pluginsintegration/plugintest/plugins_test.go @@ -227,7 +227,6 @@ func verifyCorePluginCatalogue(t *testing.T, ctx context.Context, ps *pluginstor "histogram": {}, "live": {}, "logs": {}, - "logs-new": {}, "candlestick": {}, "news": {}, "nodeGraph": {}, diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index b0f38068588..0fe081c41e8 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -44,8 +44,6 @@ const histogramPanel = async () => await import(/* webpackChunkName: "histogramPanel" */ 'app/plugins/panel/histogram/module'); const livePanel = async () => await import(/* webpackChunkName: "livePanel" */ 'app/plugins/panel/live/module'); const logsPanel = async () => await import(/* webpackChunkName: "logsPanel" */ 'app/plugins/panel/logs/module'); -const newLogsPanel = async () => - await import(/* webpackChunkName: "newLogsPanel" */ 'app/plugins/panel/logs-new/module'); const newsPanel = async () => await import(/* webpackChunkName: "newsPanel" */ 'app/plugins/panel/news/module'); const pieChartPanel = async () => await import(/* webpackChunkName: "pieChartPanel" */ 'app/plugins/panel/piechart/module'); @@ -111,7 +109,6 @@ const builtInPlugins: Record Promise; - -jest.mock('@grafana/assistant', () => ({ - ...jest.requireActual('@grafana/assistant'), - useAssistant: jest.fn(() => [true, jest.fn()]), -})); - -jest.mock('@grafana/runtime', () => ({ - ...jest.requireActual('@grafana/runtime'), - getAppEvents: jest.fn(), -})); - -const defaultProps = { - data: { - error: undefined, - request: { - panelId: 4, - app: 'dashboard', - requestId: 'A', - timezone: 'browser', - interval: '30s', - intervalMs: 30000, - maxDataPoints: 823, - targets: [], - range: getDefaultTimeRange(), - scopedVars: {}, - startTime: 1, - }, - series: [ - createDataFrame({ - refId: 'A', - fields: [ - { - name: 'timestamp', - type: FieldType.time, - values: ['2019-04-26T09:28:11.352440161Z'], - }, - { - name: 'body', - type: FieldType.string, - values: ['logline text'], - }, - { - name: 'labels', - type: FieldType.other, - values: [ - { - app: 'common_app', - }, - ], - }, - ], - meta: { - type: DataFrameType.LogLines, - }, - }), - ], - state: LoadingState.Done, - timeRange: getDefaultTimeRange(), - }, - timeZone: 'utc', - timeRange: getDefaultTimeRange(), - options: { - showLabels: false, - showTime: false, - wrapLogMessage: false, - sortOrder: LogsSortOrder.Descending, - dedupStrategy: LogsDedupStrategy.none, - enableLogDetails: false, - enableInfiniteScrolling: false, - showControls: false, - syntaxHighlighting: false, - }, - title: 'Logs panel', - id: 1, - transparent: false, - width: 400, - height: 100, - renderCounter: 0, - fieldConfig: { - defaults: {}, - overrides: [], - }, - eventBus: new EventBusSrv(), - onOptionsChange: jest.fn(), - onFieldConfigChange: jest.fn(), - replaceVariables: jest.fn(), - onChangeTimeRange: jest.fn(), -}; - -const publishMock = jest.fn(); -beforeAll(() => { - jest.mocked(getAppEvents).mockReturnValue({ - publish: publishMock, - getStream: jest.fn(), - subscribe: jest.fn(), - removeAllListeners: jest.fn(), - newScopedBus: jest.fn(), - }); -}); - -describe('LogsPanel', () => { - test('Renders a list of logs without controls ', async () => { - setup(); - await screen.findByText('logline text'); - expect(screen.queryByLabelText('Scroll to bottom')).not.toBeInTheDocument(); - expect(screen.queryByLabelText('Display levels')).not.toBeInTheDocument(); - expect(screen.queryByLabelText('Scroll to top')).not.toBeInTheDocument(); - }); - - test('Renders a list of logs with controls', async () => { - setup({ options: { ...defaultProps.options, showControls: true } }); - await screen.findByText('logline text'); - expect(screen.getByLabelText('Scroll to bottom')).toBeInTheDocument(); - expect(screen.getByLabelText('Display levels')).toBeInTheDocument(); - expect(screen.getByLabelText('Scroll to top')).toBeInTheDocument(); - }); - - test('Publishes an event with the current sort order', async () => { - publishMock.mockClear(); - setup(); - - await screen.findByText('logline text'); - - expect(publishMock).toHaveBeenCalledTimes(1); - expect(publishMock).toHaveBeenCalledWith( - new LogSortOrderChangeEvent({ - order: LogsSortOrder.Descending, - }) - ); - }); -}); - -const setup = (propsOverrides?: Partial) => { - const props: LogsPanelProps = { - ...defaultProps, - data: { - ...(propsOverrides?.data || defaultProps.data), - }, - options: { - ...(propsOverrides?.options || defaultProps.options), - }, - }; - - return { ...render(), props }; -}; diff --git a/public/app/plugins/panel/logs-new/LogsPanel.tsx b/public/app/plugins/panel/logs-new/LogsPanel.tsx deleted file mode 100644 index e62051a34c1..00000000000 --- a/public/app/plugins/panel/logs-new/LogsPanel.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { css } from '@emotion/css'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; - -import { - AbsoluteTimeRange, - CoreApp, - DataFrame, - DataHoverEvent, - GrafanaTheme2, - LoadingState, - LogRowModel, - LogSortOrderChangeEvent, - LogsSortOrder, - PanelProps, -} from '@grafana/data'; -import { config, getAppEvents } from '@grafana/runtime'; -import { usePanelContext, useStyles2 } from '@grafana/ui'; -import { LogList } from 'app/features/logs/components/panel/LogList'; -import { PanelDataErrorView } from 'app/features/panel/components/PanelDataErrorView'; - -import { dataFrameToLogsModel, dedupLogRows } from '../../../features/logs/logsModel'; -import { requestMoreLogs } from '../logs/LogsPanel'; -import { useDatasourcesFromTargets } from '../logs/useDatasourcesFromTargets'; - -import { Options } from './panelcfg.gen'; -import { isCoreApp, isLogsGrammar, isOnLogOptionsChange, isOnNewLogsReceivedType } from './types'; - -interface LogsPanelProps extends PanelProps {} - -export const LogsPanel = ({ - data, - timeZone, - fieldConfig, - options: { - controlsStorageKey, - dedupStrategy, - enableInfiniteScrolling, - grammar, - onLogOptionsChange, - onNewLogsReceived, - showControls, - showTime, - sortOrder, - syntaxHighlighting, - wrapLogMessage, - }, - id, -}: LogsPanelProps) => { - const style = useStyles2(getStyles); - const [logsContainer, setLogsContainer] = useState(null); - const [panelData, setPanelData] = useState(data); - const dataSourcesMap = useDatasourcesFromTargets(data.request?.targets); - // Prevents the scroll position to change when new data from infinite scrolling is received - const keepScrollPositionRef = useRef(false); - // Loading ref to prevent firing multiple requests - const loadingRef = useRef(false); - const { app, eventBus } = usePanelContext(); - - const logs = useMemo(() => { - const logsModel = panelData - ? dataFrameToLogsModel(panelData.series, panelData.request?.intervalMs, undefined, panelData.request?.targets) - : null; - return logsModel ? dedupLogRows(logsModel.rows, dedupStrategy) : []; - }, [dedupStrategy, panelData]); - - useEffect(() => { - getAppEvents().publish( - new LogSortOrderChangeEvent({ - order: sortOrder, - }) - ); - }, [sortOrder]); - - useEffect(() => { - if (data.state !== LoadingState.Loading) { - setPanelData(data); - } - }, [data]); - - const loadMoreLogs = useCallback( - async (scrollRange: AbsoluteTimeRange) => { - if (!data.request || !config.featureToggles.logsInfiniteScrolling || loadingRef.current) { - return; - } - loadingRef.current = true; - - const onNewLogsReceivedCallback = isOnNewLogsReceivedType(onNewLogsReceived) ? onNewLogsReceived : undefined; - - let newSeries: DataFrame[] = []; - try { - newSeries = await requestMoreLogs(dataSourcesMap, panelData, scrollRange, timeZone, onNewLogsReceivedCallback); - } catch (e) { - console.error(e); - } finally { - loadingRef.current = false; - } - - keepScrollPositionRef.current = true; - setPanelData({ - ...panelData, - series: newSeries, - }); - }, - [data.request, dataSourcesMap, onNewLogsReceived, panelData, timeZone] - ); - - const onLogRowHover = useCallback( - (row?: LogRowModel) => { - if (row) { - eventBus.publish( - new DataHoverEvent({ - point: { - time: row.timeEpochMs, - }, - }) - ); - } - }, - [eventBus] - ); - - const initialScrollPosition = useMemo(() => { - /** - * In dashboards, users with newest logs at the bottom have the expectation of keeping the scroll at the bottom - * when new data is received. See https://github.com/grafana/grafana/pull/37634 - */ - if (data.request?.app === CoreApp.Dashboard || data.request?.app === CoreApp.PanelEditor) { - return sortOrder === LogsSortOrder.Ascending ? 'bottom' : 'top'; - } - return 'top'; - }, [data.request?.app, sortOrder]); - - const storageKey = useMemo(() => { - if (controlsStorageKey) { - return controlsStorageKey; - } - if (!data.request) { - return undefined; - } - return `${data.request?.dashboardUID}.${id}`; - }, [controlsStorageKey, data.request, id]); - - if (!logs.length) { - return ; - } - - return ( -
setLogsContainer(element)}> - {logs.length > 0 && logsContainer && ( - - )} -
- ); -}; - -const getStyles = (theme: GrafanaTheme2) => ({ - container: css({ - marginBottom: theme.spacing(1.5), - minHeight: '100%', - maxHeight: '100%', - display: 'flex', - flex: 1, - flexDirection: 'column', - }), -}); diff --git a/public/app/plugins/panel/logs-new/img/icn-logs-panel.svg b/public/app/plugins/panel/logs-new/img/icn-logs-panel.svg deleted file mode 100644 index 046b59454ad..00000000000 --- a/public/app/plugins/panel/logs-new/img/icn-logs-panel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/app/plugins/panel/logs-new/module.tsx b/public/app/plugins/panel/logs-new/module.tsx deleted file mode 100644 index 04c6be4c61c..00000000000 --- a/public/app/plugins/panel/logs-new/module.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { PanelPlugin, LogsSortOrder, LogsDedupStrategy, LogsDedupDescription } from '@grafana/data'; -import { t } from '@grafana/i18n'; - -import { LogsPanel } from './LogsPanel'; -import { Options } from './panelcfg.gen'; -import { LogsPanelSuggestionsSupplier } from './suggestions'; - -export const plugin = new PanelPlugin(LogsPanel) - .setPanelOptions((builder) => { - const category = [t('logs-new.category-logs', 'Logs')]; - builder - .addBooleanSwitch({ - path: 'showTime', - name: t('logs-new.name-time', 'Time'), - category, - description: '', - defaultValue: false, - }) - .addBooleanSwitch({ - path: 'wrapLogMessage', - name: t('logs-new.name-wrap-lines', 'Wrap lines'), - category, - description: '', - defaultValue: false, - }) - .addBooleanSwitch({ - path: 'syntaxHighlighting', - name: t('logs-new.name-syntax-highlighting', 'Enable syntax highlighting'), - category, - description: t( - 'logs-new.description-syntax-highlighting', - 'Use a predefined syntax coloring grammar to highlight relevant parts of the log lines' - ), - defaultValue: true, - }) - .addBooleanSwitch({ - path: 'enableLogDetails', - name: t('logs-new.name-enable-log-details', 'Enable log details'), - category, - description: '', - defaultValue: true, - }) - .addBooleanSwitch({ - path: 'showControls', - name: t('logs-new.name-show-controls', 'Show controls'), - category, - description: t( - 'logs-new.description-show-controls', - 'Display controls to jump to the last or first log line, and filters by log level' - ), - defaultValue: false, - }) - .addBooleanSwitch({ - path: 'enableInfiniteScrolling', - name: t('logs-new.name-infinite-scrolling', 'Enable infinite scrolling'), - category, - description: t( - 'logs-new.description-infinite-scrolling', - 'Experimental. Request more results by scrolling to the bottom of the logs list.' - ), - defaultValue: false, - }) - .addRadio({ - path: 'dedupStrategy', - name: t('logs-new.name-deduplication', 'Deduplication'), - category, - description: '', - settings: { - options: [ - { - value: LogsDedupStrategy.none, - label: t('logs-new.deduplication-options.label-none', 'None'), - description: LogsDedupDescription[LogsDedupStrategy.none], - }, - { - value: LogsDedupStrategy.exact, - label: t('logs-new.deduplication-options.label-exact', 'Exact'), - description: LogsDedupDescription[LogsDedupStrategy.exact], - }, - { - value: LogsDedupStrategy.numbers, - label: t('logs-new.deduplication-options.label-numbers', 'Numbers'), - description: LogsDedupDescription[LogsDedupStrategy.numbers], - }, - { - value: LogsDedupStrategy.signature, - label: t('logs-new.deduplication-options.label-signature', 'Signature'), - description: LogsDedupDescription[LogsDedupStrategy.signature], - }, - ], - }, - defaultValue: LogsDedupStrategy.none, - }) - .addRadio({ - path: 'sortOrder', - name: t('logs-new.name-order', 'Order'), - category, - description: '', - settings: { - options: [ - { value: LogsSortOrder.Descending, label: t('logs-new.order-options.label-newest-first', 'Newest first') }, - { value: LogsSortOrder.Ascending, label: t('logs-new.order-options.label-oldest-first', 'Oldest first') }, - ], - }, - defaultValue: LogsSortOrder.Descending, - }); - }) - .setSuggestionsSupplier(new LogsPanelSuggestionsSupplier()); diff --git a/public/app/plugins/panel/logs-new/panelcfg.cue b/public/app/plugins/panel/logs-new/panelcfg.cue deleted file mode 100644 index 419217c31eb..00000000000 --- a/public/app/plugins/panel/logs-new/panelcfg.cue +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2023 Grafana Labs -// -// Licensed under the Apache License, Version 2.0 (the "License") -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package grafanaplugin - -import ( - "github.com/grafana/grafana/packages/grafana-schema/src/common" -) - -composableKinds: PanelCfg: { - maturity: "experimental" - - lineage: { - schemas: [{ - version: [0, 0] - schema: { - Options: { - showControls: bool - showTime: bool - wrapLogMessage: bool - enableLogDetails: bool - syntaxHighlighting: bool - sortOrder: common.LogsSortOrder - dedupStrategy: common.LogsDedupStrategy - grammar?: _ - enableInfiniteScrolling?: bool - onLogOptionsChange?: _ - onNewLogsReceived?: _ - controlsStorageKey?: string - } @cuetsy(kind="interface") - } - }] - lenses: [] - } -} diff --git a/public/app/plugins/panel/logs-new/panelcfg.gen.ts b/public/app/plugins/panel/logs-new/panelcfg.gen.ts deleted file mode 100644 index cce2a64411b..00000000000 --- a/public/app/plugins/panel/logs-new/panelcfg.gen.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. -// -// Generated by: -// public/app/plugins/gen.go -// Using jennies: -// TSTypesJenny -// PluginTsTypesJenny -// -// Run 'make gen-cue' from repository root to regenerate. - -import * as common from '@grafana/schema'; - -export interface Options { - controlsStorageKey?: string; - dedupStrategy: common.LogsDedupStrategy; - enableInfiniteScrolling?: boolean; - enableLogDetails: boolean; - grammar?: unknown; - onLogOptionsChange?: unknown; - onNewLogsReceived?: unknown; - showControls: boolean; - showTime: boolean; - sortOrder: common.LogsSortOrder; - syntaxHighlighting: boolean; - wrapLogMessage: boolean; -} diff --git a/public/app/plugins/panel/logs-new/plugin.json b/public/app/plugins/panel/logs-new/plugin.json deleted file mode 100644 index f4521594b79..00000000000 --- a/public/app/plugins/panel/logs-new/plugin.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "type": "panel", - "name": "Logs new", - "id": "logs-new", - "state": "alpha", - - "info": { - "author": { - "name": "Grafana Labs", - "url": "https://grafana.com" - }, - "logos": { - "small": "img/icn-logs-panel.svg", - "large": "img/icn-logs-panel.svg" - }, - "links": [ - { "name": "Raise issue", "url": "https://github.com/grafana/grafana/issues/new" }, - { - "name": "Documentation", - "url": "https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/logs/" - } - ] - } -} diff --git a/public/app/plugins/panel/logs-new/suggestions.ts b/public/app/plugins/panel/logs-new/suggestions.ts deleted file mode 100644 index 5229b4fb7f0..00000000000 --- a/public/app/plugins/panel/logs-new/suggestions.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { VisualizationSuggestionsBuilder, VisualizationSuggestionScore } from '@grafana/data'; -import { SuggestionName } from 'app/types/suggestions'; - -import { Options } from './panelcfg.gen'; - -export class LogsPanelSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const list = builder.getListAppender({ - name: '', - pluginId: 'logs-new', - options: {}, - fieldConfig: { - defaults: { - custom: {}, - }, - overrides: [], - }, - }); - - const { dataSummary: ds } = builder; - - // Require a string & time field - if (!ds.hasData || !ds.hasTimeField || !ds.hasStringField) { - return; - } - - if (ds.preferredVisualisationType === 'logs') { - list.append({ name: SuggestionName.Logs, score: VisualizationSuggestionScore.Best }); - } else { - list.append({ name: SuggestionName.Logs }); - } - } -} diff --git a/public/app/plugins/panel/logs-new/types.ts b/public/app/plugins/panel/logs-new/types.ts deleted file mode 100644 index a9a0ad8c9e0..00000000000 --- a/public/app/plugins/panel/logs-new/types.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Grammar } from 'prismjs'; - -import { CoreApp, DataFrame } from '@grafana/data'; -import { LogListControlOptions } from 'app/features/logs/components/panel/LogList'; - -type onNewLogsReceivedType = (allLogs: DataFrame[], newLogs: DataFrame[]) => void; -type onLogOptionsChangeType = (option: LogListControlOptions, value: string | boolean | string[]) => void; - -export function isOnNewLogsReceivedType(callback: unknown): callback is onNewLogsReceivedType { - return typeof callback === 'function'; -} - -export function isOnLogOptionsChange(callback: unknown): callback is onLogOptionsChangeType { - return typeof callback === 'function'; -} - -export function isLogsGrammar(grammar: unknown): grammar is Grammar { - return grammar !== null && typeof grammar === 'object' && Object.getPrototypeOf(grammar) === Object.prototype; -} - -export function isCoreApp(app: unknown): app is CoreApp { - const apps = Object.values(CoreApp).map((coreApp) => coreApp.toString()); - return typeof app === 'string' && apps.includes(app); -} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 34e8e2709d3..ddf60d46082 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -9734,30 +9734,6 @@ "label-log-stats": "{{label}}: {{total}} of {{rowCount}} rows have that label" } }, - "logs-new": { - "category-logs": "Logs", - "deduplication-options": { - "label-exact": "Exact", - "label-none": "None", - "label-numbers": "Numbers", - "label-signature": "Signature" - }, - "description-infinite-scrolling": "Experimental. Request more results by scrolling to the bottom of the logs list.", - "description-show-controls": "Display controls to jump to the last or first log line, and filters by log level", - "description-syntax-highlighting": "Use a predefined syntax coloring grammar to highlight relevant parts of the log lines", - "name-deduplication": "Deduplication", - "name-enable-log-details": "Enable log details", - "name-infinite-scrolling": "Enable infinite scrolling", - "name-order": "Order", - "name-show-controls": "Show controls", - "name-syntax-highlighting": "Enable syntax highlighting", - "name-time": "Time", - "name-wrap-lines": "Wrap lines", - "order-options": { - "label-newest-first": "Newest first", - "label-oldest-first": "Oldest first" - } - }, "manage-dashbaords": { "import-dashboard-form": { "description-existing-library-panels": "List of existing library panels. These panels are not affected by the import."