Dashboard Library: Integrate template dashboards (#112906)
* Extend interpolate endpoint to support community dashboard json interpolation Added unit tests * Implement Frontend Side - Show tabs - Fetch Community dashboads - basic cards for community - Search bar for community * Improve card community and show thumbnails * quick poc with template dashboards * better cards ui * entry point conditional added. dashboard card improved * dev dashboard for testing * details removed in template dashboard modal * improvement when loading the templates * dashboard from template entry points * interactions added * tracking event improvements. card improvement * unused import removed * 90% Complete MVP community dashboards integraton - Use DashboardCard component - Search grafana dashboard in the community tab - Make Tabs and pagination sticky - Adjust titles to be scoped by datasource name/type - Add skeleton loading for community tabs and pagination - Add dashboard details tooltip * Use DataSourcePicker for extra configuration page * Fix suggested dashboard, don't use filter but slug and also add gcomURLink * Implement badge, for now reuse Badge component * translations added * Refactor code, extract to utils function and clean up code * refactor provisioned dashboards images * add missing file * Extract API functions * standarize event handlers * Auto select tab when provisioned or community are not present, also add empty state for provisioned tab * use SecondaryAction iconButton for Details, and also use Tooltip * For suggested dashboards change Grid for Stack to fix issue with less than 3 dashboards, also add the details next to the main action * Fix styling issue with description miss-aligment * Change "use template" to use dashboard * update i18n * fix broken unit test * FF added * uid changed to make it work for both scenarios * Apply PR feedback - add eventLocation - Fix description placeholder - Fix issue with suggested dashboards card aligment, change to Grid * use datasource type instead of name, extract i18n and fix linting * Improve View on Grafana.com link * remove console.log * Fix issue with cards styling * improvements * Apply suggestion from @juanicabanas Co-authored-by: Juan Cabanas <juan.cabanas@grafana.com> * Use query params for modal state and reorder dashboard empty to keep suggestions in the middle * extract common interpolate logic in new function * fix linting * rename loadTemplateDashboard function to loadSuggestedDashboard * Improve automapping layout, add dashboard name to title and pipe the mappings * Apply style guide for callback functions, and refactor constant inputs initialization wiht reduce * Fix styling issues with focus border and pagination * fix issue with card aligment * use attach skeleton instead of custom css * Adjust all i18n to use dashboard-library key * Refactor mapping form to use one array to handle unmapped ds * Update public/app/features/dashboard/dashgrid/DashboardLibrary/CommunityDashboardSection.tsx Co-authored-by: Juan Cabanas <juan.cabanas@grafana.com> * Refactor pagination, use api pagination data * Remove unnecesary trim * Fix issue with Stack height and justifyContent in modal * Update public/app/features/dashboard/dashgrid/DashboardLibrary/SuggestedDashboards.tsx Co-authored-by: Juan Cabanas <juan.cabanas@grafana.com> * Add missing dependency * Fix issue with mapping config form, make buttons to be aligned at the bottom * Fix unit tests * Add and refactor tracking interactions to support experiment KPIS * rename unmappedInputs to unmappedDsInputs for clarity * Update public/app/features/dashboard/dashgrid/DashboardLibrary/CommunityDashboardSection.tsx Co-authored-by: Juan Cabanas <juan.cabanas@grafana.com> * rename modal and move modal to section * Simplify search params, extrack user selected dashboards to helper function, update pagination styles and use url based pagination * Move modal logic to the suggested dashboards component * Fix tracking duplication on first load and revert change on pagination url persistence * Extrac on preview community dashboard into utils * Bring old datasource-provisioned box back and rely on new feature toggle for community dashboards * change logic for showing suggested dashboards, we only need to enable that feature toggle * update i18n * Fix unit test for basic provisioned dashboard * fix css * Apply feedback * Add suggested dashboards to endpoint * Add missing feature toggle check in the backend to expose the interpolate api * Add extra test case * update swagger * Update public/app/features/dashboard/dashgrid/DashboardEmpty/DashboardEmpty.tsx Co-authored-by: Juan Cabanas <juan.cabanas@grafana.com> * update swagger * changes applied to retrieve templates dashboards from raintank * translations * Remove duplicated tracking and add gnet id to the save tracking action * interactions * improvements applied * last improvements * tracking events modified with merge. translations fixed * tests fixed * uid property removed from dto. new way of tracking the ds types added * ds types from gnet dashboard removed * fixes * tracking changed * tracking modified --------- Co-authored-by: alexandra vargas <alexa1866@gmail.com> Co-authored-by: Alexa Vargas <239999+axelavargas@users.noreply.github.com> Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
This commit is contained in:
co-authored by
alexandra vargas
Alexa Vargas
nmarrs
parent
4581ed89a0
commit
de45393eb5
@@ -47,7 +47,7 @@ func (api *ImportDashboardAPI) RegisterAPIEndpoints(routeRegister routing.RouteR
|
||||
routing.Wrap(api.ImportDashboard),
|
||||
)
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if api.features.IsEnabledGlobally(featuremgmt.FlagDashboardLibrary) || api.features.IsEnabledGlobally(featuremgmt.FlagSuggestedDashboards) {
|
||||
if api.features.IsEnabledGlobally(featuremgmt.FlagDashboardLibrary) || api.features.IsEnabledGlobally(featuremgmt.FlagSuggestedDashboards) || api.features.IsEnabledGlobally(featuremgmt.FlagDashboardTemplates) {
|
||||
route.Post(
|
||||
"/interpolate",
|
||||
authorize(accesscontrol.EvalPermission(dashboards.ActionDashboardsCreate)),
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { getDataSourceSrv, reportInteraction, config } from '@grafana/runtime';
|
||||
import { Menu, Dropdown, ToolbarButton } from '@grafana/ui';
|
||||
import {
|
||||
CONTENT_KINDS,
|
||||
DashboardLibraryInteractions,
|
||||
SOURCE_ENTRY_POINTS,
|
||||
} from 'app/features/dashboard/dashgrid/DashboardLibrary/interactions';
|
||||
import { useSelector } from 'app/types/store';
|
||||
|
||||
import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
|
||||
@@ -15,7 +20,28 @@ export const QuickAdd = ({}: Props) => {
|
||||
const navBarTree = useSelector((state) => state.navBarTree);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const createActions = useMemo(() => findCreateActions(navBarTree), [navBarTree]);
|
||||
const createActions = useMemo(() => {
|
||||
const createActions = findCreateActions(navBarTree);
|
||||
|
||||
if (config.featureToggles.dashboardTemplates) {
|
||||
const testDataSources = getDataSourceSrv().getList({ type: 'grafana-testdata-datasource' });
|
||||
if (testDataSources.length > 0) {
|
||||
createActions.splice(1, 0, {
|
||||
id: 'browse-template-dashboard',
|
||||
text: t('navigation.quick-add.new-template-dashboard-button', 'Dashboard from template'),
|
||||
url: '/dashboards?templateDashboards=true&source=quickAdd',
|
||||
onClick: () => {
|
||||
DashboardLibraryInteractions.entryPointClicked({
|
||||
entryPoint: SOURCE_ENTRY_POINTS.QUICK_ADD_BUTTON,
|
||||
contentKind: CONTENT_KINDS.TEMPLATE_DASHBOARD,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return createActions;
|
||||
}, [navBarTree]);
|
||||
const showQuickAdd = createActions.length > 0;
|
||||
|
||||
if (!showQuickAdd) {
|
||||
@@ -30,7 +56,10 @@ export const QuickAdd = ({}: Props) => {
|
||||
key={index}
|
||||
url={createAction.url}
|
||||
label={createAction.text}
|
||||
onClick={() => reportInteraction('grafana_menu_item_clicked', { url: createAction.url, from: 'quickadd' })}
|
||||
onClick={() => {
|
||||
reportInteraction('grafana_menu_item_clicked', { url: createAction.url, from: 'quickadd' });
|
||||
createAction.onClick?.();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
// Maps the ID of the nav item to a translated phrase to later pass to <Trans />
|
||||
// Because the navigation content is dynamic (defined in the backend), we can not use
|
||||
// the normal inline message definition method.
|
||||
@@ -48,7 +49,9 @@ export function getNavTitle(navId: string | undefined) {
|
||||
case 'dashboards/recently-deleted':
|
||||
return t('nav.recently-deleted.title', 'Recently deleted');
|
||||
case 'dashboards/new':
|
||||
return t('nav.new-dashboard.title', 'New dashboard');
|
||||
return config.featureToggles.dashboardTemplates
|
||||
? t('nav.new-dashboard.empty-title', 'Empty dashboard')
|
||||
: t('nav.new-dashboard.title', 'New dashboard');
|
||||
case 'dashboards/folder/new':
|
||||
return t('nav.new-folder.title', 'New folder');
|
||||
case 'dashboards/import':
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useDispatch } from 'app/types/store';
|
||||
import { FolderRepo } from '../../core/components/NestedFolderPicker/FolderRepo';
|
||||
import { contextSrv } from '../../core/services/context_srv';
|
||||
import { ManagerKind } from '../apiserver/types';
|
||||
import { TemplateDashboardModal } from '../dashboard/dashgrid/DashboardLibrary/TemplateDashboardModal';
|
||||
import { buildNavModel, getDashboardsTabID } from '../folders/state/navModel';
|
||||
import { ProvisionedFolderPreviewBanner } from '../provisioning/components/Folders/ProvisionedFolderPreviewBanner';
|
||||
import { useGetResourceRepositoryView } from '../provisioning/hooks/useGetResourceRepositoryView';
|
||||
@@ -217,6 +218,7 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
|
||||
}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
{config.featureToggles.dashboardTemplates && <TemplateDashboardModal />}
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -2,10 +2,15 @@ import { useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { locationUtil } from '@grafana/data';
|
||||
import { config, locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { config, getDataSourceSrv, locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';
|
||||
import { useCreateFolder } from 'app/api/clients/folder/v1beta1/hooks';
|
||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||
import {
|
||||
CONTENT_KINDS,
|
||||
DashboardLibraryInteractions,
|
||||
SOURCE_ENTRY_POINTS,
|
||||
} from 'app/features/dashboard/dashgrid/DashboardLibrary/interactions';
|
||||
import { RepoType } from 'app/features/provisioning/Wizard/types';
|
||||
import { NewProvisionedFolderForm } from 'app/features/provisioning/components/Folders/NewProvisionedFolderForm';
|
||||
import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance';
|
||||
@@ -15,6 +20,7 @@ import {
|
||||
getNewDashboardPhrase,
|
||||
getNewFolderPhrase,
|
||||
getNewPhrase,
|
||||
getNewTemplateDashboardPhrase,
|
||||
} from 'app/features/search/tempI18nPhrases';
|
||||
import { FolderDTO } from 'app/types/folders';
|
||||
|
||||
@@ -44,6 +50,12 @@ export default function CreateNewButton({
|
||||
const notifyApp = useAppNotification();
|
||||
const isProvisionedInstance = useIsProvisionedInstance();
|
||||
|
||||
let renderPreBuiltDashboardAction = false;
|
||||
if (config.featureToggles.dashboardTemplates) {
|
||||
const testDataSources = getDataSourceSrv().getList({ type: 'grafana-testdata-datasource' });
|
||||
renderPreBuiltDashboardAction = testDataSources.length > 0;
|
||||
}
|
||||
|
||||
const onCreateFolder = async (folderName: string) => {
|
||||
try {
|
||||
const folder = await newFolder({
|
||||
@@ -74,16 +86,30 @@ export default function CreateNewButton({
|
||||
const newMenu = (
|
||||
<Menu>
|
||||
{canCreateDashboard && (
|
||||
<MenuItem
|
||||
label={getNewDashboardPhrase()}
|
||||
onClick={() =>
|
||||
reportInteraction('grafana_menu_item_clicked', {
|
||||
url: buildUrl('/dashboard/new', parentFolder?.uid),
|
||||
from: location.pathname,
|
||||
})
|
||||
}
|
||||
url={buildUrl('/dashboard/new', parentFolder?.uid)}
|
||||
/>
|
||||
<>
|
||||
<MenuItem
|
||||
label={getNewDashboardPhrase()}
|
||||
onClick={() =>
|
||||
reportInteraction('grafana_menu_item_clicked', {
|
||||
url: buildUrl('/dashboard/new', parentFolder?.uid),
|
||||
from: location.pathname,
|
||||
})
|
||||
}
|
||||
url={buildUrl('/dashboard/new', parentFolder?.uid)}
|
||||
/>
|
||||
{renderPreBuiltDashboardAction && (
|
||||
<MenuItem
|
||||
label={getNewTemplateDashboardPhrase()}
|
||||
onClick={() =>
|
||||
DashboardLibraryInteractions.entryPointClicked({
|
||||
entryPoint: SOURCE_ENTRY_POINTS.BROWSE_DASHBOARDS_PAGE,
|
||||
contentKind: CONTENT_KINDS.TEMPLATE_DASHBOARD,
|
||||
})
|
||||
}
|
||||
url={buildUrl('/dashboards?templateDashboards=true&source=createNewButton', parentFolder?.uid)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{canCreateFolder && <MenuItem onClick={() => setShowNewFolderDrawer(true)} label={getNewFolderPhrase()} />}
|
||||
{canCreateDashboard && !isProvisionedInstance && parentFolder?.managedBy !== ManagerKind.Repo && (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
||||
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { getDataSourceSrv, config, locationService } from '@grafana/runtime';
|
||||
import { getEnrichedHelpItem } from 'app/core/components/AppChrome/MegaMenu/utils';
|
||||
import {
|
||||
shouldRenderInviteUserButton,
|
||||
@@ -9,6 +10,11 @@ import {
|
||||
} from 'app/core/components/AppChrome/TopBar/InviteUserButtonUtils';
|
||||
import { changeTheme } from 'app/core/services/theme';
|
||||
import { currentMockApiState, toggleMockApiAndReload, togglePseudoLocale } from 'app/dev-utils';
|
||||
import {
|
||||
CONTENT_KINDS,
|
||||
DashboardLibraryInteractions,
|
||||
SOURCE_ENTRY_POINTS,
|
||||
} from 'app/features/dashboard/dashgrid/DashboardLibrary/interactions';
|
||||
import { useSelector } from 'app/types/store';
|
||||
|
||||
import { CommandPaletteAction } from '../types';
|
||||
@@ -140,7 +146,31 @@ function getGlobalActions(): CommandPaletteAction[] {
|
||||
export function useStaticActions(): CommandPaletteAction[] {
|
||||
const navBarTree = useSelector((state) => state.navBarTree);
|
||||
return useMemo(() => {
|
||||
const navBarActions = navTreeToActions(navBarTree);
|
||||
let navBarActions = navTreeToActions(navBarTree);
|
||||
|
||||
if (config.featureToggles.dashboardTemplates) {
|
||||
const testDataSources = getDataSourceSrv().getList({ type: 'grafana-testdata-datasource' });
|
||||
if (testDataSources.length > 0) {
|
||||
const navBarActionsWithoutActions = navBarActions.filter((action) => action.priority !== ACTIONS_PRIORITY);
|
||||
const navBarActionsWithActions = navBarActions.filter((action) => action.priority === ACTIONS_PRIORITY);
|
||||
|
||||
navBarActionsWithActions.splice(1, 0, {
|
||||
id: 'browse-template-dashboard',
|
||||
name: t('command-palette.action.dashboard-from-template', 'Dashboard from template'),
|
||||
section: t('command-palette.section.actions', 'Actions'),
|
||||
priority: ACTIONS_PRIORITY,
|
||||
perform: () => {
|
||||
DashboardLibraryInteractions.entryPointClicked({
|
||||
entryPoint: SOURCE_ENTRY_POINTS.COMMAND_PALETTE,
|
||||
contentKind: CONTENT_KINDS.TEMPLATE_DASHBOARD,
|
||||
});
|
||||
locationService.push('/dashboards?templateDashboards=true&source=commandPalette');
|
||||
},
|
||||
});
|
||||
|
||||
navBarActions = [...navBarActionsWithoutActions, ...navBarActionsWithActions];
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRenderInviteUserButton()) {
|
||||
navBarActions.push({
|
||||
|
||||
@@ -4,8 +4,8 @@ import { config, getBackendSrv, getDataSourceSrv, isFetchError, locationService
|
||||
import { sceneGraph } from '@grafana/scenes';
|
||||
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
import { GetRepositoryFilesWithPathApiResponse, provisioningAPIv0alpha1 } from 'app/api/clients/provisioning/v0alpha1';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { StateManagerBase } from 'app/core/services/StateManagerBase';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { getMessageFromError, getMessageIdFromError, getStatusFromError } from 'app/core/utils/errors';
|
||||
import { startMeasure, stopMeasure } from 'app/core/utils/metrics';
|
||||
import {
|
||||
@@ -444,7 +444,12 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
|
||||
const scene = transformSaveModelToScene(rsp);
|
||||
|
||||
// Special handling for Template route - set up edit mode and dirty state
|
||||
if (config.featureToggles.dashboardLibrary && options.route === DashboardRoutes.Template) {
|
||||
if (
|
||||
(config.featureToggles.dashboardLibrary ||
|
||||
config.featureToggles.suggestedDashboards ||
|
||||
config.featureToggles.dashboardTemplates) &&
|
||||
options.route === DashboardRoutes.Template
|
||||
) {
|
||||
scene.setInitialSaveModel(rsp.dashboard, rsp.meta);
|
||||
scene.onEnterEditMode();
|
||||
scene.setState({ isDirty: true });
|
||||
@@ -496,14 +501,20 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
|
||||
};
|
||||
}
|
||||
|
||||
private async loadSuggestedDashboard(): Promise<DashboardDTO> {
|
||||
private async loadDashboardLibrary(): Promise<DashboardDTO> {
|
||||
// Extract template parameters from URL
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const datasource = searchParams.get('datasource');
|
||||
const gnetId = searchParams.get('gnetId');
|
||||
const pluginId = searchParams.get('pluginId');
|
||||
|
||||
const path = searchParams.get('path');
|
||||
|
||||
// Check if this is a template dashboard
|
||||
if (gnetId && datasource && pluginId) {
|
||||
return this.loadTemplateDashboard(gnetId, datasource, pluginId);
|
||||
}
|
||||
|
||||
// Check if this is a community dashboard (has gnetId) or plugin dashboard
|
||||
if (gnetId) {
|
||||
return this.loadCommunityTemplateDashboard(gnetId);
|
||||
@@ -537,6 +548,34 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
|
||||
return this.buildDashboardDTOFromInterpolated(interpolatedDashboard);
|
||||
}
|
||||
|
||||
private async loadTemplateDashboard(
|
||||
gnetId: string,
|
||||
datasource: string | null,
|
||||
pluginId: string | null
|
||||
): Promise<DashboardDTO> {
|
||||
// Fetch the community dashboard from grafana.com
|
||||
const gnetDashboard = await getBackendSrv().get(`/api/gnet/dashboards/${gnetId}`);
|
||||
|
||||
// The dashboard JSON is in the 'json' property
|
||||
const dashboardJson = gnetDashboard.json;
|
||||
|
||||
const data = {
|
||||
dashboard: dashboardJson,
|
||||
overwrite: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '*',
|
||||
type: 'datasource',
|
||||
pluginId: pluginId,
|
||||
value: datasource,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const interpolatedDashboard = await getBackendSrv().post('/api/dashboards/interpolate', data);
|
||||
return this.buildDashboardDTOFromInterpolated(interpolatedDashboard);
|
||||
}
|
||||
|
||||
private async loadCommunityTemplateDashboard(gnetId: string): Promise<DashboardDTO> {
|
||||
// Extract mappings from URL params
|
||||
const location = locationService.getLocation();
|
||||
@@ -605,7 +644,7 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
|
||||
rsp = await buildNewDashboardSaveModel(urlFolderUid);
|
||||
break;
|
||||
case DashboardRoutes.Template:
|
||||
rsp = await this.loadSuggestedDashboard();
|
||||
rsp = await this.loadDashboardLibrary();
|
||||
break;
|
||||
case DashboardRoutes.Provisioning:
|
||||
return this.loadProvisioningDashboard(slug || '', uid);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { store } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { extractDatasourceTypesFromUrl } from 'app/features/dashboard/dashgrid/DashboardLibrary/utils/communityDashboardHelpers';
|
||||
import { getDatasourceTypes } from 'app/features/dashboard/dashgrid/DashboardLibrary/utils/dashboardLibraryHelpers';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement';
|
||||
@@ -61,18 +61,21 @@ export function trackDashboardSceneCreatedOrSaved(
|
||||
const libraryItemId = urlParams.get('libraryItemId') || urlParams.get('gnetId') || undefined;
|
||||
const creationOrigin = urlParams.get('creationOrigin') || undefined;
|
||||
|
||||
// Extract datasourceTypes from URL params (supports both community and provisioned dashboards)
|
||||
const datasourceTypes = extractDatasourceTypesFromUrl();
|
||||
// Extract datasourceTypes from URL params (supports both community and provisioned dashboards) or dashboard panels
|
||||
const datasourceTypes = getDatasourceTypes(dashboard);
|
||||
const dynamicDashboardsTrackingInformation = dashboard.getDynamicDashboardsTrackingInformation();
|
||||
|
||||
const dashboardLibraryProperties = config.featureToggles.dashboardLibrary
|
||||
? {
|
||||
datasourceTypes,
|
||||
sourceEntryPoint,
|
||||
libraryItemId,
|
||||
creationOrigin,
|
||||
}
|
||||
: {};
|
||||
const dashboardLibraryProperties =
|
||||
config.featureToggles.dashboardLibrary ||
|
||||
config.featureToggles.dashboardTemplates ||
|
||||
config.featureToggles.suggestedDashboards
|
||||
? {
|
||||
datasourceTypes,
|
||||
sourceEntryPoint,
|
||||
libraryItemId,
|
||||
creationOrigin,
|
||||
}
|
||||
: {};
|
||||
|
||||
DashboardInteractions.dashboardCreatedOrSaved(isNew, {
|
||||
...initialProperties,
|
||||
|
||||
+4
-9
@@ -94,13 +94,13 @@ export const CommunityDashboardSection = ({ onShowMapping, datasourceType }: Pro
|
||||
datasourceTypes: [ds.type],
|
||||
sourceEntryPoint: SOURCE_ENTRY_POINTS.DATASOURCE_PAGE,
|
||||
eventLocation: EVENT_LOCATIONS.MODAL_COMMUNITY_TAB,
|
||||
hasResults: apiResponse.dashboards.length > 0,
|
||||
resultCount: apiResponse.dashboards.length,
|
||||
hasResults: apiResponse.items.length > 0,
|
||||
resultCount: apiResponse.items.length,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
dashboards: apiResponse.dashboards,
|
||||
dashboards: apiResponse.items,
|
||||
pages: apiResponse.pages,
|
||||
datasourceType: ds.type,
|
||||
};
|
||||
@@ -160,7 +160,6 @@ export const CommunityDashboardSection = ({ onShowMapping, datasourceType }: Pro
|
||||
return (
|
||||
<Stack direction="column" gap={2} height="100%">
|
||||
<FilterInput
|
||||
className={styles.searchInput}
|
||||
placeholder={
|
||||
datasourceType
|
||||
? t(
|
||||
@@ -258,7 +257,7 @@ export const CommunityDashboardSection = ({ onShowMapping, datasourceType }: Pro
|
||||
onClick={() => onPreviewCommunityDashboard(dashboard)}
|
||||
isLogo={isLogo}
|
||||
details={details}
|
||||
buttonText={<Trans i18nKey="dashboard-library.card.use-dashboard-button">Use dashboard</Trans>}
|
||||
kind="suggested_dashboard"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -291,9 +290,5 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
justifyContent: 'flex-end',
|
||||
zIndex: 2,
|
||||
}),
|
||||
searchInput: css({
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ interface Props {
|
||||
isLogo?: boolean; // Indicates if imageUrl is a small logo vs full screenshot
|
||||
showDatasourceProvidedBadge?: boolean;
|
||||
dimThumbnail?: boolean; // Apply 50% opacity to thumbnail when badge is shown
|
||||
buttonText?: React.ReactNode; // Optional custom button text, defaults to "Use template"
|
||||
kind: 'template_dashboard' | 'suggested_dashboard';
|
||||
}
|
||||
|
||||
function DashboardCardComponent({
|
||||
@@ -39,7 +39,7 @@ function DashboardCardComponent({
|
||||
isLogo,
|
||||
showDatasourceProvidedBadge,
|
||||
dimThumbnail,
|
||||
buttonText,
|
||||
kind,
|
||||
}: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
@@ -53,7 +53,8 @@ function DashboardCardComponent({
|
||||
alt={title}
|
||||
className={cx(
|
||||
isLogo ? styles.logo : styles.thumbnail,
|
||||
dimThumbnail && showDatasourceProvidedBadge && styles.dimmedImage
|
||||
dimThumbnail && showDatasourceProvidedBadge && styles.dimmedImage,
|
||||
kind === 'suggested_dashboard' ? styles.thumbnailCoverImage : styles.thumbnailContainImage
|
||||
)}
|
||||
onError={(e) => {
|
||||
console.error('Failed to load image for:', title, 'URL:', imageUrl);
|
||||
@@ -81,7 +82,11 @@ function DashboardCardComponent({
|
||||
</div>
|
||||
<Card.Actions className={styles.actionsContainer}>
|
||||
<Button variant="secondary" onClick={onClick}>
|
||||
{buttonText || <Trans i18nKey="dashboard-library.card.use-template-button">Use template</Trans>}
|
||||
{kind === 'template_dashboard' ? (
|
||||
<Trans i18nKey="dashboard-library.card.use-template-button">Use template</Trans>
|
||||
) : (
|
||||
<Trans i18nKey="dashboard-library.card.use-dashboard-button">Use dashboard</Trans>
|
||||
)}
|
||||
</Button>
|
||||
{details && (
|
||||
<Tooltip interactive={true} content={<DetailsTooltipContent details={details} />} placement="right">
|
||||
@@ -155,6 +160,8 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
width: '350px',
|
||||
background: 'transparent',
|
||||
gridGap: theme.spacing(1),
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
}),
|
||||
thumbnailContainer: css({
|
||||
gridArea: 'Thumbnail',
|
||||
@@ -175,8 +182,13 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
thumbnail: css({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}),
|
||||
thumbnailCoverImage: css({
|
||||
objectFit: 'cover',
|
||||
}),
|
||||
thumbnailContainImage: css({
|
||||
objectFit: 'contain',
|
||||
}),
|
||||
logoContainer: css({
|
||||
gridArea: 'Thumbnail',
|
||||
overflow: 'hidden',
|
||||
@@ -215,12 +227,11 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
}),
|
||||
description: css({
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 1,
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
margin: 0,
|
||||
height: `calc(${theme.typography.body.lineHeight} * 1em)`, // Fixed height for 1 line
|
||||
}),
|
||||
actionsContainer: css({
|
||||
marginTop: 0,
|
||||
|
||||
@@ -157,7 +157,7 @@ export const DashboardLibrarySection = () => {
|
||||
imageUrl={imageUrl}
|
||||
dashboard={dashboard}
|
||||
onClick={() => onUseProvisionedDashboard(dashboard)}
|
||||
buttonText={<Trans i18nKey="dashboard-library.card.use-dashboard-button">Use dashboard</Trans>}
|
||||
kind="suggested_dashboard"
|
||||
/>
|
||||
);
|
||||
}) || []}
|
||||
|
||||
@@ -98,7 +98,7 @@ export const SuggestedDashboards = ({ datasourceUid }: Props) => {
|
||||
}),
|
||||
]);
|
||||
|
||||
const community = communityResponse.dashboards;
|
||||
const community = communityResponse.items;
|
||||
|
||||
// Mix: 1 provisioned + 2 community
|
||||
const mixed: MixedDashboard[] = [];
|
||||
@@ -319,7 +319,7 @@ export const SuggestedDashboards = ({ datasourceUid }: Props) => {
|
||||
onClick={() => onUseProvisionedDashboard(item.dashboard)}
|
||||
showDatasourceProvidedBadge={true}
|
||||
dimThumbnail={true}
|
||||
buttonText={<Trans i18nKey="dashboard-library.card.use-dashboard-button">Use dashboard</Trans>}
|
||||
kind="suggested_dashboard"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@@ -337,7 +337,7 @@ export const SuggestedDashboards = ({ datasourceUid }: Props) => {
|
||||
onClick={() => onPreviewCommunityDashboard(item.dashboard)}
|
||||
isLogo={isLogo}
|
||||
details={details}
|
||||
buttonText={<Trans i18nKey="dashboard-library.card.use-dashboard-button">Use dashboard</Trans>}
|
||||
kind="suggested_dashboard"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { getBackendSrv, getDataSourceSrv, locationService } from '@grafana/runtime';
|
||||
import { Box, Grid, Modal, Text, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { DASHBOARD_LIBRARY_ROUTES } from '../types';
|
||||
|
||||
import { DashboardCard } from './DashboardCard';
|
||||
import {
|
||||
CONTENT_KINDS,
|
||||
CREATION_ORIGINS,
|
||||
DashboardLibraryInteractions,
|
||||
DISCOVERY_METHODS,
|
||||
EVENT_LOCATIONS,
|
||||
SourceEntryPoint,
|
||||
TemplateDashboardSourceEntryPoint,
|
||||
} from './interactions';
|
||||
import { GnetDashboard, GnetDashboardsResponse, Link } from './types';
|
||||
|
||||
const SourceEntryPointMap: Record<string, SourceEntryPoint> = {
|
||||
quickAdd: TemplateDashboardSourceEntryPoint.QUICK_ADD_BUTTON,
|
||||
commandPalette: TemplateDashboardSourceEntryPoint.COMMAND_PALETTE,
|
||||
createNewButton: TemplateDashboardSourceEntryPoint.BROWSE_DASHBOARDS_PAGE,
|
||||
};
|
||||
|
||||
export const TemplateDashboardModal = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const isOpen = searchParams.get('templateDashboards') === 'true';
|
||||
const entryPoint = searchParams.get('source') || '';
|
||||
|
||||
const testDataSource = getDataSourceSrv().getList({ type: 'grafana-testdata-datasource' })[0];
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const onClose = () => {
|
||||
searchParams.delete('templateDashboards');
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
const onPreviewDashboardClick = async (dashboard: GnetDashboard) => {
|
||||
const sourceEntryPoint = SourceEntryPointMap[entryPoint] || 'unknown';
|
||||
DashboardLibraryInteractions.itemClicked({
|
||||
contentKind: CONTENT_KINDS.TEMPLATE_DASHBOARD,
|
||||
datasourceTypes: [String(testDataSource?.type)],
|
||||
libraryItemId: String(dashboard.id),
|
||||
libraryItemTitle: dashboard.name,
|
||||
sourceEntryPoint,
|
||||
eventLocation: EVENT_LOCATIONS.BROWSE_DASHBOARDS_PAGE,
|
||||
discoveryMethod: DISCOVERY_METHODS.BROWSE,
|
||||
});
|
||||
|
||||
const params = new URLSearchParams({
|
||||
datasource: testDataSource?.uid || '',
|
||||
title: dashboard.name,
|
||||
pluginId: String(testDataSource?.type) || '',
|
||||
gnetId: String(dashboard.id),
|
||||
// tracking event purpose values
|
||||
sourceEntryPoint,
|
||||
libraryItemId: String(dashboard.id),
|
||||
creationOrigin: CREATION_ORIGINS.DASHBOARD_LIBRARY_TEMPLATE_DASHBOARD,
|
||||
contentKind: CONTENT_KINDS.TEMPLATE_DASHBOARD,
|
||||
});
|
||||
|
||||
const templateUrl = `${DASHBOARD_LIBRARY_ROUTES.Template}?${params.toString()}`;
|
||||
locationService.push(templateUrl);
|
||||
};
|
||||
|
||||
const { value: dashboards = [], loading } = useAsync(async () => {
|
||||
if (!isOpen) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await getBackendSrv().get<GnetDashboardsResponse>(
|
||||
`/api/gnet/dashboards?orgSlug=raintank&categorySlug=templates&includeScreenshots=true`,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
showErrorAlert: false,
|
||||
}
|
||||
);
|
||||
|
||||
return response.items;
|
||||
} catch (error) {
|
||||
console.error('Error loading template dashboards ', error);
|
||||
return [];
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && !loading && dashboards.length > 0) {
|
||||
DashboardLibraryInteractions.loaded({
|
||||
numberOfItems: dashboards.length,
|
||||
contentKinds: [CONTENT_KINDS.TEMPLATE_DASHBOARD],
|
||||
datasourceTypes: [String(testDataSource?.type)],
|
||||
sourceEntryPoint: SourceEntryPointMap[entryPoint] || 'unknown',
|
||||
eventLocation: EVENT_LOCATIONS.BROWSE_DASHBOARDS_PAGE,
|
||||
});
|
||||
}
|
||||
}, [isOpen, dashboards, entryPoint, testDataSource?.type, loading]);
|
||||
|
||||
if (!testDataSource || (dashboards.length === 0 && !loading)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onDismiss={onClose}
|
||||
className={styles.modal}
|
||||
title={t('dashboard-library.template-dashboard-modal.title', 'Start a dashboard from a template')}
|
||||
contentClassName={styles.modalContent}
|
||||
>
|
||||
<div className={styles.stickyHeader}>
|
||||
<Text element="p">
|
||||
<Trans i18nKey="dashboard-library.template-dashboard-modal.description">
|
||||
Get started with Grafana templates using sample data. Connect your data to power them with real metrics.
|
||||
</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
<Box direction="column" gap={4} display="flex">
|
||||
<Grid
|
||||
gap={4}
|
||||
columns={{
|
||||
xs: 1,
|
||||
sm: 2,
|
||||
lg: 3,
|
||||
}}
|
||||
>
|
||||
{loading
|
||||
? Array.from({ length: 4 }).map((_, index) => <DashboardCard.Skeleton key={index} />)
|
||||
: dashboards?.map((dashboard) => {
|
||||
const thumbnail = dashboard.screenshots?.[0]?.links.find((l: Link) => l.rel === 'image')?.href ?? '';
|
||||
const thumbnailUrl = thumbnail ? `/api/gnet${thumbnail}` : '';
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
key={dashboard.id}
|
||||
title={dashboard.name}
|
||||
imageUrl={thumbnailUrl}
|
||||
onClick={() => onPreviewDashboardClick(dashboard)}
|
||||
dashboard={dashboard}
|
||||
kind="template_dashboard"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
modal: css({
|
||||
width: '1200px',
|
||||
}),
|
||||
stickyHeader: css({
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 2,
|
||||
backgroundColor: theme.colors.background.primary,
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(2),
|
||||
}),
|
||||
modalContent: css({
|
||||
paddingTop: 0,
|
||||
height: '100%',
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -71,11 +71,11 @@ export async function fetchCommunityDashboards(
|
||||
|
||||
// Grafana.com API returns format: { page: number, pages: number, items: GnetDashboard[] }
|
||||
// We normalize it to use "dashboards" instead of "items" for consistency
|
||||
if (result && Array.isArray(result.items)) {
|
||||
if (result) {
|
||||
return {
|
||||
page: result.page || params.page,
|
||||
pages: result.pages || 1,
|
||||
dashboards: result.items,
|
||||
items: result.items,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export async function fetchCommunityDashboards(
|
||||
return {
|
||||
page: params.page,
|
||||
pages: 1,
|
||||
dashboards: [],
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,16 +7,25 @@ export const EVENT_LOCATIONS = {
|
||||
EMPTY_DASHBOARD: 'empty_dashboard',
|
||||
MODAL_PROVISIONED_TAB: 'suggested_dashboards_modal_provisioned_tab',
|
||||
MODAL_COMMUNITY_TAB: 'suggested_dashboards_modal_community_tab',
|
||||
BROWSE_DASHBOARDS_PAGE: 'browse_dashboards_page',
|
||||
} as const;
|
||||
|
||||
export const CONTENT_KINDS = {
|
||||
DATASOURCE_DASHBOARD: 'datasource_dashboard',
|
||||
COMMUNITY_DASHBOARD: 'community_dashboard',
|
||||
TEMPLATE_DASHBOARD: 'template_dashboard',
|
||||
// in future this could also include "TEMPLATE_DASHBOARD" if/when items become templates
|
||||
} as const;
|
||||
|
||||
export const TemplateDashboardSourceEntryPoint = {
|
||||
QUICK_ADD_BUTTON: 'quick_add_button',
|
||||
COMMAND_PALETTE: 'command_palette',
|
||||
BROWSE_DASHBOARDS_PAGE: 'browse_dashboards_page_create_new_button',
|
||||
} as const;
|
||||
|
||||
export const SOURCE_ENTRY_POINTS = {
|
||||
DATASOURCE_PAGE: 'datasource_page',
|
||||
...TemplateDashboardSourceEntryPoint,
|
||||
// possible future flows: CREATE_DASHBOARD, EMPTY_STATE
|
||||
} as const;
|
||||
|
||||
@@ -28,6 +37,7 @@ export const DISCOVERY_METHODS = {
|
||||
export const CREATION_ORIGINS = {
|
||||
DASHBOARD_LIBRARY_DATASOURCE_DASHBOARD: 'dashboard_library_datasource_dashboard',
|
||||
DASHBOARD_LIBRARY_COMMUNITY_DASHBOARD: 'dashboard_library_community_dashboard',
|
||||
DASHBOARD_LIBRARY_TEMPLATE_DASHBOARD: 'dashboard_library_template_dashboard',
|
||||
} as const;
|
||||
|
||||
// Derive types from constant maps for single source of truth
|
||||
@@ -91,6 +101,9 @@ export const DashboardLibraryInteractions = {
|
||||
}) => {
|
||||
reportDashboardLibraryInteraction('mapping_form_completed', properties);
|
||||
},
|
||||
entryPointClicked: (properties: { entryPoint: SourceEntryPoint; contentKind: ContentKind }) => {
|
||||
reportDashboardLibraryInteraction('entry_point_clicked', properties);
|
||||
},
|
||||
};
|
||||
|
||||
const reportDashboardLibraryInteraction = (name: string, properties?: Record<string, unknown>) => {
|
||||
|
||||
@@ -22,7 +22,6 @@ export interface Logo {
|
||||
|
||||
export interface GnetDashboard {
|
||||
id: number;
|
||||
uid: string;
|
||||
name: string;
|
||||
description: string;
|
||||
downloads: number;
|
||||
@@ -44,5 +43,5 @@ export interface GnetDashboard {
|
||||
export interface GnetDashboardsResponse {
|
||||
page: number;
|
||||
pages: number;
|
||||
dashboards: GnetDashboard[];
|
||||
items: GnetDashboard[];
|
||||
}
|
||||
|
||||
-28
@@ -9,34 +9,6 @@ import { GnetDashboard, Link } from '../types';
|
||||
|
||||
import { InputMapping, tryAutoMapDatasources, parseConstantInputs, isDataSourceInput } from './autoMapDatasources';
|
||||
|
||||
/**
|
||||
* Extract datasource types from URL parameters for tracking purposes.
|
||||
* Supports two formats:
|
||||
* - datasourceTypes: JSON array of datasource types (for community dashboards)
|
||||
* - pluginId: Single datasource type (legacy format for provisioned dashboards)
|
||||
*
|
||||
* @returns Array of datasource type strings, or undefined if not available
|
||||
*/
|
||||
export function extractDatasourceTypesFromUrl(): string[] | undefined {
|
||||
const params = locationService.getSearchObject();
|
||||
const datasourceTypesParam = params.datasourceTypes;
|
||||
const pluginIdParam = params.pluginId;
|
||||
|
||||
if (datasourceTypesParam && typeof datasourceTypesParam === 'string') {
|
||||
try {
|
||||
return JSON.parse(datasourceTypesParam);
|
||||
} catch {
|
||||
// If parsing fails, return undefined
|
||||
return undefined;
|
||||
}
|
||||
} else if (pluginIdParam && typeof pluginIdParam === 'string') {
|
||||
// Fallback to legacy pluginId for provisioned dashboards
|
||||
return [pluginIdParam];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract thumbnail URL from dashboard screenshots
|
||||
*/
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { VizPanel } from '@grafana/scenes';
|
||||
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
|
||||
import { getQueryRunnerFor } from 'app/features/dashboard-scene/utils/utils';
|
||||
|
||||
import { CONTENT_KINDS } from '../interactions';
|
||||
|
||||
function getPanelDatasourceTypes(scene: DashboardScene): string[] {
|
||||
const types = new Set<string>();
|
||||
|
||||
const panels = scene.state.body.getVizPanels();
|
||||
|
||||
for (const child of panels) {
|
||||
const ts = panelDatasourceTypes(child);
|
||||
for (const t of ts) {
|
||||
types.add(t);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(types).sort();
|
||||
}
|
||||
|
||||
function panelDatasourceTypes(vizPanel: VizPanel) {
|
||||
const types = new Set<string>();
|
||||
|
||||
const queryRunner = getQueryRunnerFor(vizPanel);
|
||||
if (queryRunner) {
|
||||
for (const q of queryRunner.state.queries) {
|
||||
types.add(q.datasource?.type ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract datasource types from URL parameters or dashboard panels based on the content kind.
|
||||
* Supports two formats:
|
||||
* - datasourceTypes: JSON array of datasource types (for community dashboards)
|
||||
* - pluginId: Single datasource type (legacy format for provisioned dashboards)
|
||||
*
|
||||
* @returns Array of datasource type strings, or undefined if not available
|
||||
*/
|
||||
export function getDatasourceTypes(dashboard: DashboardScene): string[] | undefined {
|
||||
const params = locationService.getSearchObject();
|
||||
const datasourceTypesParam = params.datasourceTypes;
|
||||
const pluginIdParam = params.pluginId;
|
||||
const contentKind = params.contentKind;
|
||||
|
||||
switch (contentKind) {
|
||||
case CONTENT_KINDS.COMMUNITY_DASHBOARD: {
|
||||
if (datasourceTypesParam && typeof datasourceTypesParam === 'string') {
|
||||
try {
|
||||
return JSON.parse(datasourceTypesParam);
|
||||
} catch {
|
||||
// If parsing fails, return undefined
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
case CONTENT_KINDS.DATASOURCE_DASHBOARD:
|
||||
return pluginIdParam && typeof pluginIdParam === 'string' ? [pluginIdParam] : undefined;
|
||||
case CONTENT_KINDS.TEMPLATE_DASHBOARD: {
|
||||
const datasourceTypes = getPanelDatasourceTypes(dashboard);
|
||||
return datasourceTypes.length > 0 ? datasourceTypes : undefined;
|
||||
}
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// TODO: remove this when new Browse Dashboards UI is no longer feature flagged
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
export function getSearchPlaceholder(includePanels = false) {
|
||||
return includePanels
|
||||
@@ -10,7 +11,13 @@ export function getSearchPlaceholder(includePanels = false) {
|
||||
}
|
||||
|
||||
export function getNewDashboardPhrase() {
|
||||
return t('search.dashboard-actions.new-dashboard', 'New dashboard');
|
||||
return config.featureToggles.dashboardTemplates
|
||||
? t('search.dashboard-actions.empty-dashboard', 'Empty dashboard')
|
||||
: t('search.dashboard-actions.new-dashboard', 'New dashboard');
|
||||
}
|
||||
|
||||
export function getNewTemplateDashboardPhrase() {
|
||||
return t('search.dashboard-actions.new-template-dashboard', 'Dashboard from template');
|
||||
}
|
||||
|
||||
export function getNewFolderPhrase() {
|
||||
|
||||
@@ -4132,6 +4132,7 @@
|
||||
"action": {
|
||||
"change-theme": "Change theme",
|
||||
"dark-theme": "Dark",
|
||||
"dashboard-from-template": "Dashboard from template",
|
||||
"light-theme": "Light",
|
||||
"scopes": "Scopes"
|
||||
},
|
||||
@@ -5718,6 +5719,10 @@
|
||||
"suggested-dashboards-subtitle": "Browse and select from data-source provided or community dashboards",
|
||||
"suggested-dashboards-title": "Build a dashboard using suggested options for your selected data source",
|
||||
"suggested-dashboards-title-with-datasource": "Build a dashboard using suggested options for your {{datasourceType}} data source",
|
||||
"template-dashboard-modal": {
|
||||
"description": "Get started with Grafana templates using sample data. Connect your data to power them with real metrics.",
|
||||
"title": "Start a dashboard from a template"
|
||||
},
|
||||
"view-all": "View all"
|
||||
},
|
||||
"dashboard-links": {
|
||||
@@ -10528,6 +10533,7 @@
|
||||
"title": "New"
|
||||
},
|
||||
"new-dashboard": {
|
||||
"empty-title": "Empty dashboard",
|
||||
"title": "New dashboard"
|
||||
},
|
||||
"new-folder": {
|
||||
@@ -10693,7 +10699,8 @@
|
||||
"aria-label": "Profile"
|
||||
},
|
||||
"quick-add": {
|
||||
"aria-label": "New"
|
||||
"aria-label": "New",
|
||||
"new-template-dashboard-button": "Dashboard from template"
|
||||
},
|
||||
"rss-button": "Latest from the blog"
|
||||
},
|
||||
@@ -12427,10 +12434,12 @@
|
||||
}
|
||||
},
|
||||
"dashboard-actions": {
|
||||
"empty-dashboard": "Empty dashboard",
|
||||
"import": "Import",
|
||||
"new": "New",
|
||||
"new-dashboard": "New dashboard",
|
||||
"new-folder": "New folder"
|
||||
"new-folder": "New folder",
|
||||
"new-template-dashboard": "Dashboard from template"
|
||||
},
|
||||
"generate-columns": {
|
||||
"score": "Score"
|
||||
|
||||
Reference in New Issue
Block a user