From 5d725d22adb0263f3270dbffaf7e17c2e8e972f5 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Tue, 10 Jan 2023 14:59:32 +0000 Subject: [PATCH] CommandPalette: Search for dashboards using API (#61090) * CommandPalette: Search for dashboards using API * Fix ordering of dashboards * Put recent + search dashboards in root list, refactor actions into hook * limit recent dashboards to 5 * search debounce to 200ms * update priorities * extract i18n --- public/app/AppWrapper.tsx | 3 +- .../commandPalette/CommandPalette.tsx | 49 ++++-------- .../actions/dashboard.nav.actions.ts | 32 -------- .../actions/dashboardActions.ts | 76 +++++++++++++++++++ ...al.static.actions.tsx => staticActions.ts} | 72 ++++++------------ .../commandPalette/actions/useActions.ts | 50 ++++++++++++ public/app/features/commandPalette/types.ts | 18 +++++ public/app/features/commandPalette/values.ts | 4 + public/locales/de-DE/grafana.json | 16 +++- public/locales/en-US/grafana.json | 18 ++++- public/locales/es-ES/grafana.json | 16 +++- public/locales/fr-FR/grafana.json | 26 +++++-- public/locales/pseudo-LOCALE/grafana.json | 18 ++++- public/locales/zh-Hans/grafana.json | 16 +++- 14 files changed, 285 insertions(+), 129 deletions(-) delete mode 100644 public/app/features/commandPalette/actions/dashboard.nav.actions.ts create mode 100644 public/app/features/commandPalette/actions/dashboardActions.ts rename public/app/features/commandPalette/actions/{global.static.actions.tsx => staticActions.ts} (54%) create mode 100644 public/app/features/commandPalette/actions/useActions.ts create mode 100644 public/app/features/commandPalette/types.ts create mode 100644 public/app/features/commandPalette/values.ts diff --git a/public/app/AppWrapper.tsx b/public/app/AppWrapper.tsx index b7924f787e3..109a4e9d81f 100644 --- a/public/app/AppWrapper.tsx +++ b/public/app/AppWrapper.tsx @@ -91,7 +91,8 @@ export class AppWrapper extends React.Component { const styles = useStyles2(getSearchStyles); - const [actions, setActions] = useState([]); - const [staticActions, setStaticActions] = useState([]); - const { query, showing } = useKBar((state) => ({ - showing: state.visualState === VisualState.showing, - })); - const isNotLogin = locationService.getLocation().pathname !== '/login'; - const { navBarTree } = useSelector((state) => { - return { - navBarTree: state.navBarTree, - }; - }); + const { query, showing, searchQuery } = useKBar((state) => ({ + showing: state.visualState === VisualState.showing, + searchQuery: state.searchQuery, + })); + + const actions = useActions(searchQuery, showing); + useRegisterActions(actions, [actions]); const ref = useRef(null); const { overlayProps } = useOverlay( @@ -52,26 +45,10 @@ export const CommandPalette = () => { ); const { dialogProps } = useDialog({}, ref); + // Report interaction when opened useEffect(() => { - if (isNotLogin) { - const staticActionsResp = getGlobalActions(navBarTree); - setStaticActions(staticActionsResp); - setActions([...staticActionsResp]); - } - }, [isNotLogin, navBarTree]); - - useEffect(() => { - if (showing) { - reportInteraction('command_palette_opened'); - - // Do dashboard search on demand - getDashboardNavActions('go/dashboard').then((dashAct) => { - setActions([...staticActions, ...dashAct]); - }); - } - }, [showing, staticActions]); - - useRegisterActions(actions, [actions]); + showing && reportInteraction('command_palette_opened'); + }, [showing]); return actions.length > 0 ? ( diff --git a/public/app/features/commandPalette/actions/dashboard.nav.actions.ts b/public/app/features/commandPalette/actions/dashboard.nav.actions.ts deleted file mode 100644 index 61a4841ecc2..00000000000 --- a/public/app/features/commandPalette/actions/dashboard.nav.actions.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Action } from 'kbar'; - -import { locationUtil } from '@grafana/data'; -import { locationService } from '@grafana/runtime'; -import { getGrafanaSearcher } from 'app/features/search/service'; - -async function getDashboardNav(parentId: string): Promise { - const data = await getGrafanaSearcher().search({ - kind: ['dashboard'], - query: '*', - limit: 500, - }); - - const goToDashboardActions: Action[] = data.view.map((item) => { - const { url, name } = item; // items are backed by DataFrameView, so must hold the url in a closure - return { - parent: parentId, - id: `go/dashboard/${url}`, - name: `${name}`, - perform: () => { - locationService.push(locationUtil.stripBaseFromUrl(url)); - }, - }; - }); - - return goToDashboardActions; -} - -export default async (parentId: string) => { - const dashboardNav = await getDashboardNav(parentId); - return dashboardNav; -}; diff --git a/public/app/features/commandPalette/actions/dashboardActions.ts b/public/app/features/commandPalette/actions/dashboardActions.ts new file mode 100644 index 00000000000..0fc75cb6e00 --- /dev/null +++ b/public/app/features/commandPalette/actions/dashboardActions.ts @@ -0,0 +1,76 @@ +import { locationUtil } from '@grafana/data'; +import { locationService } from '@grafana/runtime'; +import { t } from 'app/core/internationalization'; +import impressionSrv from 'app/core/services/impression_srv'; +import { getGrafanaSearcher } from 'app/features/search/service'; + +import { CommandPaletteAction } from '../types'; +import { RECENT_DASHBOARDS_PRORITY, SEARCH_RESULTS_PRORITY } from '../values'; + +const MAX_SEARCH_RESULTS = 100; +const MAX_RECENT_DASHBOARDS = 5; + +export async function getRecentDashboardActions(): Promise { + const recentUids = (await impressionSrv.getDashboardOpened()).slice(0, MAX_RECENT_DASHBOARDS); + const resultsDataFrame = await getGrafanaSearcher().search({ + kind: ['dashboard'], + limit: MAX_RECENT_DASHBOARDS, + uid: recentUids, + }); + + // Search results are alphabetical, so reorder them according to recently viewed + const recentResults = resultsDataFrame.view.toArray(); + recentResults.sort((resultA, resultB) => { + const orderA = recentUids.indexOf(resultA.uid); + const orderB = recentUids.indexOf(resultB.uid); + return orderA - orderB; + }); + + const recentDashboardActions: CommandPaletteAction[] = recentResults.map((item) => { + const { url, name } = item; // items are backed by DataFrameView, so must hold the url in a closure + return { + id: `recent-dashboards/${url}`, + name: `${name}`, + section: t('command-palette.section.recent-dashboards', 'Recently viewed dashboards'), + priority: RECENT_DASHBOARDS_PRORITY, + perform: () => { + locationService.push(locationUtil.stripBaseFromUrl(url)); + }, + }; + }); + + return recentDashboardActions; +} + +export async function getDashboardSearchResultActions(searchQuery: string): Promise { + // Empty strings should not come through to here + if (searchQuery.length === 0) { + return []; + } + + const data = await getGrafanaSearcher().search({ + kind: ['dashboard'], + query: searchQuery, + limit: MAX_SEARCH_RESULTS, + }); + + const goToDashboardActions: CommandPaletteAction[] = data.view.map((item) => { + const { url, name } = item; // items are backed by DataFrameView, so must hold the url in a closure + return { + id: `go/dashboard/${url}`, + name: `${name}`, + section: t('command-palette.section.dashboard-search-results', 'Dashboards'), + priority: SEARCH_RESULTS_PRORITY, + perform: () => { + locationService.push(locationUtil.stripBaseFromUrl(url)); + }, + }; + }); + + return goToDashboardActions; +} + +// export default async (parentId: string) => { +// const dashboardNav = await getDashboardNav(parentId); +// return dashboardNav; +// }; diff --git a/public/app/features/commandPalette/actions/global.static.actions.tsx b/public/app/features/commandPalette/actions/staticActions.ts similarity index 54% rename from public/app/features/commandPalette/actions/global.static.actions.tsx rename to public/app/features/commandPalette/actions/staticActions.ts index 3ab6ca206fe..0eed8786fd2 100644 --- a/public/app/features/commandPalette/actions/global.static.actions.tsx +++ b/public/app/features/commandPalette/actions/staticActions.ts @@ -1,27 +1,21 @@ -import { Action, Priority } from 'kbar'; -import React from 'react'; - -import { isIconName, locationUtil, NavModelItem } from '@grafana/data'; +import { locationUtil, NavModelItem } from '@grafana/data'; import { locationService } from '@grafana/runtime'; -import { Icon } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; import { changeTheme } from 'app/core/services/theme'; -const SECTION_PAGES = 'Pages'; -const SECTION_ACTIONS = 'Actions'; -const SECTION_PREFERENCES = 'Preferences'; +import { CommandPaletteAction } from '../types'; +import { DEFAULT_PRIORITY, PREFERENCES_PRIORITY } from '../values'; -export interface NavBarActions { - url: string; - actions: Action[]; -} +// We reuse this, but translations cannot be in module scope (t must be called after i18n has set up,) +const getPagesSectionTranslation = () => t('command-palette.section.pages', 'Pages'); // TODO: Clean this once ID is mandatory on nav items function idForNavItem(navItem: NavModelItem) { return 'navModel.' + navItem.id ?? navItem.url ?? navItem.text ?? navItem.subTitle; } -function navTreeToActions(navTree: NavModelItem[], parent?: NavModelItem): Action[] { - const navActions: Action[] = []; +function navTreeToActions(navTree: NavModelItem[], parent?: NavModelItem): CommandPaletteAction[] { + const navActions: CommandPaletteAction[] = []; for (const navItem of navTree) { const { url, text, isCreateAction, children } = navItem; @@ -31,15 +25,15 @@ function navTreeToActions(navTree: NavModelItem[], parent?: NavModelItem): Actio continue; } - const action: Action = { + const section = isCreateAction ? t('command-palette.section.actions', 'Actions') : getPagesSectionTranslation(); + + const action = { id: idForNavItem(navItem), name: text, // TODO: translate - section: isCreateAction ? SECTION_ACTIONS : SECTION_PAGES, + section: section, perform: url ? () => locationService.push(locationUtil.stripBaseFromUrl(url)) : undefined, parent: parent && idForNavItem(parent), - - // Only show icons for top level items - icon: !parent && iconForNavItem(navItem), + priority: DEFAULT_PRIORITY, }; navActions.push(action); @@ -53,46 +47,40 @@ function navTreeToActions(navTree: NavModelItem[], parent?: NavModelItem): Actio return navActions; } -export default (navBarTree: NavModelItem[]) => { - const globalActions: Action[] = [ - { - // TODO: Figure out what section, if any, to put this in - id: 'go/dashboard', - name: 'Dashboards...', - keywords: 'navigate', - priority: Priority.NORMAL, - }, +export default (navBarTree: NavModelItem[]): CommandPaletteAction[] => { + const globalActions: CommandPaletteAction[] = [ { id: 'go/search', - name: 'Search', + name: t('command-palette.action.search', 'Search'), keywords: 'navigate', - icon: , perform: () => locationService.push('?search=open'), - section: SECTION_PAGES, + section: t('command-palette.section.pages', 'Pages'), shortcut: ['s', 'o'], + priority: DEFAULT_PRIORITY, }, { id: 'preferences/theme', - name: 'Change theme...', + name: t('command-palette.action.change-theme', 'Change theme...'), keywords: 'interface color dark light', - section: SECTION_PREFERENCES, + section: t('command-palette.section.preferences', 'Preferences'), shortcut: ['c', 't'], + priority: PREFERENCES_PRIORITY, }, { id: 'preferences/dark-theme', - name: 'Dark', + name: t('command-palette.action.dark-theme', 'Dark'), keywords: 'dark theme', - section: '', perform: () => changeTheme('dark'), parent: 'preferences/theme', + priority: PREFERENCES_PRIORITY, }, { id: 'preferences/light-theme', - name: 'Light', + name: t('command-palette.action.light-theme', 'Light'), keywords: 'light theme', - section: '', perform: () => changeTheme('light'), parent: 'preferences/theme', + priority: PREFERENCES_PRIORITY, }, ]; @@ -100,13 +88,3 @@ export default (navBarTree: NavModelItem[]) => { return [...globalActions, ...navBarActions]; }; - -function iconForNavItem(navItem: NavModelItem) { - if (navItem.icon && isIconName(navItem.icon)) { - return ; - } else if (navItem.img) { - return ; - } - - return undefined; -} diff --git a/public/app/features/commandPalette/actions/useActions.ts b/public/app/features/commandPalette/actions/useActions.ts new file mode 100644 index 00000000000..8b5a88e7c97 --- /dev/null +++ b/public/app/features/commandPalette/actions/useActions.ts @@ -0,0 +1,50 @@ +import debounce from 'debounce-promise'; +import { useEffect, useMemo, useState } from 'react'; + +import { useSelector } from 'app/types'; + +import { CommandPaletteAction } from '../types'; + +import { getDashboardSearchResultActions, getRecentDashboardActions } from './dashboardActions'; +import getStaticActions from './staticActions'; + +const debouncedDashboardSearch = debounce(getDashboardSearchResultActions, 200); + +export default function useActions(searchQuery: string, isShowing: boolean) { + const [staticActions, setStaticActions] = useState([]); + const [dashboardResultActions, setDashboardResultActions] = useState([]); + + const { navBarTree } = useSelector((state) => { + return { + navBarTree: state.navBarTree, + }; + }); + + // Load standard static actions + useEffect(() => { + const staticActionsResp = getStaticActions(navBarTree); + setStaticActions(staticActionsResp); + }, [navBarTree]); + + // Load recent dashboards - we don't want them to reload when the nav tree changes + useEffect(() => { + getRecentDashboardActions() + .then((recentDashboardActions) => setStaticActions((v) => [...v, ...recentDashboardActions])) + .catch((err) => { + console.error('Error loading recent dashboard actions', err); + }); + }, []); + + // Hit dashboards API + useEffect(() => { + if (isShowing && searchQuery.length > 0) { + debouncedDashboardSearch(searchQuery).then((resultActions) => { + setDashboardResultActions(resultActions); + }); + } + }, [isShowing, searchQuery]); + + const actions = useMemo(() => [...staticActions, ...dashboardResultActions], [staticActions, dashboardResultActions]); + + return actions; +} diff --git a/public/app/features/commandPalette/types.ts b/public/app/features/commandPalette/types.ts new file mode 100644 index 00000000000..01c1690025a --- /dev/null +++ b/public/app/features/commandPalette/types.ts @@ -0,0 +1,18 @@ +import { Action } from 'kbar'; + +type NotNullable = Exclude; + +// Create our own action type to make priority mandatory. +// Parent actions require a section, but not child actions +export type CommandPaletteAction = RootCommandPaletteAction | ChildCommandPaletteAction; + +type RootCommandPaletteAction = Omit & { + section: NotNullable; + priority: NotNullable; +}; + +type ChildCommandPaletteAction = Action & { + parent: NotNullable; + + priority: NotNullable; +}; diff --git a/public/app/features/commandPalette/values.ts b/public/app/features/commandPalette/values.ts new file mode 100644 index 00000000000..6b529adee0d --- /dev/null +++ b/public/app/features/commandPalette/values.ts @@ -0,0 +1,4 @@ +export const RECENT_DASHBOARDS_PRORITY = 4; +export const DEFAULT_PRIORITY = 3; +export const PREFERENCES_PRIORITY = 2; +export const SEARCH_RESULTS_PRORITY = 1; // Dynamic actions should be below static ones so the list doesn't 'jump' when they come in diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 265760325ed..0a944ead003 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -5,6 +5,21 @@ "success": "Kopiert" } }, + "command-palette": { + "action": { + "change-theme": "", + "dark-theme": "", + "light-theme": "", + "search": "" + }, + "section": { + "actions": "", + "dashboard-search-results": "", + "pages": "", + "preferences": "", + "recent-dashboards": "" + } + }, "common": { "locale": { "default": "Standard" @@ -83,7 +98,6 @@ "datasource-onboarding": { "contact-admin": "Bitte wenden Sie sich an Ihren Administrator, um die Datenquellen zu konfigurieren.", "explanation": "Um Ihre Daten zu visualisieren, müssen Sie sie zunächst verknüpfen.", - "logo": "Logo für die Datenquelle {{datasourceName}}", "new-dashboard": "Neues Dashboard", "preferred": "Verbinden Sie Ihre bevorzugte Datenquelle:", "sampleData": "Oder erstellen Sie ein neues Dashboard mit Beispieldaten", diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 51e16eb3bff..07087e1a58e 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -5,6 +5,21 @@ "success": "Copied" } }, + "command-palette": { + "action": { + "change-theme": "Change theme...", + "dark-theme": "Dark", + "light-theme": "Light", + "search": "Search" + }, + "section": { + "actions": "Actions", + "dashboard-search-results": "Dashboards", + "pages": "Pages", + "preferences": "Preferences", + "recent-dashboards": "Recently viewed dashboards" + } + }, "common": { "locale": { "default": "Default" @@ -83,7 +98,6 @@ "datasource-onboarding": { "contact-admin": "Please contact your administrator to configure data sources.", "explanation": "To visualize your data, you'll need to connect it first.", - "logo": "Logo for {{datasourceName}} data source", "new-dashboard": "New dashboard", "preferred": "Connect your preferred data source:", "sampleData": "Or set up a new dashboard with sample data", @@ -314,7 +328,7 @@ }, "support-bundles": { "subtitle": "Download support bundles", - "title": "Support Bundles" + "title": "Support bundles" }, "teams": { "subtitle": "Groups of users that have common dashboard and permission needs", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 3d9f6994bfb..420b4704cd0 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -5,6 +5,21 @@ "success": "Copiado" } }, + "command-palette": { + "action": { + "change-theme": "", + "dark-theme": "", + "light-theme": "", + "search": "" + }, + "section": { + "actions": "", + "dashboard-search-results": "", + "pages": "", + "preferences": "", + "recent-dashboards": "" + } + }, "common": { "locale": { "default": "Por defecto" @@ -83,7 +98,6 @@ "datasource-onboarding": { "contact-admin": "Póngase en contacto con su administrador para configurar las fuentes de datos.", "explanation": "Para visualizar sus datos, primero tendrá que conectar una fuente.", - "logo": "Logo para la fuente de datos {{datasourceName}}", "new-dashboard": "Nuevo panel de control", "preferred": "Conecte su fuente de datos preferida:", "sampleData": "O configure un nuevo panel de control con datos de muestra", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 276b9efd2ca..314f091930f 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -5,6 +5,21 @@ "success": "Copié" } }, + "command-palette": { + "action": { + "change-theme": "", + "dark-theme": "", + "light-theme": "", + "search": "" + }, + "section": { + "actions": "", + "dashboard-search-results": "", + "pages": "", + "preferences": "", + "recent-dashboards": "" + } + }, "common": { "locale": { "default": "Par défaut" @@ -21,7 +36,7 @@ "query-tab": "Requête", "stats-tab": "Statistiques", "subtitle": "{{queryCount}} requêtes avec un délai total de requête de {{formatted}}", - "title": "Inspecter : {{panelTitle}}" + "title": "Inspecter\u00a0: {{panelTitle}}" }, "inspect-data": { "data-options": "Options de données", @@ -51,7 +66,7 @@ "panel-json-description": "Le modèle enregistré dans le tableau de bord JSON qui configure comment tout fonctionne.", "panel-json-label": "Panneau JSON", "select-source": "Sélectionner la source", - "unknown": "Objet inconnu : {{show}}" + "unknown": "Objet inconnu\u00a0: {{show}}" }, "inspect-meta": { "no-inspector": "Pas d'inspecteur de métadonnées" @@ -83,9 +98,8 @@ "datasource-onboarding": { "contact-admin": "Veuillez contacter votre administrateur pour configurer les sources de données.", "explanation": "Pour visualiser vos données, vous devrez d’abord les connecter.", - "logo": "Logo pour la source de données {{datasourceName}}", "new-dashboard": "Nouveau tableau de bord", - "preferred": "Connectez votre source de données préférée :", + "preferred": "Connectez votre source de données préférée\u00a0:", "sampleData": "Ou établissez un nouveau tableau de bord avec des exemples de données", "viewAll": "Afficher tout", "welcome": "Bienvenue aux tableaux de bord Grafana !" @@ -105,7 +119,7 @@ }, "library-panels": { "save": { - "error": "Erreur lors de l'enregistrement du panneau de bibliothèque : \"{{errorMsg}}\"", + "error": "Erreur lors de l'enregistrement du panneau de bibliothèque\u00a0: \"{{errorMsg}}\"", "success": "Panneau de bibliothèque enregistré" } }, @@ -421,7 +435,7 @@ "info-text-1": "Un instantané est un moyen instantané de partager publiquement un tableau de bord interactif. Lors de la création, nous supprimons les données sensibles telles que les requêtes (métrique, modèle et annotation) et les liens du panneau, pour ne laisser que les métriques visibles et les noms de séries intégrés dans votre tableau de bord.", "info-text-2": "N'oubliez pas que votre instantané <1>peut être consulté par une personne qui dispose du lien et qui peut accéder à l'URL. Partagez judicieusement.", "local-button": "Instantané local", - "mistake-message": "Avez-vous commis une erreur ? ", + "mistake-message": "Avez-vous commis une erreur\u00a0? ", "name": "Nom de l'instantané", "timeout": "Délai d’expiration (secondes)", "timeout-description": "Vous devrez peut-être configurer la valeur du délai d'expiration si la collecte des métriques de votre tableau de bord prend beaucoup de temps.", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index f93ff178e03..b4a79f44706 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -5,6 +5,21 @@ "success": "Cőpįęđ" } }, + "command-palette": { + "action": { + "change-theme": "Cĥäʼnģę ŧĥęmę...", + "dark-theme": "Đäřĸ", + "light-theme": "Ŀįģĥŧ", + "search": "Ŝęäřčĥ" + }, + "section": { + "actions": "Åčŧįőʼnş", + "dashboard-search-results": "Đäşĥþőäřđş", + "pages": "Päģęş", + "preferences": "Přęƒęřęʼnčęş", + "recent-dashboards": "Ŗęčęʼnŧľy vįęŵęđ đäşĥþőäřđş" + } + }, "common": { "locale": { "default": "Đęƒäūľŧ" @@ -83,7 +98,6 @@ "datasource-onboarding": { "contact-admin": "Pľęäşę čőʼnŧäčŧ yőūř äđmįʼnįşŧřäŧőř ŧő čőʼnƒįģūřę đäŧä şőūřčęş.", "explanation": "Ŧő vįşūäľįžę yőūř đäŧä, yőū'ľľ ʼnęęđ ŧő čőʼnʼnęčŧ įŧ ƒįřşŧ.", - "logo": "Ŀőģő ƒőř {{datasourceName}} đäŧä şőūřčę", "new-dashboard": "Ńęŵ đäşĥþőäřđ", "preferred": "Cőʼnʼnęčŧ yőūř přęƒęřřęđ đäŧä şőūřčę:", "sampleData": "Øř şęŧ ūp ä ʼnęŵ đäşĥþőäřđ ŵįŧĥ şämpľę đäŧä", @@ -314,7 +328,7 @@ }, "support-bundles": { "subtitle": "Đőŵʼnľőäđ şūppőřŧ þūʼnđľęş", - "title": "Ŝūppőřŧ ßūʼnđľęş" + "title": "Ŝūppőřŧ þūʼnđľęş" }, "teams": { "subtitle": "Ğřőūpş őƒ ūşęřş ŧĥäŧ ĥävę čőmmőʼn đäşĥþőäřđ äʼnđ pęřmįşşįőʼn ʼnęęđş", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index a9746d6e3b1..872a4249703 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -5,6 +5,21 @@ "success": "" } }, + "command-palette": { + "action": { + "change-theme": "", + "dark-theme": "", + "light-theme": "", + "search": "" + }, + "section": { + "actions": "", + "dashboard-search-results": "", + "pages": "", + "preferences": "", + "recent-dashboards": "" + } + }, "common": { "locale": { "default": "默认" @@ -83,7 +98,6 @@ "datasource-onboarding": { "contact-admin": "", "explanation": "", - "logo": "", "new-dashboard": "", "preferred": "", "sampleData": "",