({
+ title: 'Provisioning',
+ description: 'Enable required Grafana features for provisioning',
+ additional: false,
+ icon: 'cog',
+ isConfigured: hasRequiredFeatures,
+ setupSteps: [
+ {
+ title: 'Enable Required Feature Toggles',
+ description: 'Add these settings to your custom.ini file to enable necessary features:',
+ code: feature_ini,
+ },
+ ],
+ });
+
+ const showModal = (feature: Feature) => {
+ // only show modal if feature is not configured
+ if (!feature.isConfigured) {
+ setActiveFeature(feature);
+ setShowModal(true);
+ }
+ };
+
+ const onDismiss = () => {
+ setShowModal(false);
+ setActiveFeature(null);
+ };
+
+ // Separate required and optional features
+ const requiredFeatures = features.filter((feature) => !feature.additional);
+ const optionalFeatures = features.filter((feature) => feature.additional);
+
+ return (
+
+
+ All Features
+
+
+ {requiredFeatures.map((feature, index) => (
+ showModal(basicSetup)}
+ showSetupButton={!feature.isConfigured}
+ />
+ ))}
+ {optionalFeatures.map((feature, index) => (
+ showModal(feature)}
+ showSetupButton={!feature.isConfigured}
+ />
+ ))}
+
+
+ {showInstructionsModal && activeFeature && (
+
+ )}
+
+ );
+};
diff --git a/public/app/features/provisioning/Setup/SetupModal.tsx b/public/app/features/provisioning/Setup/SetupModal.tsx
new file mode 100644
index 00000000000..4fdc31bbb2d
--- /dev/null
+++ b/public/app/features/provisioning/Setup/SetupModal.tsx
@@ -0,0 +1,84 @@
+import { useState } from 'react';
+import { Modal, Button, useStyles2, Stack, Text } from '@grafana/ui';
+import { GrafanaTheme2 } from '@grafana/data';
+import { css } from '@emotion/css';
+import { Feature } from './types';
+import { SetupStep } from './SetupStep';
+import { Sidebar } from './Sidebar';
+
+export interface Props {
+ feature: Feature;
+ isOpen: boolean;
+ onDismiss: () => void;
+}
+
+export const SetupModal = ({ feature, isOpen, onDismiss }: Props) => {
+ const styles = useStyles2(getStyles);
+
+ const [currentStep, setCurrentStep] = useState(0);
+
+ const isFirstStep = currentStep === 0;
+ const isLastStep = currentStep === feature.setupSteps.length - 1;
+ const stepTitles = feature.setupSteps.map((step) => step.title);
+
+ const handleNext = () => !isLastStep && setCurrentStep(currentStep + 1);
+ const handlePrevious = () => !isFirstStep && setCurrentStep(currentStep - 1);
+
+ return (
+
+
+
+ {feature.description}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {isLastStep ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+};
+
+const getStyles = (theme: GrafanaTheme2) => ({
+ modal: css`
+ width: 1100px;
+ max-width: 95%;
+ `,
+ description: css`
+ margin-bottom: ${theme.spacing(3)};
+ padding: 0 ${theme.spacing(1)};
+ `,
+ contentWrapper: css`
+ flex: 1;
+ overflow-y: auto;
+ min-width: 0;
+ padding: ${theme.spacing(2)};
+ border-left: 1px solid ${theme.colors.border.weak};
+ `,
+ footer: css`
+ padding: ${theme.spacing(2)};
+ border-top: 1px solid ${theme.colors.border.weak};
+ margin-top: ${theme.spacing(2)};
+ `,
+});
diff --git a/public/app/features/provisioning/Setup/SetupPage.tsx b/public/app/features/provisioning/Setup/SetupPage.tsx
new file mode 100644
index 00000000000..e5adb704596
--- /dev/null
+++ b/public/app/features/provisioning/Setup/SetupPage.tsx
@@ -0,0 +1,17 @@
+import { Page } from 'app/core/components/Page/Page';
+import { StatusAlerts } from './StatusAlerts';
+import { FeatureList } from './FeatureList';
+
+export default function SetupPage() {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/public/app/features/provisioning/Setup/SetupStep.tsx b/public/app/features/provisioning/Setup/SetupStep.tsx
new file mode 100644
index 00000000000..527212dc01a
--- /dev/null
+++ b/public/app/features/provisioning/Setup/SetupStep.tsx
@@ -0,0 +1,24 @@
+import { Container, Text } from '@grafana/ui';
+import { Step } from './types';
+import { CodeBlock } from '../components/CodeBlock';
+
+export interface Props {
+ step: Step;
+}
+
+export const SetupStep = ({ step }: Props) => {
+ return (
+ <>
+
+ {step?.title}
+
+ {step.description && (
+
+ {step.description}
+
+ )}
+
+ {step.code && }
+ >
+ );
+};
diff --git a/public/app/features/provisioning/Setup/Sidebar.tsx b/public/app/features/provisioning/Setup/Sidebar.tsx
new file mode 100644
index 00000000000..9045707f912
--- /dev/null
+++ b/public/app/features/provisioning/Setup/Sidebar.tsx
@@ -0,0 +1,44 @@
+import { useStyles2, Stack } from '@grafana/ui';
+import { css } from '@emotion/css';
+import { GrafanaTheme2 } from '@grafana/data';
+import { SidebarItem, getStyles as getStepItemStyles } from './SidebarItem';
+
+interface Props {
+ steps: string[];
+ currentStep: number;
+ onStepClick: (index: number) => void;
+}
+
+export const Sidebar = ({ steps, currentStep, onStepClick }: Props) => {
+ if (steps.length === 0 || steps.length === 1) {
+ return null;
+ }
+
+ const styles = useStyles2(getStyles);
+ const stepItemStyles = useStyles2(getStepItemStyles);
+
+ return (
+
+
+ {steps.map((step, index) => (
+
+ ))}
+
+
+ );
+};
+
+const getStyles = (theme: GrafanaTheme2) => ({
+ sidebar: css`
+ width: 260px;
+ padding: ${theme.spacing(1)};
+ overflow-y: auto;
+ `,
+});
diff --git a/public/app/features/provisioning/Setup/SidebarItem.tsx b/public/app/features/provisioning/Setup/SidebarItem.tsx
new file mode 100644
index 00000000000..fa501f270aa
--- /dev/null
+++ b/public/app/features/provisioning/Setup/SidebarItem.tsx
@@ -0,0 +1,70 @@
+import { IconButton, Text, Stack, Card } from '@grafana/ui';
+import { css } from '@emotion/css';
+import { GrafanaTheme2 } from '@grafana/data';
+
+export interface Props {
+ step: string;
+ index: number;
+ currentStep: number;
+ onStepClick: (index: number) => void;
+ styles: ReturnType;
+}
+
+export const SidebarItem = ({ step, index, currentStep, onStepClick, styles }: Props) => {
+ const isCompleted = index < currentStep;
+ const isCurrent = index === currentStep;
+ const isPending = index > currentStep;
+
+ const getStepStatus = () => {
+ if (isCompleted) return { icon: 'check-circle' as const, color: 'success', label: 'Completed step' };
+ if (isCurrent) return { icon: 'circle' as const, color: 'primary', label: 'Current step' };
+ return { icon: 'circle' as const, color: 'secondary', label: 'Pending step' };
+ };
+
+ const { icon, color, label } = getStepStatus();
+
+ const handleClick = () => onStepClick(index);
+ const handleIconClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ onStepClick(index);
+ };
+
+ return (
+
+
+
+
+ {step}
+
+
+
+ );
+};
+
+export const getStyles = (theme: GrafanaTheme2) => ({
+ stepItem: css`
+ padding: ${theme.spacing(1)};
+ cursor: pointer;
+ &:hover {
+ background: ${theme.colors.action.hover};
+ }
+ `,
+ activeStep: css`
+ color: ${theme.colors.primary.text};
+ `,
+ plainCard: css`
+ background: transparent;
+ border: none;
+ box-shadow: none;
+ `,
+});
diff --git a/public/app/features/provisioning/Setup/StatusAlerts.tsx b/public/app/features/provisioning/Setup/StatusAlerts.tsx
new file mode 100644
index 00000000000..ac83f730bf9
--- /dev/null
+++ b/public/app/features/provisioning/Setup/StatusAlerts.tsx
@@ -0,0 +1,64 @@
+import { locationService } from '@grafana/runtime';
+import { Alert } from '@grafana/ui';
+import { getConfigurationStatus } from './utils';
+
+interface Props {
+ /**
+ * Whether to show the "Setup Now" button
+ * @default true
+ */
+ showSetupButton?: boolean;
+
+ /**
+ * Whether to show a success banner when everything is configured
+ * @default false
+ */
+ showSuccessBanner?: boolean;
+}
+
+export function StatusAlerts({ showSetupButton = true, showSuccessBanner = false }: Props) {
+ const { everythingConfigured, missingRequiredFeatures } = getConfigurationStatus();
+
+ // Early return if everything is configured and we don't want to show success banner
+ if (everythingConfigured && !showSuccessBanner) {
+ return null;
+ }
+
+ // Handle success case
+ if (everythingConfigured) {
+ return (
+
+ All required and optional features are properly configured. Your system is ready to use.
+
+ );
+ }
+
+ const handleSetupClick = () => {
+ locationService.push('/admin/provisioning/setup');
+ };
+
+ const commonSetupButtonProps = showSetupButton
+ ? {
+ buttonContent: 'Set Up Now',
+ onRemove: handleSetupClick,
+ }
+ : {};
+
+ // Required features missing - show error alert
+ if (missingRequiredFeatures) {
+ return (
+
+ Some required features are not properly configured. Please complete the setup for these features to ensure the
+ system runs properly.
+
+ );
+ }
+
+ // Only optional features missing - show info alert
+ return (
+
+ Some additional features like Github webhook integration or Previews are not configured. These features can
+ enhance your experience.
+
+ );
+}
diff --git a/public/app/features/provisioning/Setup/types.ts b/public/app/features/provisioning/Setup/types.ts
new file mode 100644
index 00000000000..aeacaa6dc02
--- /dev/null
+++ b/public/app/features/provisioning/Setup/types.ts
@@ -0,0 +1,86 @@
+import { FeatureToggles } from '@grafana/data';
+
+export interface Step {
+ title: string;
+ description?: string;
+ code?: string;
+ copyCode?: boolean;
+}
+
+export interface Feature {
+ title: string;
+ description: string;
+ additional: boolean;
+ icon?: string; // Optional icon name for the feature
+ setupSteps: Step[];
+ isConfigured: boolean;
+ docsLink?: string;
+}
+
+// List of required feature toggles
+export const requiredFeatureToggles: Array = [
+ 'provisioning',
+ 'kubernetesDashboards',
+ 'kubernetesClientDashboardsFolders',
+ 'unifiedStorageSearch',
+ 'unifiedStorageSearchUI',
+];
+
+// Configuration examples
+export const feature_ini = `# In your custom.ini file
+app_mode = development
+[feature_toggles]
+provisioning = true
+kubernetesDashboards = true
+unifiedStorageSearch = true
+unifiedStorageSearchUI = true
+kubernetesClientDashboardsFolders = true
+
+# If you want easy kubectl setup development mode
+grafanaAPIServerEnsureKubectlAccess = true`;
+
+// Configuration examples
+export const custom_ini = `# In your custom.ini file
+app_mode = development
+[feature_toggles]
+provisioning = true
+kubernetesDashboards = true
+unifiedStorageSearch = true
+unifiedStorageSearchUI = true
+kubernetesClientDashboardsFolders = true
+
+# If you want easy kubectl setup development mode
+grafanaAPIServerEnsureKubectlAccess = true
+
+# For Github webhook support, you will need something like:
+[server]
+root_url = https://supreme-exact-beetle.ngrok-free.app
+
+# For dashboard preview generation, you will need something like:
+[rendering]
+server_url = http://localhost:8081/render
+callback_url = http://localhost:3000/
+ `;
+
+export const ngrok_example = `ngrok http 3000
+
+Help shape K8s Bindings https://ngrok.com/new-features-update?ref=k8s
+
+Session Status online
+Account Roberto Jiménez Sánchez (Plan: Free)
+Version 3.18.4
+Region Europe (eu)
+Latency 44ms
+Web Interface http://127.0.0.1:4040
+Forwarding https://d60d-83-33-235-27.ngrok-free.app -> http://localhost:3000
+Connections ttl opn rt1 rt5 p50 p90
+ 50 2 0.00 0.00 83.03 90.56
+
+HTTP Requests
+-------------
+
+09:18:46.147 CET GET /favicon.ico 302 Found
+09:18:46.402 CET GET /login`;
+
+export const root_url_ini = `[server]
+root_url = https://d60d-83-33-235-27.ngrok-free.app`;
diff --git a/public/app/features/provisioning/Setup/utils.ts b/public/app/features/provisioning/Setup/utils.ts
new file mode 100644
index 00000000000..9e916e2755f
--- /dev/null
+++ b/public/app/features/provisioning/Setup/utils.ts
@@ -0,0 +1,46 @@
+import { config } from '@grafana/runtime';
+import { requiredFeatureToggles } from './types';
+
+/**
+ * Checks if all required feature toggles are enabled
+ * @returns true if all required feature toggles are enabled
+ */
+export const checkRequiredFeatures = (): boolean => {
+ const featureToggles = config.featureToggles || {};
+ return requiredFeatureToggles.every((toggle) => featureToggles[toggle]);
+};
+
+/**
+ * Checks if public access is configured
+ * @returns true if the app URL is configured for external access
+ */
+export const checkPublicAccess = (): boolean => {
+ return Boolean(config.appUrl && config.appUrl !== 'http://localhost:3000/');
+};
+
+/**
+ * Checks if image renderer is configured
+ * @returns true if the image renderer is available
+ */
+export const checkImageRenderer = (): boolean => {
+ return Boolean(config.rendererAvailable);
+};
+
+/**
+ * Returns the configuration status of all features
+ * @returns Object containing the status of required and optional features
+ */
+export const getConfigurationStatus = () => {
+ const hasRequiredFeatures = checkRequiredFeatures();
+ const hasPublicAccess = checkPublicAccess();
+ const hasImageRenderer = checkImageRenderer();
+
+ return {
+ hasRequiredFeatures,
+ hasPublicAccess,
+ hasImageRenderer,
+ missingOnlyOptionalFeatures: hasRequiredFeatures && (!hasPublicAccess || !hasImageRenderer),
+ missingRequiredFeatures: !hasRequiredFeatures,
+ everythingConfigured: hasRequiredFeatures && hasPublicAccess && hasImageRenderer,
+ };
+};
diff --git a/public/app/features/provisioning/SetupWarningPage.tsx b/public/app/features/provisioning/SetupWarningPage.tsx
deleted file mode 100644
index 695c54194db..00000000000
--- a/public/app/features/provisioning/SetupWarningPage.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Page } from 'app/core/components/Page/Page';
-
-import { SetupWarnings } from './SetupWarnings';
-
-export default function SetupWarningPage() {
- return (
-
-
-
-
-
- );
-}
diff --git a/public/app/features/provisioning/SetupWarnings.tsx b/public/app/features/provisioning/SetupWarnings.tsx
deleted file mode 100644
index d0d439d448b..00000000000
--- a/public/app/features/provisioning/SetupWarnings.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import { useLocalStorage } from 'react-use';
-
-import { FeatureToggles } from '@grafana/data';
-import { config } from '@grafana/runtime';
-import { Alert, Text, Collapse } from '@grafana/ui';
-
-const requiredFeatureToggles: Array = [
- 'provisioning',
- 'kubernetesDashboards',
- 'kubernetesClientDashboardsFolders',
- 'unifiedStorageSearch',
- 'unifiedStorageSearchUI',
-];
-
-const custom_ini = `app_mode = development
-
-[feature_toggles]
-provisioning = true
-kubernetesDashboards = true
-unifiedStorageSearch = true
-unifiedStorageSearchUI = true
-kubernetesClientDashboardsFolders = true
-
-# If you want easy kubectl setup development mode
-grafanaAPIServerEnsureKubectlAccess = true
-
-# For Github webhook support, you will need something like:
-[server]
-root_url = https://supreme-exact-beetle.ngrok-free.app`;
-
-const ngrok_example = `ngrok http 3000
-
-Help shape K8s Bindings https://ngrok.com/new-features-update?ref=k8s
-
-Session Status online
-Account Roberto Jiménez Sánchez (Plan: Free)
-Version 3.18.4
-Region Europe (eu)
-Latency 44ms
-Web Interface http://127.0.0.1:4040
-Forwarding https://d60d-83-33-235-27.ngrok-free.app -> http://localhost:3000
-Connections ttl opn rt1 rt5 p50 p90
- 50 2 0.00 0.00 83.03 90.56
-
-HTTP Requests
--------------
-
-09:18:46.147 CET GET /favicon.ico 302 Found
-09:18:46.402 CET GET /login`;
-
-const webhook_ini = `...
-
-[server]
-root_url = https://d60d-83-33-235-27.ngrok-free.app`;
-
-export function SetupWarnings() {
- const [isCustomIniOpen, setCustomIniOpen] = useLocalStorage('collapse_custom_ini', true);
- const [isWebhookOpen, setWebhookOpen] = useLocalStorage('collapse_webhook', true);
-
- const missingFeatures = requiredFeatureToggles.filter((feature) => !config.featureToggles[feature]);
-
- const handleCustomIniToggle = () => {
- setCustomIniOpen(!isCustomIniOpen);
- };
-
- const handleWebhookToggle = () => {
- setWebhookOpen(!isWebhookOpen);
- };
-
- if (missingFeatures.length === 0) {
- return null;
- }
-
- return (
- <>
-
- {missingFeatures.map((feature) => (
-
- Missing required feature toggle: {feature}
-
- ))}
-
-
-
-
-
- {custom_ini}
-
-
- NOTE: the above config is not this machine's config
-
-
-
-
-
-
- Webhook support requires the server to run on a public URL.
-
- {webhook_ini}
-
- To set up public access to a local machine, consider ngrok
-
- {ngrok_example}
-
-
-
- >
- );
-}
diff --git a/public/app/features/provisioning/Wizard/MigrateToProvisioningPage.tsx b/public/app/features/provisioning/Wizard/MigrateToProvisioningPage.tsx
index 3b96e946a21..8f15e903371 100644
--- a/public/app/features/provisioning/Wizard/MigrateToProvisioningPage.tsx
+++ b/public/app/features/provisioning/Wizard/MigrateToProvisioningPage.tsx
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom-v5-compat';
import { Page } from 'app/core/components/Page/Page';
-import { SetupWarnings } from '../SetupWarnings';
+import { StatusAlerts } from '../Setup/StatusAlerts';
import { useGetFrontendSettingsQuery } from '../api';
import { PROVISIONING_URL } from '../constants';
@@ -37,7 +37,7 @@ export default function MigrateToProvisioningPage() {
pageNav={{ text: 'Migrate to Provisioning', subTitle: 'Manage this instance from provisioning' }}
>
-
+