String(permission.action)}\n />\n >\n );\n }\n\n if (pluginConfig?.configPages) {\n for (const configPage of pluginConfig.configPages) {\n if (pageId === configPage.id) {\n return (\n \n );\n }\n }\n }\n\n if (pageId === PluginTabIds.USAGE && pluginConfig) {\n return (\n \n );\n }\n\n if (pageId === PluginTabIds.DASHBOARDS && pluginConfig) {\n return (\n \n );\n }\n\n return (\n \n );\n}\n\nexport const getStyles = (theme: GrafanaTheme2) => ({\n container: css({\n height: '100%',\n }),\n readme: css({\n '& img': {\n maxWidth: '100%',\n },\n 'h1, h2, h3': {\n marginTop: theme.spacing(3),\n marginBottom: theme.spacing(2),\n },\n '*:first-child': {\n marginTop: 0,\n },\n li: {\n marginLeft: theme.spacing(2),\n '& > p': {\n margin: theme.spacing(1, 0),\n },\n },\n a: {\n color: theme.colors.text.link,\n '&:hover': {\n color: theme.colors.text.link,\n textDecoration: 'underline',\n },\n },\n table: {\n tableLayout: 'fixed',\n width: '100%',\n 'td, th': {\n overflowX: 'auto',\n padding: theme.spacing(0.5, 1),\n },\n 'table, th, td': {\n border: `1px solid ${theme.colors.border.medium}`,\n borderCollapse: 'collapse',\n },\n },\n }),\n});\n","import React, { ReactElement } from 'react';\n\nimport { PluginErrorCode } from '@grafana/data';\nimport { selectors } from '@grafana/e2e-selectors';\nimport { Alert } from '@grafana/ui';\n\nimport { CatalogPlugin } from '../types';\n\ntype Props = {\n className?: string;\n plugin: CatalogPlugin;\n};\n\nexport function PluginDetailsDisabledError({ className, plugin }: Props): ReactElement | null {\n if (!plugin.isDisabled) {\n return null;\n }\n\n return (\n \n {renderDescriptionFromError(plugin.error)}\n Please contact your server administrator to get this resolved.
\n \n Read more about managing plugins\n \n \n );\n}\n\nfunction renderDescriptionFromError(error?: PluginErrorCode): ReactElement {\n switch (error) {\n case PluginErrorCode.modifiedSignature:\n return (\n \n Grafana Labs checks each plugin to verify that it has a valid digital signature. While doing this, we\n discovered that the content of this plugin does not match its signature. We can not guarantee the trustworthy\n of this plugin and have therefore disabled it. We recommend you to reinstall the plugin to make sure you are\n running a verified version of this plugin.\n
\n );\n case PluginErrorCode.invalidSignature:\n return (\n \n Grafana Labs checks each plugin to verify that it has a valid digital signature. While doing this, we\n discovered that it was invalid. We can not guarantee the trustworthy of this plugin and have therefore\n disabled it. We recommend you to reinstall the plugin to make sure you are running a verified version of this\n plugin.\n
\n );\n case PluginErrorCode.missingSignature:\n return (\n \n Grafana Labs checks each plugin to verify that it has a valid digital signature. While doing this, we\n discovered that there is no signature for this plugin. We can not guarantee the trustworthy of this plugin and\n have therefore disabled it. We recommend you to reinstall the plugin to make sure you are running a verified\n version of this plugin.\n
\n );\n default:\n return (\n \n We failed to run this plugin due to an unkown reason and have therefore disabled it. We recommend you to\n reinstall the plugin to make sure you are running a working version of this plugin.\n
\n );\n }\n}\n","import React from 'react';\n\nimport { PluginErrorCode, PluginSignatureStatus } from '@grafana/data';\nimport { selectors } from '@grafana/e2e-selectors';\nimport { Alert } from '@grafana/ui';\n\nimport { CatalogPlugin } from '../types';\n\ntype Props = {\n className?: string;\n plugin: CatalogPlugin;\n};\n\n// Designed to show signature information inside the active tab on the plugin's details page\nexport function PluginDetailsSignature({ className, plugin }: Props): React.ReactElement | null {\n const isSignatureValid = plugin.signature === PluginSignatureStatus.valid;\n const isCore = plugin.signature === PluginSignatureStatus.internal;\n const isDisabled = plugin.isDisabled && isDisabledDueTooSignatureError(plugin.error);\n\n // The basic information is already available in the header\n if (isSignatureValid || isCore || isDisabled) {\n return null;\n }\n\n return (\n \n \n Grafana Labs checks each plugin to verify that it has a valid digital signature. Plugin signature verification\n is part of our security measures to ensure plugins are safe and trustworthy. Grafana Labs can’t guarantee the\n integrity of this unsigned plugin. Ask the plugin author to request it to be signed.\n
\n\n \n Read more about plugins signing.\n \n \n );\n}\n\nfunction isDisabledDueTooSignatureError(error: PluginErrorCode | undefined) {\n // If the plugin is disabled due to signature error we rely on the disabled\n // error message instad of the warning about the signature.\n\n switch (error) {\n case PluginErrorCode.invalidSignature:\n case PluginErrorCode.missingSignature:\n case PluginErrorCode.modifiedSignature:\n return true;\n\n default:\n return false;\n }\n}\n","import { useMemo } from 'react';\nimport { useLocation } from 'react-router-dom';\n\nimport { GrafanaPlugin, NavModelItem, PluginIncludeType, PluginType } from '@grafana/data';\nimport { config } from '@grafana/runtime';\nimport { contextSrv } from 'app/core/core';\nimport { AccessControlAction } from 'app/types';\n\nimport { usePluginConfig } from '../hooks/usePluginConfig';\nimport { CatalogPlugin, PluginTabIds, PluginTabLabels } from '../types';\n\ntype ReturnType = {\n error: Error | undefined;\n loading: boolean;\n navModel: NavModelItem;\n activePageId: PluginTabIds | string;\n};\n\nexport const usePluginDetailsTabs = (plugin?: CatalogPlugin, pageId?: PluginTabIds): ReturnType => {\n const { loading, error, value: pluginConfig } = usePluginConfig(plugin);\n const { pathname } = useLocation();\n const defaultTab = useDefaultPage(plugin, pluginConfig);\n const isPublished = Boolean(plugin?.isPublished);\n\n const currentPageId = pageId || defaultTab;\n const navModelChildren = useMemo(() => {\n const canConfigurePlugins = plugin && contextSrv.hasPermissionInMetadata(AccessControlAction.PluginsWrite, plugin);\n const navModelChildren: NavModelItem[] = [];\n if (isPublished) {\n navModelChildren.push({\n text: PluginTabLabels.VERSIONS,\n id: PluginTabIds.VERSIONS,\n icon: 'history',\n url: `${pathname}?page=${PluginTabIds.VERSIONS}`,\n active: PluginTabIds.VERSIONS === currentPageId,\n });\n }\n\n // Not extending the tabs with the config pages if the plugin is not installed\n if (!pluginConfig) {\n return navModelChildren;\n }\n\n if (config.featureToggles.externalServiceAccounts && (plugin?.iam || plugin?.details?.iam)) {\n navModelChildren.push({\n text: PluginTabLabels.IAM,\n icon: 'shield',\n id: PluginTabIds.IAM,\n url: `${pathname}?page=${PluginTabIds.IAM}`,\n active: PluginTabIds.IAM === currentPageId,\n });\n }\n\n if (config.featureToggles.panelTitleSearch && pluginConfig.meta.type === PluginType.panel) {\n navModelChildren.push({\n text: PluginTabLabels.USAGE,\n icon: 'list-ul',\n id: PluginTabIds.USAGE,\n url: `${pathname}?page=${PluginTabIds.USAGE}`,\n active: PluginTabIds.USAGE === currentPageId,\n });\n }\n\n if (!canConfigurePlugins) {\n return navModelChildren;\n }\n\n if (pluginConfig.meta.type === PluginType.app) {\n if (pluginConfig.angularConfigCtrl) {\n navModelChildren.push({\n text: 'Config',\n icon: 'cog',\n id: PluginTabIds.CONFIG,\n url: `${pathname}?page=${PluginTabIds.CONFIG}`,\n active: PluginTabIds.CONFIG === currentPageId,\n });\n }\n\n if (pluginConfig.configPages) {\n for (const configPage of pluginConfig.configPages) {\n navModelChildren.push({\n text: configPage.title,\n icon: configPage.icon,\n id: configPage.id,\n url: `${pathname}?page=${configPage.id}`,\n active: configPage.id === currentPageId,\n });\n }\n }\n\n if (pluginConfig.meta.includes?.find((include) => include.type === PluginIncludeType.dashboard)) {\n navModelChildren.push({\n text: 'Dashboards',\n icon: 'apps',\n id: PluginTabIds.DASHBOARDS,\n url: `${pathname}?page=${PluginTabIds.DASHBOARDS}`,\n active: PluginTabIds.DASHBOARDS === currentPageId,\n });\n }\n }\n\n return navModelChildren;\n }, [plugin, pluginConfig, pathname, isPublished, currentPageId]);\n\n const navModel: NavModelItem = {\n text: plugin?.name ?? '',\n img: plugin?.info.logos.small,\n children: [\n {\n text: PluginTabLabels.OVERVIEW,\n icon: 'file-alt',\n id: PluginTabIds.OVERVIEW,\n url: `${pathname}?page=${PluginTabIds.OVERVIEW}`,\n active: PluginTabIds.OVERVIEW === currentPageId,\n },\n ...navModelChildren,\n ],\n };\n\n return {\n error,\n loading,\n navModel,\n activePageId: currentPageId,\n };\n};\n\nfunction useDefaultPage(plugin: CatalogPlugin | undefined, pluginConfig: GrafanaPlugin | undefined | null) {\n if (!plugin || !pluginConfig) {\n return PluginTabIds.OVERVIEW;\n }\n\n const hasAccess = contextSrv.hasPermissionInMetadata(AccessControlAction.PluginsWrite, plugin);\n\n if (!hasAccess || pluginConfig.meta.type !== PluginType.app) {\n return PluginTabIds.OVERVIEW;\n }\n\n if (pluginConfig.angularConfigCtrl) {\n return PluginTabIds.CONFIG;\n }\n\n if (pluginConfig.configPages?.length) {\n return pluginConfig.configPages[0].id;\n }\n\n return PluginTabIds.OVERVIEW;\n}\n","import React from 'react';\n\nimport { PluginMeta } from '@grafana/data';\nimport { Button } from '@grafana/ui';\nimport { contextSrv } from 'app/core/core';\nimport { AccessControlAction } from 'app/types';\n\nimport { updatePluginSettings } from '../../api';\nimport { usePluginConfig } from '../../hooks/usePluginConfig';\nimport { CatalogPlugin } from '../../types';\n\ntype Props = {\n plugin: CatalogPlugin;\n};\n\nexport function GetStartedWithApp({ plugin }: Props): React.ReactElement | null {\n const { value: pluginConfig } = usePluginConfig(plugin);\n\n if (!pluginConfig) {\n return null;\n }\n\n // Enforce RBAC\n if (!contextSrv.hasPermissionInMetadata(AccessControlAction.PluginsWrite, plugin)) {\n return null;\n }\n\n const { enabled, jsonData } = pluginConfig?.meta;\n\n const enable = () =>\n updatePluginSettingsAndReload(plugin.id, {\n enabled: true,\n pinned: true,\n jsonData,\n });\n\n const disable = () => {\n updatePluginSettingsAndReload(plugin.id, {\n enabled: false,\n pinned: false,\n jsonData,\n });\n };\n\n return (\n <>\n {!enabled && (\n \n )}\n\n {enabled && (\n \n )}\n >\n );\n}\n\nconst updatePluginSettingsAndReload = async (id: string, data: Partial) => {\n try {\n await updatePluginSettings(id, data);\n\n // Reloading the page as the plugin meta changes made here wouldn't be propagated throughout the app.\n window.location.reload();\n } catch (e) {\n console.error('Error while updating the plugin', e);\n }\n};\n","import { config } from 'app/core/config';\nimport { contextSrv } from 'app/core/services/context_srv';\nimport { AccessControlAction } from 'app/types';\n\nexport function isGrafanaAdmin(): boolean {\n return config.bootData.user.isGrafanaAdmin;\n}\n\nexport function isOrgAdmin() {\n return contextSrv.hasRole('Admin');\n}\n\nexport function isDataSourceEditor() {\n return (\n contextSrv.hasPermission(AccessControlAction.DataSourcesCreate) &&\n contextSrv.hasPermission(AccessControlAction.DataSourcesWrite)\n );\n}\n","import React, { useCallback } from 'react';\n\nimport { DataSourcePluginMeta } from '@grafana/data';\nimport { config } from '@grafana/runtime';\nimport { Button } from '@grafana/ui';\nimport { useDataSourcesRoutes, addDataSource } from 'app/features/datasources/state';\nimport { useDispatch } from 'app/types';\n\nimport { isDataSourceEditor } from '../../permissions';\nimport { CatalogPlugin } from '../../types';\n\ntype Props = {\n plugin: CatalogPlugin;\n};\n\nexport function GetStartedWithDataSource({ plugin }: Props): React.ReactElement | null {\n const dispatch = useDispatch();\n const dataSourcesRoutes = useDataSourcesRoutes();\n const onAddDataSource = useCallback(() => {\n const meta = {\n name: plugin.name,\n id: plugin.id,\n } as DataSourcePluginMeta;\n\n dispatch(addDataSource(meta, dataSourcesRoutes.Edit));\n }, [dispatch, plugin, dataSourcesRoutes]);\n\n if (!isDataSourceEditor()) {\n return null;\n }\n\n const disabledButton =\n config.featureToggles.managedPluginsInstall && config.pluginAdminExternalManageEnabled && !plugin.isFullyInstalled;\n\n return (\n \n );\n}\n","import React, { ReactElement } from 'react';\n\nimport { PluginType } from '@grafana/data';\n\nimport { CatalogPlugin } from '../../types';\n\nimport { GetStartedWithApp } from './GetStartedWithApp';\nimport { GetStartedWithDataSource } from './GetStartedWithDataSource';\n\ntype Props = {\n plugin: CatalogPlugin;\n};\n\nexport function GetStartedWithPlugin({ plugin }: Props): ReactElement | null {\n if (!plugin.isInstalled || plugin.isDisabled) {\n return null;\n }\n\n switch (plugin.type) {\n case PluginType.datasource:\n return ;\n case PluginType.app:\n return ;\n default:\n return null;\n }\n}\n","import { css } from '@emotion/css';\nimport React from 'react';\n\nimport { GrafanaTheme2, PluginType } from '@grafana/data';\nimport { config, featureEnabled } from '@grafana/runtime';\nimport { HorizontalGroup, Icon, LinkButton, useStyles2 } from '@grafana/ui';\nimport { contextSrv } from 'app/core/core';\nimport { AccessControlAction } from 'app/types';\n\nimport { getExternalManageLink } from '../../helpers';\nimport { useIsRemotePluginsAvailable } from '../../state/hooks';\nimport { CatalogPlugin, PluginStatus, Version } from '../../types';\n\ninterface Props {\n plugin: CatalogPlugin;\n pluginStatus: PluginStatus;\n latestCompatibleVersion?: Version;\n}\n\nexport const InstallControlsWarning = ({ plugin, pluginStatus, latestCompatibleVersion }: Props) => {\n const styles = useStyles2(getStyles);\n const isExternallyManaged = config.pluginAdminExternalManageEnabled;\n const hasPermission = contextSrv.hasPermission(AccessControlAction.PluginsInstall);\n const isRemotePluginsAvailable = useIsRemotePluginsAvailable();\n const isCompatible = Boolean(latestCompatibleVersion);\n\n if (plugin.type === PluginType.renderer) {\n return Renderer plugins cannot be managed by the Plugin Catalog.
;\n }\n\n if (plugin.type === PluginType.secretsmanager) {\n return Secrets manager plugins cannot be managed by the Plugin Catalog.
;\n }\n\n if (plugin.isEnterprise && !featureEnabled('enterprise.plugins')) {\n return (\n \n No valid Grafana Enterprise license detected.\n \n Learn more\n \n \n );\n }\n\n if (plugin.isDev) {\n return (\n This is a development build of the plugin and can't be uninstalled.
\n );\n }\n\n if (!hasPermission && !isExternallyManaged) {\n return {statusToMessage(pluginStatus)}
;\n }\n\n if (!plugin.isPublished) {\n return (\n \n );\n }\n\n if (!isCompatible) {\n return (\n \n \n This plugin doesn't support your version of Grafana.\n
\n );\n }\n\n if (!isRemotePluginsAvailable) {\n return (\n \n The install controls have been disabled because the Grafana server cannot access grafana.com.\n
\n );\n }\n\n return null;\n};\n\nexport const getStyles = (theme: GrafanaTheme2) => {\n return {\n message: css`\n color: ${theme.colors.text.secondary};\n `,\n };\n};\n\nfunction statusToMessage(status: PluginStatus): string {\n switch (status) {\n case PluginStatus.INSTALL:\n case PluginStatus.REINSTALL:\n return `You do not have permission to install this plugin.`;\n case PluginStatus.UNINSTALL:\n return `You do not have permission to uninstall this plugin.`;\n case PluginStatus.UPDATE:\n return `You do not have permission to update this plugin.`;\n default:\n return `You do not have permission to manage this plugin.`;\n }\n}\n","import { reportInteraction } from '@grafana/runtime';\n\ntype PluginTrackingProps = {\n // The ID of the plugin (e.g. grafana-azure-monitor-datasource)\n plugin_id: string;\n // The type of the plugin (e.g. 'app' or 'datasource')\n plugin_type?: string;\n // The path where the plugin details page was rendered (e.g. /plugins/grafana-azure-monitor-datasource )\n path: string;\n};\n\nexport const trackPluginInstalled = (props: PluginTrackingProps) => {\n reportInteraction('grafana_plugin_install_clicked', props);\n};\n\nexport const trackPluginUninstalled = (props: PluginTrackingProps) => {\n reportInteraction('grafana_plugin_uninstall_clicked', props);\n};\n","import React, { useEffect, useState } from 'react';\nimport { useLocation } from 'react-router-dom';\n\nimport { AppEvents } from '@grafana/data';\nimport { config, locationService } from '@grafana/runtime';\nimport { Button, HorizontalGroup, ConfirmModal } from '@grafana/ui';\nimport appEvents from 'app/core/app_events';\nimport configCore from 'app/core/config';\nimport { useQueryParams } from 'app/core/hooks/useQueryParams';\nimport { removePluginFromNavTree } from 'app/core/reducers/navBarTree';\nimport { useDispatch } from 'app/types';\n\nimport {\n useInstallStatus,\n useUninstallStatus,\n useInstall,\n useUninstall,\n useUnsetInstall,\n useFetchDetailsLazy,\n} from '../../state/hooks';\nimport { trackPluginInstalled, trackPluginUninstalled } from '../../tracking';\nimport { CatalogPlugin, PluginStatus, PluginTabIds, Version } from '../../types';\n\ntype InstallControlsButtonProps = {\n plugin: CatalogPlugin;\n pluginStatus: PluginStatus;\n latestCompatibleVersion?: Version;\n hasInstallWarning?: boolean;\n setNeedReload?: (needReload: boolean) => void;\n};\n\nexport function InstallControlsButton({\n plugin,\n pluginStatus,\n latestCompatibleVersion,\n hasInstallWarning,\n setNeedReload,\n}: InstallControlsButtonProps) {\n const dispatch = useDispatch();\n const [queryParams] = useQueryParams();\n const location = useLocation();\n const { isInstalling, error: errorInstalling } = useInstallStatus();\n const { isUninstalling, error: errorUninstalling } = useUninstallStatus();\n const install = useInstall();\n const uninstall = useUninstall();\n const unsetInstall = useUnsetInstall();\n const fetchDetails = useFetchDetailsLazy();\n const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false);\n const showConfirmModal = () => setIsConfirmModalVisible(true);\n const hideConfirmModal = () => setIsConfirmModalVisible(false);\n const uninstallBtnText = isUninstalling ? 'Uninstalling' : 'Uninstall';\n const trackingProps = {\n plugin_id: plugin.id,\n plugin_type: plugin.type,\n path: location.pathname,\n };\n\n useEffect(() => {\n return () => {\n // Remove possible installation errors\n unsetInstall();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const onInstall = async () => {\n trackPluginInstalled(trackingProps);\n const result = await install(plugin.id, latestCompatibleVersion?.version);\n if (!errorInstalling && !('error' in result)) {\n let successMessage = `Installed ${plugin.name}`;\n if (config.pluginAdminExternalManageEnabled && configCore.featureToggles.managedPluginsInstall) {\n successMessage = 'Install requested, this may take a few minutes.';\n }\n\n appEvents.emit(AppEvents.alertSuccess, [successMessage]);\n if (plugin.type === 'app') {\n setNeedReload?.(true);\n }\n\n await fetchDetails(plugin.id);\n }\n };\n\n const onUninstall = async () => {\n hideConfirmModal();\n trackPluginUninstalled(trackingProps);\n await uninstall(plugin.id);\n if (!errorUninstalling) {\n // If an app plugin is uninstalled we need to reset the active tab when the config / dashboards tabs are removed.\n const activePageId = queryParams.page;\n const isViewingAppConfigPage = activePageId !== PluginTabIds.OVERVIEW && activePageId !== PluginTabIds.VERSIONS;\n if (isViewingAppConfigPage) {\n locationService.replace(`${location.pathname}?page=${PluginTabIds.OVERVIEW}`);\n }\n\n let successMessage = `Uninstalled ${plugin.name}`;\n if (config.pluginAdminExternalManageEnabled && configCore.featureToggles.managedPluginsInstall) {\n successMessage = 'Uninstall requested, this may take a few minutes.';\n }\n\n appEvents.emit(AppEvents.alertSuccess, [successMessage]);\n if (plugin.type === 'app') {\n dispatch(removePluginFromNavTree({ pluginID: plugin.id }));\n setNeedReload?.(false);\n }\n }\n };\n\n const onUpdate = async () => {\n await install(plugin.id, latestCompatibleVersion?.version, true);\n if (!errorInstalling) {\n appEvents.emit(AppEvents.alertSuccess, [`Updated ${plugin.name}`]);\n }\n };\n\n if (pluginStatus === PluginStatus.UNINSTALL) {\n return (\n <>\n \n \n \n \n >\n );\n }\n\n if (!plugin.isPublished || hasInstallWarning) {\n // Cannot be updated or installed\n return null;\n }\n\n if (pluginStatus === PluginStatus.UPDATE) {\n return (\n \n \n \n \n );\n }\n const shouldDisable = isInstalling || errorInstalling || (!config.angularSupportEnabled && plugin.angularDetected);\n return (\n \n );\n}\n","import React from 'react';\n\nimport { config } from '@grafana/runtime';\nimport { HorizontalGroup, LinkButton } from '@grafana/ui';\n\nimport { getExternalManageLink } from '../../helpers';\nimport { PluginStatus } from '../../types';\n\ntype ExternallyManagedButtonProps = {\n pluginId: string;\n pluginStatus: PluginStatus;\n angularDetected?: boolean;\n};\n\nexport function ExternallyManagedButton({ pluginId, pluginStatus, angularDetected }: ExternallyManagedButtonProps) {\n const externalManageLink = `${getExternalManageLink(pluginId)}/?tab=installation`;\n\n if (pluginStatus === PluginStatus.UPDATE) {\n return (\n \n \n Update via grafana.com\n \n \n Uninstall via grafana.com\n \n \n );\n }\n\n if (pluginStatus === PluginStatus.UNINSTALL) {\n return (\n \n Uninstall via grafana.com\n \n );\n }\n\n return (\n \n Install via grafana.com\n \n );\n}\n","import { css } from '@emotion/css';\nimport React, { useState } from 'react';\n\nimport { GrafanaTheme2 } from '@grafana/data';\nimport { config } from '@grafana/runtime';\nimport { HorizontalGroup, Icon, useStyles2, VerticalGroup } from '@grafana/ui';\nimport configCore from 'app/core/config';\n\nimport { GetStartedWithPlugin } from '../components/GetStartedWithPlugin';\nimport { InstallControlsButton } from '../components/InstallControls';\nimport { ExternallyManagedButton } from '../components/InstallControls/ExternallyManagedButton';\nimport { getLatestCompatibleVersion, hasInstallControlWarning, isInstallControlsEnabled } from '../helpers';\nimport { useIsRemotePluginsAvailable } from '../state/hooks';\nimport { CatalogPlugin, PluginStatus } from '../types';\n\ninterface Props {\n plugin?: CatalogPlugin;\n}\n\nexport const PluginActions = ({ plugin }: Props) => {\n const styles = useStyles2(getStyles);\n const isRemotePluginsAvailable = useIsRemotePluginsAvailable();\n const latestCompatibleVersion = getLatestCompatibleVersion(plugin?.details?.versions);\n const [needReload, setNeedReload] = useState(false);\n\n if (!plugin) {\n return null;\n }\n\n const hasInstallWarning = hasInstallControlWarning(plugin, isRemotePluginsAvailable, latestCompatibleVersion);\n const isExternallyManaged = config.pluginAdminExternalManageEnabled;\n const pluginStatus = plugin.isInstalled\n ? plugin.hasUpdate\n ? PluginStatus.UPDATE\n : PluginStatus.UNINSTALL\n : PluginStatus.INSTALL;\n const isInstallControlsDisabled = plugin.isCore || plugin.isDisabled || !isInstallControlsEnabled();\n\n return (\n \n \n {!isInstallControlsDisabled && (\n <>\n {isExternallyManaged && !hasInstallWarning && !configCore.featureToggles.managedPluginsInstall ? (\n \n ) : (\n \n )}\n >\n )}\n \n \n {needReload && (\n \n \n Refresh the page to see the changes\n \n )}\n \n );\n};\n\nconst getStyles = (theme: GrafanaTheme2) => {\n return {\n message: css`\n color: ${theme.colors.text.secondary};\n `,\n };\n};\n","import { css } from '@emotion/css';\nimport React from 'react';\n\nimport { GrafanaTheme2 } from '@grafana/data';\nimport { Alert, useStyles2 } from '@grafana/ui';\n\nimport { InstallControlsWarning } from '../components/InstallControls';\nimport { getLatestCompatibleVersion, hasInstallControlWarning } from '../helpers';\nimport { useInstallStatus, useIsRemotePluginsAvailable } from '../state/hooks';\nimport { CatalogPlugin, PluginStatus } from '../types';\n\ninterface Props {\n plugin?: CatalogPlugin;\n}\n\nexport const PluginSubtitle = ({ plugin }: Props) => {\n const isRemotePluginsAvailable = useIsRemotePluginsAvailable();\n const styles = useStyles2(getStyles);\n const { error: errorInstalling } = useInstallStatus();\n if (!plugin) {\n return null;\n }\n const latestCompatibleVersion = getLatestCompatibleVersion(plugin.details?.versions);\n const pluginStatus = plugin.isInstalled\n ? plugin.hasUpdate\n ? PluginStatus.UPDATE\n : PluginStatus.UNINSTALL\n : PluginStatus.INSTALL;\n\n return (\n \n {errorInstalling && (\n
\n {typeof errorInstalling === 'string' ? errorInstalling : errorInstalling.error}\n \n )}\n {plugin?.description &&
{plugin?.description}
}\n {plugin?.details?.links && plugin.details.links.length > 0 && (\n
\n {plugin.details.links.map((link, index) => (\n \n {index > 0 && ' | '}\n \n {link.name}\n \n \n ))}\n \n )}\n {hasInstallControlWarning(plugin, isRemotePluginsAvailable, latestCompatibleVersion) && (\n
\n )}\n
\n );\n};\n\nexport const getStyles = (theme: GrafanaTheme2) => {\n return {\n subtitle: css`\n display: flex;\n flex-direction: column;\n gap: ${theme.spacing(1)};\n `,\n };\n};\n","import { css } from '@emotion/css';\nimport React from 'react';\n\nimport { GrafanaTheme2 } from '@grafana/data';\nimport { useStyles2, Icon, Stack } from '@grafana/ui';\n\nimport { CatalogPlugin, PluginIconName } from '../types';\n\ntype Props = {\n plugin: CatalogPlugin;\n grafanaDependency?: string;\n className?: string;\n};\n\nexport function PluginDetailsHeaderDependencies({ plugin, grafanaDependency }: Props): React.ReactElement | null {\n const styles = useStyles2(getStyles);\n const pluginDependencies = plugin.details?.pluginDependencies;\n const hasNoDependencyInfo = !grafanaDependency && (!pluginDependencies || !pluginDependencies.length);\n\n if (hasNoDependencyInfo) {\n return null;\n }\n\n return (\n \n {/* Grafana dependency */}\n {Boolean(grafanaDependency) && (\n \n \n Grafana {grafanaDependency}\n
\n )}\n\n {/* Plugin dependencies */}\n {pluginDependencies && pluginDependencies.length > 0 && (\n \n {pluginDependencies.map((p) => {\n return (\n \n \n {p.name} {p.version}\n \n );\n })}\n
\n )}\n \n );\n}\n\nexport const getStyles = (theme: GrafanaTheme2) => {\n return {\n dependencyTitle: css`\n margin-right: ${theme.spacing(0.5)};\n\n &::after {\n content: '';\n padding: 0;\n }\n `,\n depBadge: css({\n display: 'flex',\n alignItems: 'flex-start',\n }),\n icon: css`\n color: ${theme.colors.text.secondary};\n margin-right: ${theme.spacing(0.5)};\n `,\n };\n};\n","import { css } from '@emotion/css';\nimport { capitalize } from 'lodash';\nimport React from 'react';\n\nimport { GrafanaTheme2, PluginSignatureType } from '@grafana/data';\nimport { useStyles2, Icon, Badge, IconName } from '@grafana/ui';\n\nconst SIGNATURE_ICONS: Record = {\n [PluginSignatureType.grafana]: 'grafana',\n [PluginSignatureType.commercial]: 'shield',\n [PluginSignatureType.community]: 'shield',\n DEFAULT: 'shield-exclamation',\n};\n\ntype Props = {\n signatureType?: PluginSignatureType;\n signatureOrg?: string;\n};\n\n// Shows more information about a valid signature\nexport function PluginSignatureDetailsBadge({ signatureType, signatureOrg = '' }: Props): React.ReactElement | null {\n const styles = useStyles2(getStyles);\n\n if (!signatureType && !signatureOrg) {\n return null;\n }\n\n const signatureTypeText = signatureType === PluginSignatureType.grafana ? 'Grafana Labs' : capitalize(signatureType);\n const signatureIcon = SIGNATURE_ICONS[signatureType || ''] || SIGNATURE_ICONS.DEFAULT;\n\n return (\n <>\n \n \n Level: \n \n \n {signatureTypeText}\n
\n \n\n \n Signed by: {signatureOrg}\n \n >\n );\n}\n\nexport const DetailsBadge = ({ children }: React.PropsWithChildren<{}>) => {\n const styles = useStyles2(getStyles);\n\n return ;\n};\n\nconst getStyles = (theme: GrafanaTheme2) => ({\n badge: css`\n background-color: ${theme.colors.background.canvas};\n border-color: ${theme.colors.border.strong};\n color: ${theme.colors.text.secondary};\n white-space: nowrap;\n `,\n detailsWrapper: css`\n align-items: center;\n display: flex;\n `,\n strong: css`\n color: ${theme.colors.text.primary};\n `,\n icon: css`\n margin-right: ${theme.spacing(0.5)};\n `,\n});\n","import { css } from '@emotion/css';\nimport React from 'react';\n\nimport { GrafanaTheme2, PluginSignatureStatus } from '@grafana/data';\nimport { PluginSignatureBadge, useStyles2 } from '@grafana/ui';\n\nimport { CatalogPlugin } from '../types';\n\nimport { PluginSignatureDetailsBadge } from './PluginSignatureDetailsBadge';\n\ntype Props = {\n plugin: CatalogPlugin;\n};\n\n// Designed to show plugin signature information in the header on the plugin's details page\nexport function PluginDetailsHeaderSignature({ plugin }: Props): React.ReactElement {\n const styles = useStyles2(getStyles);\n const isSignatureValid = plugin.signature === PluginSignatureStatus.valid;\n\n return (\n \n
\n \n \n\n {isSignatureValid && (\n
\n )}\n
\n );\n}\n\nexport const getStyles = (theme: GrafanaTheme2) => {\n return {\n container: css`\n display: flex;\n flex-wrap: wrap;\n gap: ${theme.spacing(0.5)};\n `,\n link: css`\n display: inline-flex;\n align-items: center;\n `,\n };\n};\n","import { css } from '@emotion/css';\nimport React from 'react';\n\nimport { GrafanaTheme2, PluginSignatureType } from '@grafana/data';\n\nimport { PageInfoItem } from '../../../../core/components/Page/types';\nimport { PluginDisabledBadge } from '../components/Badges';\nimport { PluginDetailsHeaderDependencies } from '../components/PluginDetailsHeaderDependencies';\nimport { PluginDetailsHeaderSignature } from '../components/PluginDetailsHeaderSignature';\nimport { getLatestCompatibleVersion } from '../helpers';\nimport { CatalogPlugin } from '../types';\n\nexport const usePluginInfo = (plugin?: CatalogPlugin): PageInfoItem[] => {\n const info: PageInfoItem[] = [];\n\n if (!plugin) {\n return info;\n }\n\n // Populate info\n const latestCompatibleVersion = getLatestCompatibleVersion(plugin.details?.versions);\n const useLatestCompatibleInfo = !plugin.isInstalled;\n let version = plugin.installedVersion;\n if (!version && useLatestCompatibleInfo && latestCompatibleVersion?.version) {\n version = latestCompatibleVersion?.version;\n }\n\n if (Boolean(version)) {\n info.push({\n label: 'Version',\n value: version,\n });\n }\n\n if (Boolean(plugin.orgName)) {\n info.push({\n label: 'From',\n value: plugin.orgName,\n });\n }\n\n const showDownloads =\n !plugin.signatureType ||\n plugin.signatureType === PluginSignatureType.community ||\n plugin.signatureType === PluginSignatureType.commercial;\n if (showDownloads && Boolean(plugin.downloads > 0)) {\n info.push({\n label: 'Downloads',\n value: new Intl.NumberFormat().format(plugin.downloads),\n });\n }\n\n const pluginDependencies = plugin.details?.pluginDependencies;\n let grafanaDependency = plugin.details?.grafanaDependency;\n if (useLatestCompatibleInfo && latestCompatibleVersion?.grafanaDependency) {\n grafanaDependency = latestCompatibleVersion?.grafanaDependency;\n }\n const hasNoDependencyInfo = !grafanaDependency && (!pluginDependencies || !pluginDependencies.length);\n\n if (!hasNoDependencyInfo) {\n info.push({\n label: 'Dependencies',\n value: ,\n });\n }\n\n if (plugin.isDisabled) {\n info.push({\n label: 'Status',\n value: ,\n });\n }\n\n info.push({\n label: 'Signature',\n value: ,\n });\n\n return info;\n};\n\nexport const getStyles = (theme: GrafanaTheme2) => {\n return {\n subtitle: css`\n display: flex;\n flex-direction: column;\n gap: ${theme.spacing(1)};\n `,\n };\n};\n","import React from 'react';\n\nimport { PageInfoItem } from '../../../../core/components/Page/types';\nimport { PluginActions } from '../components/PluginActions';\nimport { PluginSubtitle } from '../components/PluginSubtitle';\nimport { CatalogPlugin } from '../types';\n\nimport { usePluginInfo } from './usePluginInfo';\n\ntype ReturnType = {\n actions: React.ReactNode;\n info: PageInfoItem[];\n subtitle: React.ReactNode;\n};\n\nexport const usePluginPageExtensions = (plugin?: CatalogPlugin): ReturnType => {\n const info = usePluginInfo(plugin);\n\n return {\n actions: ,\n info,\n subtitle: ,\n };\n};\n","import React, { useState } from 'react';\n\nimport { renderMarkdown } from '@grafana/data';\nimport { Alert } from '@grafana/ui';\n\nimport { CatalogPlugin } from '../types';\n\ntype Props = {\n className?: string;\n plugin: CatalogPlugin;\n};\n\nexport function PluginDetailsDeprecatedWarning(props: Props): React.ReactElement | null {\n const { className, plugin } = props;\n const [dismissed, setDismissed] = useState(false);\n const isWarningVisible = plugin.isDeprecated && !dismissed;\n\n return isWarningVisible ? (\n setDismissed(true)}>\n \n This {plugin.type} plugin is{' '}\n \n deprecated\n {' '}\n and has been removed from the catalog.\n
\n\n {/* Additional contextual deprecation message supporting markdown */}\n {plugin.details?.statusContext && (\n \n )}\n \n ) : null;\n}\n","import { css } from '@emotion/css';\nimport React from 'react';\nimport { useLocation } from 'react-router-dom';\n\nimport { GrafanaTheme2, NavModelItem } from '@grafana/data';\nimport { config } from '@grafana/runtime';\nimport { useStyles2, TabContent, Alert } from '@grafana/ui';\nimport { Layout } from '@grafana/ui/src/components/Layout/Layout';\nimport { Page } from 'app/core/components/Page/Page';\nimport { AppNotificationSeverity } from 'app/types';\n\nimport { AngularDeprecationPluginNotice } from '../../angularDeprecation/AngularDeprecationPluginNotice';\nimport { Loader } from '../components/Loader';\nimport { PluginDetailsBody } from '../components/PluginDetailsBody';\nimport { PluginDetailsDisabledError } from '../components/PluginDetailsDisabledError';\nimport { PluginDetailsSignature } from '../components/PluginDetailsSignature';\nimport { usePluginDetailsTabs } from '../hooks/usePluginDetailsTabs';\nimport { usePluginPageExtensions } from '../hooks/usePluginPageExtensions';\nimport { useGetSingle, useFetchStatus, useFetchDetailsStatus } from '../state/hooks';\nimport { PluginTabIds } from '../types';\n\nimport { PluginDetailsDeprecatedWarning } from './PluginDetailsDeprecatedWarning';\n\nexport type Props = {\n // The ID of the plugin\n pluginId: string;\n // The navigation ID used for displaying the sidebar navigation\n navId?: string;\n // Can be used to customise the title & subtitle for the not found page\n notFoundNavModel?: NavModelItem;\n // Can be used to customise the content shown when a plugin with the given ID cannot be found\n notFoundComponent?: React.ReactElement;\n};\n\nexport function PluginDetailsPage({\n pluginId,\n navId = 'plugins',\n notFoundComponent = ,\n notFoundNavModel = {\n text: 'Unknown plugin',\n subTitle: 'The requested ID does not belong to any plugin',\n active: true,\n },\n}: Props) {\n const location = useLocation();\n const queryParams = new URLSearchParams(location.search);\n const plugin = useGetSingle(pluginId); // fetches the plugin settings for this Grafana instance\n const { navModel, activePageId } = usePluginDetailsTabs(plugin, queryParams.get('page') as PluginTabIds);\n const { actions, info, subtitle } = usePluginPageExtensions(plugin);\n const { isLoading: isFetchLoading } = useFetchStatus();\n const { isLoading: isFetchDetailsLoading } = useFetchDetailsStatus();\n const styles = useStyles2(getStyles);\n\n if (isFetchLoading || isFetchDetailsLoading) {\n return (\n \n \n \n );\n }\n\n if (!plugin) {\n return (\n \n {notFoundComponent}\n \n );\n }\n\n return (\n \n \n \n {plugin.angularDetected && (\n \n )}\n \n \n \n \n \n \n \n );\n}\n\nexport const getStyles = (theme: GrafanaTheme2) => {\n return {\n alert: css`\n margin-bottom: ${theme.spacing(2)};\n `,\n subtitle: css`\n display: flex;\n flex-direction: column;\n gap: ${theme.spacing(1)};\n `,\n // Needed due to block formatting context\n tabContent: css`\n overflow: auto;\n height: 100%;\n padding-left: 5px;\n `,\n };\n};\n\nfunction NotFoundPlugin() {\n return (\n \n \n That plugin cannot be found. Please check the url is correct or
\n go to the plugin catalog.\n \n \n );\n}\n","import { createSelector } from '@reduxjs/toolkit';\n\nimport { PluginError, PluginType, unEscapeStringFromRegex } from '@grafana/data';\n\nimport { filterByKeyword } from '../helpers';\nimport { RequestStatus, PluginCatalogStoreState } from '../types';\n\nimport { pluginsAdapter } from './reducer';\n\nexport const selectRoot = (state: PluginCatalogStoreState) => state.plugins;\n\nexport const selectItems = createSelector(selectRoot, ({ items }) => items);\n\nexport const selectDisplayMode = createSelector(selectRoot, ({ settings }) => settings.displayMode);\n\nexport const { selectAll, selectById } = pluginsAdapter.getSelectors(selectItems);\n\nexport type PluginFilters = {\n // Searches for a string in certain fields (e.g. \"name\" or \"orgName\")\n // (Note: this will be an escaped regex string as it comes from `FilterInput`)\n keyword?: string;\n\n // (Optional, only applied if set)\n type?: PluginType;\n\n // (Optional, only applied if set)\n isInstalled?: boolean;\n\n // (Optional, only applied if set)\n isEnterprise?: boolean;\n};\n\nexport const selectPlugins = (filters: PluginFilters) =>\n createSelector(selectAll, (plugins) => {\n const keyword = filters.keyword ? unEscapeStringFromRegex(filters.keyword.toLowerCase()) : '';\n const filteredPluginIds = keyword !== '' ? filterByKeyword(plugins, keyword) : null;\n\n return plugins.filter((plugin) => {\n if (keyword && filteredPluginIds == null) {\n return false;\n }\n\n if (keyword && !filteredPluginIds?.includes(plugin.id)) {\n return false;\n }\n\n if (filters.type && plugin.type !== filters.type) {\n return false;\n }\n\n if (filters.isInstalled !== undefined && plugin.isInstalled !== filters.isInstalled) {\n return false;\n }\n\n if (filters.isEnterprise !== undefined && plugin.isEnterprise !== filters.isEnterprise) {\n return false;\n }\n\n return true;\n });\n });\n\nexport const selectPluginErrors = (filterByPluginType?: PluginType) =>\n createSelector(selectAll, (plugins) => {\n const pluginErrors: PluginError[] = [];\n for (const plugin of plugins) {\n if (plugin.error && (!filterByPluginType || plugin.type === filterByPluginType)) {\n pluginErrors.push({\n pluginId: plugin.id,\n errorCode: plugin.error,\n pluginType: plugin.type,\n });\n }\n }\n return pluginErrors;\n });\n\n// The following selectors are used to get information about the outstanding or completed plugins-related network requests.\nexport const selectRequest = (actionType: string) =>\n createSelector(selectRoot, ({ requests = {} }) => requests[actionType]);\n\nexport const selectIsRequestPending = (actionType: string) =>\n createSelector(selectRequest(actionType), (request) => request?.status === RequestStatus.Pending);\n\nexport const selectRequestError = (actionType: string) =>\n createSelector(selectRequest(actionType), (request) =>\n request?.status === RequestStatus.Rejected ? request?.error : null\n );\n\nexport const selectIsRequestNotFetched = (actionType: string) =>\n createSelector(selectRequest(actionType), (request) => request === undefined);\n","import { useEffect, useMemo } from 'react';\n\nimport { PluginError, PluginType } from '@grafana/data';\nimport { useDispatch, useSelector } from 'app/types';\n\nimport { sortPlugins, Sorters } from '../helpers';\nimport { CatalogPlugin, PluginListDisplayMode } from '../types';\n\nimport { fetchAll, fetchDetails, fetchRemotePlugins, install, uninstall, fetchAllLocal, unsetInstall } from './actions';\nimport { setDisplayMode } from './reducer';\nimport {\n selectPlugins,\n selectById,\n selectIsRequestPending,\n selectRequestError,\n selectIsRequestNotFetched,\n selectDisplayMode,\n selectPluginErrors,\n type PluginFilters,\n} from './selectors';\n\nexport const useGetAll = (filters: PluginFilters, sortBy: Sorters = Sorters.nameAsc) => {\n useFetchAll();\n\n const selector = useMemo(() => selectPlugins(filters), [filters]);\n const plugins = useSelector(selector);\n // As the locally installed plugins load quicker than the remote ones, we only show a loading state until these are being loaded\n // (In case the remote ones are not loaded within a reasonable timeout, we will merge those with the locally installed plugins once they are loaded)\n const { isLoading, error } = useLocalFetchStatus();\n const sortedPlugins = sortPlugins(plugins, sortBy);\n\n return {\n isLoading,\n error,\n plugins: sortedPlugins,\n };\n};\n\nexport const useGetSingle = (id: string): CatalogPlugin | undefined => {\n useFetchAll();\n useFetchDetails(id);\n\n return useSelector((state) => selectById(state, id));\n};\n\nexport const useGetSingleLocalWithoutDetails = (id: string): CatalogPlugin | undefined => {\n useFetchAllLocal();\n return useSelector((state) => selectById(state, id));\n};\n\nexport const useGetErrors = (filterByPluginType?: PluginType): PluginError[] => {\n useFetchAll();\n\n return useSelector(selectPluginErrors(filterByPluginType));\n};\n\nexport const useInstall = () => {\n const dispatch = useDispatch();\n return (id: string, version?: string, isUpdating?: boolean) => dispatch(install({ id, version, isUpdating }));\n};\n\nexport const useUnsetInstall = () => {\n const dispatch = useDispatch();\n\n return () => dispatch(unsetInstall());\n};\n\nexport const useUninstall = () => {\n const dispatch = useDispatch();\n\n return (id: string) => dispatch(uninstall(id));\n};\n\nexport const useIsRemotePluginsAvailable = () => {\n const error = useSelector(selectRequestError(fetchRemotePlugins.typePrefix));\n return error === null;\n};\n\nexport const useLocalFetchStatus = () => {\n const isLoading = useSelector(selectIsRequestPending('plugins/fetchLocal'));\n const error = useSelector(selectRequestError('plugins/fetchLocal'));\n\n return { isLoading, error };\n};\n\nexport const useFetchStatus = () => {\n const isLoading = useSelector(selectIsRequestPending(fetchAll.typePrefix));\n const error = useSelector(selectRequestError(fetchAll.typePrefix));\n\n return { isLoading, error };\n};\n\nexport const useFetchDetailsStatus = () => {\n const isLoading = useSelector(selectIsRequestPending(fetchDetails.typePrefix));\n const error = useSelector(selectRequestError(fetchDetails.typePrefix));\n\n return { isLoading, error };\n};\n\nexport const useInstallStatus = () => {\n const isInstalling = useSelector(selectIsRequestPending(install.typePrefix));\n const error = useSelector(selectRequestError(install.typePrefix));\n\n return { isInstalling, error };\n};\n\nexport const useUninstallStatus = () => {\n const isUninstalling = useSelector(selectIsRequestPending(uninstall.typePrefix));\n const error = useSelector(selectRequestError(uninstall.typePrefix));\n\n return { isUninstalling, error };\n};\n\n// Only fetches in case they were not fetched yet\nexport const useFetchAll = () => {\n const dispatch = useDispatch();\n const isNotFetched = useSelector(selectIsRequestNotFetched(fetchAll.typePrefix));\n\n useEffect(() => {\n isNotFetched && dispatch(fetchAll());\n }, []); // eslint-disable-line\n};\n\n// Only fetches in case they were not fetched yet\nexport const useFetchAllLocal = () => {\n const dispatch = useDispatch();\n const isNotFetched = useSelector(selectIsRequestNotFetched(fetchAllLocal.typePrefix));\n\n useEffect(() => {\n isNotFetched && dispatch(fetchAllLocal());\n }, []); // eslint-disable-line\n};\n\nexport const useFetchDetails = (id: string) => {\n const dispatch = useDispatch();\n const plugin = useSelector((state) => selectById(state, id));\n const isNotFetching = !useSelector(selectIsRequestPending(fetchDetails.typePrefix));\n const shouldFetch = isNotFetching && plugin && !plugin.details;\n\n useEffect(() => {\n shouldFetch && dispatch(fetchDetails(id));\n }, [plugin]); // eslint-disable-line\n};\n\nexport const useFetchDetailsLazy = () => {\n const dispatch = useDispatch();\n\n return (id: string) => dispatch(fetchDetails(id));\n};\n\nexport const useDisplayMode = () => {\n const dispatch = useDispatch();\n const displayMode = useSelector(selectDisplayMode);\n\n return {\n displayMode,\n setDisplayMode: (v: PluginListDisplayMode) => dispatch(setDisplayMode(v)),\n };\n};\n"],"names":["DashboardsTable","dashboards","onImport","onRemove","buttonText","dashboard","index","PluginDisabledBadge","error","tooltip","errorCodeToTooltip","Badge","getBadgeColor","theme","PluginInstalledBadge","customBadgeStyles","PluginEnterpriseBadge","plugin","onClick","ev","PluginSignatureBadge","Button","PluginUpdateAvailableBadge","styles","getStyles","PluginAngularBadge","PluginDeprecatedBadge","Loader","text","LoadingPlaceholder","VersionList","versions","installedVersion","latestCompatibleVersion","version","isInstalledVersion","usePluginConfig","useAsync","AppConfigCtrlWrapper","props","pluginId","updateCmd","res","callback","deprecationWarning","prevProps","loader","template","scopeProps","angularCtrl","model","withRightMargin","element","PluginDashboards","resolve","dash","overwrite","datasource","installCmd","loading","PluginUsage","searchQuery","results","found","width","height","SearchResultsTable","of","Spinner","EmptyListCTA","Alert","PluginDetailsBody","queryParams","pageId","pluginConfig","columns","value","permissions","Stack","InteractiveTable","permission","configPage","PluginContextProvider","PluginDetailsDisabledError","className","selectors","renderDescriptionFromError","PluginDetailsSignature","isSignatureValid","isCore","isDisabled","isDisabledDueTooSignatureError","usePluginDetailsTabs","pathname","defaultTab","useDefaultPage","isPublished","currentPageId","navModelChildren","canConfigurePlugins","include","navModel","GetStartedWithApp","enabled","jsonData","enable","updatePluginSettingsAndReload","disable","id","data","e","isGrafanaAdmin","isOrgAdmin","isDataSourceEditor","GetStartedWithDataSource","dispatch","dataSourcesRoutes","onAddDataSource","meta","disabledButton","GetStartedWithPlugin","InstallControlsWarning","pluginStatus","isExternallyManaged","hasPermission","isRemotePluginsAvailable","isCompatible","statusToMessage","Icon","status","trackPluginInstalled","trackPluginUninstalled","InstallControlsButton","hasInstallWarning","setNeedReload","useQueryParams","location","isInstalling","errorInstalling","isUninstalling","errorUninstalling","install","uninstall","unsetInstall","fetchDetails","isConfirmModalVisible","setIsConfirmModalVisible","showConfirmModal","hideConfirmModal","uninstallBtnText","trackingProps","onInstall","result","successMessage","onUninstall","activePageId","onUpdate","ConfirmModal","shouldDisable","ExternallyManagedButton","angularDetected","externalManageLink","PluginActions","needReload","isInstallControlsDisabled","PluginSubtitle","link","PluginDetailsHeaderDependencies","grafanaDependency","pluginDependencies","p","SIGNATURE_ICONS","PluginSignatureDetailsBadge","signatureType","signatureOrg","signatureTypeText","signatureIcon","DetailsBadge","children","PluginDetailsHeaderSignature","usePluginInfo","info","useLatestCompatibleInfo","usePluginPageExtensions","PluginDetailsDeprecatedWarning","dismissed","setDismissed","PluginDetailsPage","navId","notFoundComponent","NotFoundPlugin","notFoundNavModel","actions","subtitle","isFetchLoading","isFetchDetailsLoading","Page","TabContent","AngularDeprecationPluginNotice","Layout","selectRoot","state","selectItems","items","selectDisplayMode","settings","selectAll","selectPlugins","filters","plugins","keyword","filteredPluginIds","selectPluginErrors","filterByPluginType","pluginErrors","selectRequest","actionType","requests","selectIsRequestPending","request","selectRequestError","useGetAll","sortBy","useFetchAll","selector","isLoading","useLocalFetchStatus","sortedPlugins","useGetSingle","useFetchDetails","useGetSingleLocalWithoutDetails","useFetchAllLocal","useGetErrors","useInstall","isUpdating","useUnsetInstall","useUninstall","useIsRemotePluginsAvailable","useFetchStatus","useFetchDetailsStatus","useInstallStatus","useUninstallStatus","isNotFetched","shouldFetch","useFetchDetailsLazy","useDisplayMode","v"],"sourceRoot":""}