diff --git a/docs/sources/administration/announcement-banner/_index.md b/docs/sources/administration/announcement-banner/_index.md new file mode 100644 index 00000000000..14782fb77c2 --- /dev/null +++ b/docs/sources/administration/announcement-banner/_index.md @@ -0,0 +1,79 @@ +--- +draft: true +aliases: + - ../administration/reports/ + - ../enterprise/export-pdf/ + - ../enterprise/reporting/ + - ../panels/create-reports/ + - reporting/ +keywords: + - grafana + - announcement +labels: + products: + - cloud + - enterprise +menuTitle: Announcement banner +title: Create and configure announcement banner +description: Creat a banner to show important updates and information at the top of on every page +refs: + rbac: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/roles-and-permissions/access-control/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//administration/roles-and-permissions/access-control/ +--- + +# Create and configure announcement banner + +Announcement banner allows you to show important updates and information at the top of every page in Grafana. You can use the announcement banner to communicate important information to your users, such as maintenance windows, new features, or other important updates. + +## Create or update an announcement banner + +Only organization administrators can create announcement banner by default. You can customize who can create announcement banner with [Role-based access control](ref:rbac). + +To create or update an announcement banner, follow these steps: + +1. Click **Administration > General > Announcement banner** in the side navigation menu. + + The Announcement banner page allows you to view, create and update the settings for a notification banner. Only one banner can be created at a time. + +2. Toggle the **Enable** switch on to enable the announcement banner. It can be toggled off at any time to disable the banner. + +3. Enter the **Message** for the announcement banner. + + The message field supports Markdown. To add a header, use the following syntax: + + ```markdown + ### Header + ``` + + To add a link, use the following syntax: + + ```markdown + [link text](https://www.example.com) + ``` + + The preview of the configured banner will appear on top of the form, under the **Preview** section. + +4. Select the banner's start date and time in the **Starts** field. + + By default, the banner starts being displayed immediately. You can set a future date and time for the banner to start displaying. + +5. Select the banner's end date and time in the **Ends** field. + + By default, the banner is displayed indefinitely. You can set a future date and time for the banner to stop displaying. + +6. Select the banner's visibility. + + **Everyone** - The banner is visible to all users, including on login page. + + **Authenticated users** - The banner is visible to only authenticated users. + +7. Select the type of banner in the **Variant** field. + + This will determine the color of the banner's background. + +8. Click **Save** to save the banner settings. + + The banner will now be displayed at the top of every page in Grafana. diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-telegram.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-telegram.md index e0c237211de..4b95b3c52ac 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-telegram.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-telegram.md @@ -22,6 +22,11 @@ Use the Grafana Alerting - Telegram integration to send [Telegram](https://teleg ## Before you begin +### Telegram limitation + +Telegram messages are limited to 4096 UTF-8 characters. If you use a `parse_mode` other than `None`, truncation may result in an invalid message, causing the notification to fail. +For longer messages, we recommend using an alternative contact method. + ### Telegram bot API token and chat ID To integrate Grafana with Telegram, you need to obtain a Telegram **bot API token** and a **chat ID** (i.e., the ID of the Telegram chat where you want to receive the alert notifications). diff --git a/docs/sources/dashboards/assess-dashboard-usage/index.md b/docs/sources/dashboards/assess-dashboard-usage/index.md index 195e64b132c..1be93c317ed 100644 --- a/docs/sources/dashboards/assess-dashboard-usage/index.md +++ b/docs/sources/dashboards/assess-dashboard-usage/index.md @@ -50,9 +50,11 @@ refs: Usage insights enables you to have a better understanding of how your Grafana instance is used. -> **Note:** Available in [Grafana Enterprise](ref:grafana-enterprise) and [Grafana Cloud](/docs/grafana-cloud/). -> Grafana Cloud insights logs include additional fields with their own dashboards. -> Read more in the [Grafana Cloud documentation](/docs/grafana-cloud/usage-insights/). +{{< admonition type="note" >}} +Available in [Grafana Enterprise](ref:grafana-enterprise) and [Grafana Cloud](https://grafana.com/docs/grafana-cloud/). +Grafana Cloud insights logs include additional fields with their own dashboards. +Read more in the [Grafana Cloud documentation](https://grafana.com/docs/grafana-cloud/account-management/usage-insights/). +{{< /admonition >}} The usage insights feature collects a number of aggregated data and stores them in the database: @@ -77,7 +79,7 @@ For every dashboard and data source, you can access usage information. To see dashboard usage information, click the dashboard insights icon in the header. -{{< figure src="/media/docs/grafana/dashboards/screenshot-dashboard-insights.png" max-width="400px" class="docs-image--no-shadow" alt="Dashboard insights icon" >}} +{{< figure src="/media/docs/grafana/dashboards/screenshot-dashboard-insights-11.2.png" max-width="400px" class="docs-image--no-shadow" alt="Dashboard insights icon" >}} Dashboard insights show the following information: diff --git a/docs/sources/dashboards/build-dashboards/create-dashboard/index.md b/docs/sources/dashboards/build-dashboards/create-dashboard/index.md index bd4b2534295..f5ff0f8781d 100644 --- a/docs/sources/dashboards/build-dashboards/create-dashboard/index.md +++ b/docs/sources/dashboards/build-dashboards/create-dashboard/index.md @@ -19,22 +19,22 @@ refs: - pattern: /docs/grafana/ destination: /docs/grafana//datasources/#special-data-sources - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//datasources/#special-data-sources + destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/#special-data-sources visualization-specific-options: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/visualizations/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/visualizations/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/ configure-standard-options: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/configure-standard-options/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/configure-standard-options/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-standard-options/ configure-value-mappings: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/configure-value-mappings/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/configure-value-mappings/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-value-mappings/ generative-ai-features: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/manage-dashboards/#set-up-generative-ai-features-for-dashboards @@ -44,12 +44,12 @@ refs: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/configure-thresholds/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/configure-thresholds/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-thresholds/ data-sources: - pattern: /docs/grafana/ destination: /docs/grafana//datasources/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//datasources/ + destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/ add-a-data-source: - pattern: /docs/grafana/ destination: /docs/grafana//administration/data-source-management/#add-a-data-source @@ -69,17 +69,12 @@ refs: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/configure-panel-options/#configure-repeating-panels - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/configure-panel-options/#configure-repeating-panels + destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-panel-options/#configure-repeating-panels override-field-values: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/configure-overrides/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/configure-overrides/ - dashboard: - - pattern: /docs/grafana/ - destination: /docs/grafana//datasources/#special-data-sources - - pattern: /docs/grafana-cloud/ - destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/#special-data-sources + destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-overrides/ --- ## Create a dashboard @@ -95,7 +90,7 @@ Dashboards and panels allow you to show your data in visual form. Each panel nee **To create a dashboard**: -1. Click **Dashboards** in the left-side menu. +1. Click **Dashboards** in the main menu. 1. Click **New** and select **New Dashboard**. 1. On the empty dashboard, click **+ Add visualization**. @@ -104,7 +99,7 @@ Dashboards and panels allow you to show your data in visual form. Each panel nee 1. In the dialog box that opens, do one of the following: - Select one of your existing data sources. - - Select one of the Grafana's [built-in special data sources](ref:built-in-special-data-sources). + - Select one of the Grafana [built-in special data sources](ref:built-in-special-data-sources). - Click **Configure a new data source** to set up a new one (Admins only). {{< figure class="float-right" src="/media/docs/grafana/dashboards/screenshot-data-source-selector-10.0.png" max-width="800px" alt="Select data source modal" >}} @@ -115,14 +110,10 @@ Dashboards and panels allow you to show your data in visual form. Each panel nee For more information about data sources, refer to [Data sources](ref:data-sources) for specific guidelines. 1. Write or construct a query in the query language of your data source. - -1. Click the Refresh dashboard icon to query the data source. - - ![Refresh dashboard icon](/media/docs/grafana/dashboards/screenshot-refresh-dashboard-9.5.png) - +1. Click **Refresh** to query the data source. 1. In the visualization list, select a visualization type. - ![Visualization selector](/media/docs/grafana/dashboards/screenshot-select-visualization-9-5.png) + ![Visualization selector](/media/docs/grafana/dashboards/screenshot-select-visualization-11-2.png) Grafana displays a preview of your query results with the visualization applied. @@ -139,30 +130,35 @@ Dashboards and panels allow you to show your data in visual form. Each panel nee - [Configure thresholds](ref:configure-thresholds) - [Configure standard options](ref:configure-standard-options) -1. When you've finished editing your panel, click **Save** to save the dashboard. +1. When you've finished editing your panel, click **Save dashboard**. - Alternatively, click **Apply** if you want to see your changes applied to the dashboard first. Then click the save icon in the dashboard header. + Alternatively, click **Back to dashboard** if you want to see your changes applied to the dashboard first. Then click **Save dashboard** when you're ready. 1. Enter a title and description for your dashboard or have Grafana create them using [generative AI features](ref:generative-ai-features). 1. Select a folder, if applicable. 1. Click **Save**. -1. To add more panels to the dashboard, click **Add** in the dashboard header and select **Visualization** in the drop-down. +1. To add more panels to the dashboard, click **Back to dashboard**. + Then click **Add** in the dashboard header and select **Visualization** in the drop-down. ![Add drop-down](/media/docs/grafana/dashboards/screenshot-add-dropdown-10.0.png) When you add additional panels to the dashboard, you're taken straight to the **Edit panel** view. -## Copy an existing dashboard +1. When you've saved all the changes you want to make to the dashboard, click **Exit edit**. + + Now, when you want to make more changes to the saved dashboard, click **Edit** in the top-right corner. + +## Copy a dashboard -To copy an existing dashboard, follow these steps: +To copy a dashboard, follow these steps: -1. Click **Dashboards** in the primary menu. -1. Open the dashboard to be copied. -1. Click **Settings** (gear icon) in the top right of the dashboard. -1. Click **Save as** in the top-right corner of the dashboard. +1. Click **Dashboards** in the main menu. +1. Open the dashboard you want to copy. +1. Click **Edit** in top-right corner. +1. Click the **Save dashboard** drop-down and select **Save as copy**. 1. (Optional) Specify the name, folder, description, and whether or not to copy the original dashboard tags for the copied dashboard. - By default, the copied dashboard has the same name as the original dashboard with the word "Copy" appended and is located in the same folder. + By default, the copied dashboard has the same name as the original dashboard with the word "Copy" appended and is in the same folder. 1. Click **Save**. @@ -178,7 +174,7 @@ To see an example of repeating rows, refer to [Dashboard with repeating rows](ht **To configure repeating rows:** -1. Click **Dashboards** in the left-side menu. +1. Click **Dashboards** in the main menu. 1. Navigate to the dashboard you want to work on. 1. At the top of the dashboard, click **Add** and select **Row** in the drop-down. @@ -192,7 +188,7 @@ To provide context to dashboard users, add the variable to the row title. ### Repeating rows and the Dashboard special data source -If a row includes panels using the special [Dashboard](ref:dashboard) data source—the data source that uses a result set from another panel in the same dashboard—then corresponding panels in repeated rows will reference the panel in the original row, not the ones in the repeated rows. +If a row includes panels using the special [Dashboard data source](ref:built-in-special-data-sources)—the data source that uses a result set from another panel in the same dashboard—then corresponding panels in repeated rows will reference the panel in the original row, not the ones in the repeated rows. For example, in a dashboard: @@ -205,7 +201,7 @@ For example, in a dashboard: You can place a panel on a dashboard in any location. -1. Click **Dashboards** in the left-side menu. +1. Click **Dashboards** in the main menu. 1. Navigate to the dashboard you want to work on. 1. Click the panel title and drag the panel to the new location. @@ -213,6 +209,6 @@ You can place a panel on a dashboard in any location. You can size a dashboard panel to suits your needs. -1. Click **Dashboards** in the left-side menu. +1. Click **Dashboards** in the main menu. 1. Navigate to the dashboard you want to work on. 1. To adjust the size of the panel, click and drag the lower-right corner of the panel. diff --git a/docs/sources/dashboards/build-dashboards/manage-dashboard-links/index.md b/docs/sources/dashboards/build-dashboards/manage-dashboard-links/index.md index 0960cceea09..94e75533736 100644 --- a/docs/sources/dashboards/build-dashboards/manage-dashboard-links/index.md +++ b/docs/sources/dashboards/build-dashboards/manage-dashboard-links/index.md @@ -26,19 +26,19 @@ weight: 500 refs: data-links: - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/configure-data-links/#data-links + destination: /docs/grafana//panels-visualizations/configure-data-links/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/configure-data-links/#data-links + destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-data-links/ data-link-variables: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/configure-data-links/#data-link-variables - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/configure-data-links/#data-link-variables + destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-data-links/#data-link-variables dashboard-url-variables: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/build-dashboards/create-dashboard-url-variables/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//dashboards/build-dashboards/create-dashboard-url-variables/ + destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/create-dashboard-url-variables/ --- # Manage dashboard links @@ -83,51 +83,64 @@ Once you've added a dashboard link, it appears in the upper right corner of your Add links to other dashboards at the top of your current dashboard. -1. While viewing the dashboard you want to link, click the gear at the top of the screen to open **Dashboard settings**. -1. Click **Links** and then click **Add Dashboard Link** or **New**. -1. In **Type**, select **dashboards**. -1. Select link options: - - **With tags** – Enter tags to limit the linked dashboards to only the ones with the tags you enter. Otherwise, Grafana includes links to all other dashboards. - - **As dropdown** – If you are linking to lots of dashboards, then you probably want to select this option and add an optional title to the dropdown. Otherwise, Grafana displays the dashboard links side by side across the top of your dashboard. - - **Time range** – Select this option to include the dashboard time range in the link. When the user clicks the link, the linked dashboard opens with the indicated time range already set. **Example:** https://play.grafana.org/d/000000010/annotations?orgId=1&from=now-3h&to=now - - **Variable values** – Select this option to include template variables currently used as query parameters in the link. When the user clicks the link, any matching templates in the linked dashboard are set to the values from the link. For more information, see [Dashboard URL variables](ref:dashboard-url-variables). - - **Open in new tab** – Select this option if you want the dashboard link to open in a new tab or window. -1. Click **Add**. +1. In the dashboard you want to link, click **Edit**. +1. Click **Settings**. +1. Go to the **Links** tab and then click **Add dashboard link**. + + The default link type is **Dashboards**. + +1. In the **With tags** drop-down, enter tags to limit the linked dashboards to only the ones with the tags you enter. + + If you don't add any tags, Grafana includes links to all other dashboards. + +1. Set link options: + + - **Show as dropdown** – If you are linking to lots of dashboards, then you probably want to select this option and add an optional title to the dropdown. Otherwise, Grafana displays the dashboard links side by side across the top of your dashboard. + - **Include current time range** – Select this option to include the dashboard time range in the link. When the user clicks the link, the linked dashboard opens with the indicated time range already set. **Example:** https://play.grafana.org/d/000000010/annotations?orgId=1&from=now-3h&to=now + - **Include current template variable values** – Select this option to include template variables currently used as query parameters in the link. When the user clicks the link, any matching templates in the linked dashboard are set to the values from the link. For more information, see [Dashboard URL variables](ref:dashboard-url-variables). + - **Open link in new tab** – Select this option if you want the dashboard link to open in a new tab or window. + +1. Click **Save dashboard** in the top-right corner. +1. Click **Back to dashboard** and then **Exit edit**. ### Add a URL link to a dashboard Add a link to a URL at the top of your current dashboard. You can link to any available URL, including dashboards, panels, or external sites. You can even control the time range to ensure the user is zoomed in on the right data in Grafana. -1. While viewing the dashboard you want to link, click the gear at the top of the screen to open **Dashboard settings**. -1. Click **Links** and then click **Add Dashboard Link** or **New**. -1. In **Type**, select **link**. -1. Select link options: - - **Url** – Enter the URL you want to link to. Depending on the target, you might want to include field values. **Example:** https://github.com/grafana/grafana/issues/new?title=Dashboard%3A%20HTTP%20Requests - - **Title** – Enter the title you want the link to display. - - **Tooltip** – Enter the tooltip you want the link to display when the user hovers their mouse over it. - - **Icon** – Choose the icon you want displayed with the link. - - **Time range** – Select this option to include the dashboard time range in the link. When the user clicks the link, the linked dashboard opens with the indicated time range already set. **Example:** https://play.grafana.org/d/000000010/annotations?orgId=1&from=now-3h&to=now - - `from` - Defines the lower limit of the time range, specified in ms epoch. - - `to` - Defines the upper limit of the time range, specified in ms epoch. - - `time` and `time.window` - Define a time range from `time-time.window/2` to `time+time.window/2`. Both params should be specified in ms. For example `?time=1500000000000&time.window=10000` will result in 10s time range from 1499999995000 to 1500000005000. - - **Variable values** – Select this option to include template variables currently used as query parameters in the link. When the user clicks the link, any matching templates in the linked dashboard are set to the values from the link. Here is the variable format: `https://${you-domain}/path/to/your/dashboard?var-${template-variable1}=value1&var-{template-variable2}=value2` **Example:** https://play.grafana.org/d/000000074/alerting?var-app=backend&var-server=backend_01&var-server=backend_03&var-interval=1h - - **Open in new tab** – Select this option if you want the dashboard link to open in a new tab or window. -1. Click **Add**. +1. In the dashboard you want to link, click **Edit**. +1. Click **Settings**. +1. Go to the **Links** tab and then click **Add dashboard link**. +1. In the **Type** drop-down, select **Link**. +1. In the **URL** field, enter the URL to which you want to link. + + Depending on the target, you might want to include field values. **Example:** https://github.com/grafana/grafana/issues/new?title=Dashboard%3A%20HTTP%20Requests + +1. In the **Tooltip** field, enter the tooltip you want the link to display when the user hovers their mouse over it. +1. In the **Icon** drop-down, choose the icon you want displayed with the link. +1. Set link options; by default, these options are enabled for URL links: + + - **Include current time range** – Select this option to include the dashboard time range in the link. When the user clicks the link, the linked dashboard opens with the indicated time range already set. **Example:** https://play.grafana.org/d/000000010/annotations?orgId=1&from=now-3h&to=now + - **Include current template variable values** – Select this option to include template variables currently used as query parameters in the link. When the user clicks the link, any matching templates in the linked dashboard are set to the values from the link. + - **Open link in new tab** – Select this option if you want the dashboard link to open in a new tab or window. + +1. Click **Save dashboard** in the top-right corner. +1. Click **Back to dashboard** and then **Exit edit**. ### Update a dashboard link -To change or update an existing dashboard link, follow this procedure. +To change or update a dashboard link, follow this procedure. -1. In Dashboard Settings, on the Links tab, click the existing link that you want to edit. -1. Change the settings and then click **Update**. +1. In the dashboard settings, on the **Links** tab, click the link that you want to edit. +1. Change the settings and then click **Save dashboard**. +1. Click **Back to dashboard** and then **Exit edit**. ## Duplicate a dashboard link -To duplicate an existing dashboard link, click the duplicate icon next to the existing link that you want to duplicate. +To duplicate a dashboard link, click the copy link icon next to the link that you want to duplicate. ### Delete a dashboard link -To delete an existing dashboard link, click the trash icon next to the duplicate icon that you want to delete. +To delete a dashboard link, click the red **X** next to the link that you want to delete and then **Delete**. ## Panel links @@ -144,7 +157,7 @@ Click the icon next to the panel title to see available panel links. To use a keyboard shortcut to open the panel, hover over the panel and press `e`. -1. Expand the **Panel options** section, scroll down to Panel links. +1. Expand the **Panel options** section, scroll down to **Panel links**. 1. Click **Add link**. 1. Enter a **Title**. **Title** is a human-readable label for the link that will be displayed in the UI. 1. Enter the **URL** you want to link to. @@ -152,9 +165,10 @@ Click the icon next to the panel title to see available panel links. - `from` - Defines the lower limit of the time range, specified in ms epoch. - `to` - Defines the upper limit of the time range, specified in ms epoch. - `time` and `time.window` - Define a time range from `time-time.window/2` to `time+time.window/2`. Both params should be specified in ms. For example `?time=1500000000000&time.window=10000` will result in 10s time range from 1499999995000 to 1500000005000. -1. If you want the link to open in a new tab, then select **Open in a new tab**. -1. Click **Save** to save changes and close the window. -1. Click **Save** in the upper right to save your changes to the dashboard. +1. If you want the link to open in a new tab, then select **Open in new tab**. +1. Click **Save** to save changes and close the dialog box. +1. Click **Save dashboard** in the top-right corner. +1. Click **Back to dashboard** and then **Exit edit**. ### Update a panel link @@ -167,8 +181,9 @@ Click the icon next to the panel title to see available panel links. 1. Find the link that you want to make changes to. 1. Click the Edit (pencil) icon to open the Edit link window. 1. Make any necessary changes. -1. Click **Save** to save changes and close the window. -1. Click **Save** in the upper right to save your changes to the dashboard. +1. Click **Save** to save changes and close the dialog box. +1. Click **Save dashboard** in the top-right corner. +1. Click **Back to dashboard** and then **Exit edit**. ### Delete a panel link @@ -180,4 +195,5 @@ Click the icon next to the panel title to see available panel links. 1. Expand the **Panel options** section, scroll down to Panel links. 1. Find the link that you want to delete. 1. Click the **X** icon next to the link you want to delete. -1. Click **Save** in the upper right to save your changes to the dashboard. +1. Click **Save dashboard** in the top-right corner. +1. Click **Back to dashboard** and then **Exit edit**. diff --git a/docs/sources/dashboards/share-dashboards-panels/index.md b/docs/sources/dashboards/share-dashboards-panels/index.md index 5aacb8fde45..01ef05c1923 100644 --- a/docs/sources/dashboards/share-dashboards-panels/index.md +++ b/docs/sources/dashboards/share-dashboards-panels/index.md @@ -59,9 +59,9 @@ You must have an authorized viewer permission to see an image rendered by a dire The same permission is also required to view embedded links unless you have anonymous access permission enabled for your Grafana instance. -{{% admonition type="note" %}} +{{< admonition type="note" >}} As of Grafana 8.0, anonymous access permission is not available in Grafana Cloud. -{{% /admonition %}} +{{< /admonition >}} When you share a panel or dashboard as a snapshot, a snapshot (which is a panel or dashboard at the moment you take the snapshot) is publicly available on the web. Anyone with a link to it can access it. Because snapshots do not require any authorization to view, Grafana removes information related to the account it came from, as well as any sensitive data from the snapshot. @@ -69,13 +69,13 @@ When you share a panel or dashboard as a snapshot, a snapshot (which is a panel You can share a dashboard as a direct link or as a snapshot. You can also export a dashboard. -{{% admonition type="note" %}} +{{< admonition type="note" >}} If you change a dashboard, ensure that you save the changes before sharing. -{{% /admonition %}} +{{< /admonition >}} -1. Click **Dashboards** in the left-side menu. +1. Click **Dashboards** in the main menu. 1. Click the dashboard you want to share. -1. Click the **Share** button at the top right of the screen. +1. Click **Share** in the top-right corner. The share dialog opens and shows the Link tab. @@ -108,7 +108,7 @@ If you created a snapshot by mistake, click **Delete snapshot** in the dialog bo To delete existing snapshots, follow these steps: -1. In the primary menu, click **Dashboards**. +1. Click **Dashboards** in the main menu. 1. Click **Snapshots** to go to the snapshots management page. 1. Click the red **x** next to the snapshot URL that you want to delete. @@ -120,7 +120,7 @@ The dashboard export action creates a Grafana JSON file that contains everything 1. Click **Dashboards** in the main menu. 1. Open the dashboard you want to export. -1. Click the **Share** icon in the top navigation bar. +1. Click **Share** in the top-right corner. 1. Click **Export**. If you're exporting the dashboard to use in another instance, with different data source UIDs, enable the **Export for sharing externally** switch. @@ -139,12 +139,14 @@ A template variable of the type `Constant` is automatically hidden in the dashbo You can generate and save PDF files of any dashboard. -> **Note:** Available in [Grafana Enterprise](ref:grafana-enterprise) and [Grafana Cloud](/docs/grafana-cloud/). +{{< admonition type="note" >}} +Available in [Grafana Enterprise](ref:grafana-enterprise) and [Grafana Cloud](/docs/grafana-cloud/). +{{< /admonition >}} -1. Click **Dashboards** in the left-side menu. +1. Click **Dashboards** in the main menu. 1. Click the dashboard you want to share. -1. Click the **Share** button at the top right of the screen. -1. On the PDF tab, select a layout option for the exported dashboard: **Portrait** or **Landscape**. +1. Click **Share** in the top-right corner. +1. On the **PDF** tab, select a layout option for the exported dashboard: **Portrait** or **Landscape**. 1. Click **Save as PDF** to render the dashboard as a PDF file. Grafana opens the PDF file in a new window or browser tab. @@ -210,7 +212,7 @@ If you created a snapshot by mistake, click **Delete snapshot** in the dialog bo To delete existing snapshots, follow these steps: -1. In the primary menu, click **Dashboards**. +1. Click **Dashboards** in the main menu. 1. Click **Snapshots** to go to the snapshots management page. 1. Click the red **x** next to the snapshot URL that you want to delete. @@ -220,7 +222,9 @@ The snapshot is immediately deleted. You may need to clear your browser cache or You can embed a panel using an iframe on another web site. A viewer must be signed into Grafana to view the graph. -**> Note:** As of Grafana 8.0, anonymous access permission is no longer available for Grafana Cloud. +{{< admonition type="note" >}} +As of Grafana 8.0, anonymous access permission is no longer available for Grafana Cloud. +{{< /admonition >}} Here is an example of the HTML code: @@ -243,4 +247,4 @@ To create a library panel from the **Share Panel** dialog: 1. In **Library panel name**, enter the name. 1. In **Save in folder**, select the folder in which to save the library panel. By default, the root level is selected. 1. Click **Create library panel** to save your changes. -1. Save the dashboard. +1. Click **Save dashboard**. diff --git a/docs/sources/datasources/influxdb/query-editor/index.md b/docs/sources/datasources/influxdb/query-editor/index.md index 1c72886c783..76f1a50d733 100644 --- a/docs/sources/datasources/influxdb/query-editor/index.md +++ b/docs/sources/datasources/influxdb/query-editor/index.md @@ -19,16 +19,18 @@ refs: query-transform-data: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/query-transform-data/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/ panel-inspector: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/panel-inspector/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/panel-inspector/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/panel-inspector/ logs: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/visualizations/logs/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/visualizations/logs/ + destination: grafana-cloud/visualizations/panels-visualizations/visualizations/logs/ --- # InfluxDB query editor diff --git a/docs/sources/getting-started/build-first-dashboard.md b/docs/sources/getting-started/build-first-dashboard.md index 6b21909b9a5..0f1c4025b4c 100644 --- a/docs/sources/getting-started/build-first-dashboard.md +++ b/docs/sources/getting-started/build-first-dashboard.md @@ -14,7 +14,7 @@ weight: 200 # Build your first dashboard -This topic helps you get started with Grafana and build your first dashboard using the built-in `Grafana` data source. To learn more about Grafana, refer to [Introduction to Grafana]({{< relref "../introduction" >}}). +This topic helps you get started with Grafana and build your first dashboard using the built-in `Grafana` data source. To learn more about Grafana, refer to [Introduction to Grafana](https://grafana.com/docs/grafana//introduction/). {{% admonition type="note" %}} Grafana also offers a [free account with Grafana Cloud](/signup/cloud/connect-account?pg=gsdocs) to help getting started even easier and faster. You can install Grafana to self-host or get a free Grafana Cloud account. @@ -22,7 +22,7 @@ Grafana also offers a [free account with Grafana Cloud](/signup/cloud/connect-ac #### Install Grafana -Grafana can be installed on many different operating systems. For a list of the minimum hardware and software requirements, as well as instructions on installing Grafana, refer to [Install Grafana]({{< relref "../setup-grafana/installation" >}}). +Grafana can be installed on many different operating systems. For a list of the minimum hardware and software requirements, as well as instructions on installing Grafana, refer to [Install Grafana](https://grafana.com/docs/grafana//setup-grafana/installation/). #### Sign in to Grafana @@ -39,18 +39,18 @@ To sign in to Grafana for the first time: 1. Click **OK** on the prompt and change your password. -{{% admonition type="note" %}} +{{< admonition type="note" >}} We strongly recommend that you change the default administrator password. -{{% /admonition %}} +{{< /admonition >}} #### Create a dashboard -If you've already set up a data source that you know how to query, refer to [Create a dashboard]({{< relref "../dashboards/build-dashboards/create-dashboard" >}}) instead. +If you've already set up a data source that you know how to query, refer to [Create a dashboard](https://grafana.com/docs/grafana//dashboards/build-dashboards/create-dashboard/) instead. To create your first dashboard using the built-in `-- Grafana --` data source: -1. Click **Dashboards** in the left-side menu. -1. On the Dashboards page, click **New** and select **New Dashboard** from the drop-down menu. +1. Click **Dashboards** in the main menu. +1. On the **Dashboards** page, click **New** and select **New Dashboard** from the drop-down menu. 1. On the dashboard, click **+ Add visualization**. ![Empty dashboard state](/media/docs/grafana/dashboards/empty-dashboard-10.2.png) @@ -59,35 +59,33 @@ To create your first dashboard using the built-in `-- Grafana --` data source: {{< figure class="float-right" src="/media/docs/grafana/dashboards/screenshot-data-source-selector-10.0.png" max-width="800px" alt="Select data source dialog box" >}} - This configures your [query]({{< relref "../panels-visualizations/query-transform-data#add-a-query" >}}) and generates the Random Walk dashboard. - -1. Click the Refresh dashboard icon to query the data source. - - ![Refresh dashboard icon](/media/docs/grafana/dashboards/screenshot-refresh-dashboard-9.5.png) + This configures your [query](https://grafana.com/docs/grafana//panels-visualizations/query-transform-data/#add-a-query) and generates the Random Walk dashboard. -1. When you've finished editing your panel, click **Save** to save the dashboard. +1. Click **Refresh** to query the data source. +1. When you've finished editing your panel, click **Save dashboard**. - Alternatively, click **Apply** if you want to see your changes applied to the dashboard first. Then click the save icon in the dashboard header. + Alternatively, click **Back to dashboard** if you want to see your changes applied to the dashboard first. Then click **Save dashboard** when you're ready. 1. Add a descriptive title for the dashboard, or have Grafana create one using [generative AI features](https://grafana.com/docs/grafana//dashboards/manage-dashboards#set-up-generative-ai-features-for-dashboards), and then click **Save**. +1. Click **Back to dashboard** and then **Exit edit**. Congratulations, you have created your first dashboard and it's displaying results. #### Next steps -Continue to experiment with what you have built, try the [explore workflow]({{< relref "../explore" >}}) or another visualization feature. Refer to [Data sources]({{< relref "../datasources" >}}) for a list of supported data sources and instructions on how to [add a data source]({{< relref "../datasources#add-a-data-source" >}}). The following topics will be of interest to you: +Continue to experiment with what you have built, try the [explore workflow](https://grafana.com/docs/grafana//explore/) or another visualization feature. Refer to [Data sources](https://grafana.com/docs/grafana//datasources/) for a list of supported data sources and instructions on how to [add a data source](https://grafana.com/docs/grafana//datasources/#add-a-data-source). The following topics will be of interest to you: -- [Panels and visualizations]({{< relref "../panels-visualizations" >}}) -- [Dashboards]({{< relref "../dashboards" >}}) -- [Keyboard shortcuts]({{< relref "../dashboards/use-dashboards#keyboard-shortcuts" >}}) +- [Panels and visualizations](https://grafana.com/docs/grafana//panels-visualizations/) +- [Dashboards](https://grafana.com/docs/grafana//dashboards/) +- [Keyboard shortcuts](https://grafana.com/docs/grafana//dashboards/use-dashboards/#keyboard-shortcuts) - [Plugins](/grafana/plugins?orderBy=weight&direction=asc) ##### Admins The following topics are of interest to Grafana server admin users: -- [Grafana configuration]({{< relref "../setup-grafana/configure-grafana" >}}) -- [Authentication]({{< relref "../setup-grafana/configure-security/configure-authentication" >}}) -- [User permissions and roles]({{< relref "../administration/roles-and-permissions" >}}) -- [Provisioning]({{< relref "../administration/provisioning" >}}) -- [Grafana CLI]({{< relref "../cli" >}}) +- [Grafana configuration](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/) +- [Authentication](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/) +- [User permissions and roles](https://grafana.com/docs/grafana//administration/roles-and-permissions/) +- [Provisioning](https://grafana.com/docs/grafana//administration/provisioning/) +- [Grafana CLI](https://grafana.com/docs/grafana//cli/) diff --git a/docs/sources/panels-visualizations/configure-data-links/index.md b/docs/sources/panels-visualizations/configure-data-links/index.md index a6f644da36e..1b454b46848 100644 --- a/docs/sources/panels-visualizations/configure-data-links/index.md +++ b/docs/sources/panels-visualizations/configure-data-links/index.md @@ -250,5 +250,5 @@ If you want to add all of the current dashboard's variables to the URL, then use 1. If you want the link to open in a new tab, then toggle the **Open in a new tab** switch. 1. Click **Save** to save changes and close the dialog box. -1. Click **Apply** to see your changes in the dashboard. -1. Click the **Save dashboard** icon to save your changes to the dashboard. +1. Click **Save dashboard**. +1. Click **Back to dashboard** and then **Exit edit**. diff --git a/docs/sources/panels-visualizations/configure-overrides/index.md b/docs/sources/panels-visualizations/configure-overrides/index.md index c9ad3f7a815..2b7264ee6d4 100644 --- a/docs/sources/panels-visualizations/configure-overrides/index.md +++ b/docs/sources/panels-visualizations/configure-overrides/index.md @@ -244,7 +244,8 @@ To add a field override, follow these steps: 1. Select the field option that you want to apply. 1. Continue to add overrides to this field by clicking **Add override property**. 1. Add as many overrides as you need. -1. When you're finished, click **Save** to save all panel edits to the dashboard. +1. When you're finished, click **Save dashboard**. +1. Click **Back to dashboard** and then **Exit edit**. ## Edit a field override @@ -259,5 +260,7 @@ To edit a field override, follow these steps: - Edit settings on existing overrides or field selection parameters. - Delete existing override properties by clicking the **X** next to the property. - Delete an override entirely by clicking the trash icon at the top-right corner. +1. Click **Save dashboard**. +1. Click **Back to dashboard** and then **Exit edit**. The changes you make take effect immediately. diff --git a/docs/sources/panels-visualizations/configure-panel-options/index.md b/docs/sources/panels-visualizations/configure-panel-options/index.md index aa80d4f5def..59c466a20fb 100644 --- a/docs/sources/panels-visualizations/configure-panel-options/index.md +++ b/docs/sources/panels-visualizations/configure-panel-options/index.md @@ -95,7 +95,8 @@ To configure repeating panels, follow these steps: - **Vertical** - Arrange panels in a column. The width of repeated panels is the same as the original, repeated panel. 1. If you selected **Horizontal** in the previous step, select a value in the **Max per row** drop-down list to control the maximum number of panels that can be in a row. -1. Click **Save**. +1. Click **Save dashboard**. +1. Click **Back to dashboard** and then **Exit edit**. 1. To propagate changes to all panels, reload the dashboard. You can stop a panel from repeating by selecting **Disable repeating** in the **Repeat by variable** drop-down list. diff --git a/docs/sources/panels-visualizations/configure-thresholds/index.md b/docs/sources/panels-visualizations/configure-thresholds/index.md index 5c5c9bb3707..102b8ea4505 100644 --- a/docs/sources/panels-visualizations/configure-thresholds/index.md +++ b/docs/sources/panels-visualizations/configure-thresholds/index.md @@ -174,6 +174,8 @@ You can add as many thresholds to a visualization as you want. Grafana automatic 1. Click the colored circle to the left of the threshold value to open the color picker, where you can update the threshold color. 1. Under **Thresholds mode**, select either **Absolute** or **Percentage**. 1. Under **Show thresholds**, set how the threshold is displayed or turn it off. +1. Click **Save dashboard**. +1. Click **Back to dashboard** and then **Exit edit**. To delete a threshold, navigate to the panel that contains the threshold and click the trash icon next to the threshold you want to remove. diff --git a/docs/sources/panels-visualizations/configure-value-mappings/index.md b/docs/sources/panels-visualizations/configure-value-mappings/index.md index 8b9f77276cb..754eeed29ee 100644 --- a/docs/sources/panels-visualizations/configure-value-mappings/index.md +++ b/docs/sources/panels-visualizations/configure-value-mappings/index.md @@ -185,3 +185,6 @@ The following image shows a table visualization with value mappings. If you want 1. Click **Update** to save the value mapping. After you've added a mapping, the **Edit value mappings** button replaces the **Add value mappings** button. Click the edit button to add or update mappings. + +1. Click **Save dashboard**. +1. Click **Back to dashboard** and then **Exit edit**. diff --git a/docs/sources/panels-visualizations/panel-editor-overview/index.md b/docs/sources/panels-visualizations/panel-editor-overview/index.md index 38fba108702..4d131a900ca 100644 --- a/docs/sources/panels-visualizations/panel-editor-overview/index.md +++ b/docs/sources/panels-visualizations/panel-editor-overview/index.md @@ -52,7 +52,7 @@ refs: In the panel editor, you can update all the elements of a visualization including the data source, queries, time range, and visualization display options. -![Panel editor](/media/docs/grafana/panels-visualizations/screenshot-panel-editor-view.png) +![Panel editor](/media/docs/grafana/panels-visualizations/screenshot-grafana-11.2-panel-editor.png) This following sections describe the areas of the Grafana panel editor. @@ -60,26 +60,25 @@ This following sections describe the areas of the Grafana panel editor. The header section lists the dashboard in which the panel appears and the following controls: -- **Discard:** Discards changes you have made to the panel since you last saved the dashboard. -- **Save:** Saves changes you made to the panel. -- **Apply:** Applies changes you made and closes the panel editor, returning you to the dashboard. You'll have to save the dashboard to persist the applied changes. +- **Back to dashboard** - Return to the dashboard with changes applied, but not yet saved. +- **Discard panel changes** - Discard changes you have made to the panel since you last saved the dashboard. +- **Save dashboard** - Save your changes to the dashboard. ## Visualization preview The visualization preview section contains the following options: -- **Table view:** Convert any visualization to a table so you can see the data. Table views are helpful for troubleshooting. This view only contains the raw data. It doesn't include transformations you might have applied to the data or the formatting options available in the [Table](ref:table) visualization. -- **Fill:** The visualization preview fills the available space. If you change the width of the side pane or height of the bottom pane the visualization changes to fill the available space. -- **Actual:** The visualization preview has the exact size as the size on the dashboard. If not enough space is available, the visualization scales down preserving the aspect ratio. -- **Time range controls:** **Default** is either the browser local timezone or the timezone selected at a higher level. +- **Table view** - Convert any visualization to a table so you can see the data. Table views are helpful for troubleshooting. This view only contains the raw data. It doesn't include transformations you might have applied to the data or the formatting options available in the [Table](ref:table) visualization. +- **Time range controls** - **Default** is either the browser local timezone or the timezone selected at a higher level. +- **Refresh** - Query the data source. ## Data section The data section contains tabs where you enter queries, transform your data, and create alert rules (if applicable). -- **Query tab:** Select your data source and enter queries here. For more information, refer to [Add a query](ref:add-a-query). When you create a new dashboard, you'll be prompted to select a data source before you get to the panel editor. You set or update the data source in existing dashboards using the drop-down in the **Query** tab. -- **Transform tab:** Apply data transformations. For more information, refer to [Transform data](ref:transform-data). -- **Alert tab:** Write alert rules. For more information, refer to [the overview of Grafana Alerting](ref:the-overview-of-grafana-alerting). +- **Queries** - Select your data source and enter queries here. For more information, refer to [Add a query](ref:add-a-query). When you create a new dashboard, you'll be prompted to select a data source before you get to the panel editor. You set or update the data source in existing dashboards using the drop-down in the **Queries** tab. +- **Transformations** - Apply data transformations. For more information, refer to [Transform data](ref:transform-data). +- **Alert** - Write alert rules. For more information, refer to [the overview of Grafana Alerting](ref:the-overview-of-grafana-alerting). ## Panel display options diff --git a/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts index af517e367a2..6583afdedb2 100644 --- a/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts @@ -58,6 +58,10 @@ export interface TempoQuery extends common.DataQuery { * Defines the maximum number of spans per spanset that are returned from Tempo */ spss?: number; + /** + * For metric queries, the step size to use + */ + step?: string; /** * The type of the table that is used to display the search results */ diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 4e8acb6a3f3..544551fddda 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -8,8 +8,8 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/apimachinery/identity" dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/metrics" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -28,13 +28,13 @@ func (hs *HTTPServer) getCreatedSnapshotHandler() web.Handler { if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesSnapshots) { namespaceMapper := request.GetNamespaceMapper(hs.Cfg) return func(w http.ResponseWriter, r *http.Request) { - user, err := appcontext.User(r.Context()) + user, err := identity.GetRequester(r.Context()) if err != nil || user == nil { errhttp.Write(r.Context(), fmt.Errorf("no user"), w) return } r.URL.Path = "/apis/dashboardsnapshot.grafana.app/v0alpha1/namespaces/" + - namespaceMapper(user.OrgID) + "/dashboardsnapshots/create" + namespaceMapper(user.GetOrgID()) + "/dashboardsnapshots/create" hs.clientConfigProvider.DirectlyServeHTTP(w, r) } } diff --git a/pkg/api/ds_query.go b/pkg/api/ds_query.go index 77d02dc63fa..65f58ee81be 100644 --- a/pkg/api/ds_query.go +++ b/pkg/api/ds_query.go @@ -11,7 +11,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/middleware/requestmeta" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" @@ -43,12 +43,12 @@ func (hs *HTTPServer) getDSQueryEndpoint() web.Handler { // rewrite requests from /ds/query to the new query service namespaceMapper := request.GetNamespaceMapper(hs.Cfg) return func(w http.ResponseWriter, r *http.Request) { - user, err := appcontext.User(r.Context()) + user, err := identity.GetRequester(r.Context()) if err != nil || user == nil { errhttp.Write(r.Context(), fmt.Errorf("no user"), w) return } - r.URL.Path = "/apis/query.grafana.app/v0alpha1/namespaces/" + namespaceMapper(user.OrgID) + "/query" + r.URL.Path = "/apis/query.grafana.app/v0alpha1/namespaces/" + namespaceMapper(user.GetOrgID()) + "/query" hs.clientConfigProvider.DirectlyServeHTTP(w, r) } } diff --git a/pkg/apiserver/builder/helper.go b/pkg/apiserver/builder/helper.go index 195ff60f54a..8c0e84db2e2 100644 --- a/pkg/apiserver/builder/helper.go +++ b/pkg/apiserver/builder/helper.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/prometheus/client_golang/prometheus" "golang.org/x/mod/semver" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -26,7 +27,6 @@ import ( "github.com/grafana/grafana/pkg/apiserver/endpoints/filters" "github.com/grafana/grafana/pkg/services/apiserver/options" - "github.com/prometheus/client_golang/prometheus" ) // TODO: this is a temporary hack to make rest.Connecter work with resource level routes @@ -96,6 +96,7 @@ func SetupConfig( // Needs to run last in request chain to function as expected, hence we register it first. handler := filters.WithTracingHTTPLoggingAttributes(requestHandler) + handler = filters.WithRequester(handler) handler = genericapiserver.DefaultBuildHandlerChain(handler, c) handler = filters.WithAcceptHeader(handler) handler = filters.WithPathRewriters(handler, pathRewriters) diff --git a/pkg/apiserver/endpoints/filters/requester.go b/pkg/apiserver/endpoints/filters/requester.go new file mode 100644 index 00000000000..eb88aa690bd --- /dev/null +++ b/pkg/apiserver/endpoints/filters/requester.go @@ -0,0 +1,67 @@ +package filters + +import ( + "net/http" + "slices" + + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/klog" + + "github.com/grafana/grafana/pkg/apimachinery/identity" +) + +// WithRequester makes sure there is an identity.Requester in context +func WithRequester(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + requester, err := identity.GetRequester(ctx) + if err != nil { + // Find the kubernetes user info + info, ok := request.UserFrom(ctx) + if ok { + if info.GetName() == user.Anonymous { + requester = &identity.StaticRequester{ + Namespace: identity.NamespaceAnonymous, + Name: info.GetName(), + Login: info.GetName(), + Permissions: map[int64]map[string][]string{}, + } + } + + if info.GetName() == user.APIServerUser || + slices.Contains(info.GetGroups(), user.SystemPrivilegedGroup) { + orgId := int64(1) + requester = &identity.StaticRequester{ + UserID: 1, + OrgID: orgId, + Name: info.GetName(), + Login: info.GetName(), + OrgRole: identity.RoleAdmin, + IsGrafanaAdmin: true, + Permissions: map[int64]map[string][]string{ + orgId: { + "*": {"*"}, // all resources, all scopes + + // Dashboards do not support wildcard action + // dashboards.ActionDashboardsRead: {"*"}, + // dashboards.ActionDashboardsCreate: {"*"}, + // dashboards.ActionDashboardsWrite: {"*"}, + // dashboards.ActionDashboardsDelete: {"*"}, + // dashboards.ActionFoldersCreate: {"*"}, + // dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}, // access to read all folders + }, + }, + } + } + + if requester != nil { + req = req.WithContext(identity.WithRequester(ctx, requester)) + } else { + klog.V(5).Info("unable to map the k8s user to grafana requester", "user", info) + } + } + } + handler.ServeHTTP(w, req) + }) +} diff --git a/pkg/infra/appcontext/user.go b/pkg/infra/appcontext/user.go index c2a92da8d6a..d50eaacd8b9 100644 --- a/pkg/infra/appcontext/user.go +++ b/pkg/infra/appcontext/user.go @@ -4,13 +4,7 @@ import ( "context" "fmt" - k8suser "k8s.io/apiserver/pkg/authentication/user" - "k8s.io/apiserver/pkg/endpoints/request" - "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" - contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - "github.com/grafana/grafana/pkg/services/dashboards" grpccontext "github.com/grafana/grafana/pkg/services/grpcserver/context" "github.com/grafana/grafana/pkg/services/user" ) @@ -20,11 +14,16 @@ type ctxUserKey struct{} // WithUser adds the supplied SignedInUser to the context. func WithUser(ctx context.Context, usr *user.SignedInUser) context.Context { ctx = context.WithValue(ctx, ctxUserKey{}, usr) + // make sure it is also in the simplified version + if usr == nil || usr.IsNil() { + return identity.WithRequester(ctx, nil) + } return identity.WithRequester(ctx, usr) } // User extracts the SignedInUser from the supplied context. // Supports context set by appcontext.WithUser, gRPC server context, and HTTP ReqContext. +// Deprecated: use identity.GetRequester(ctx) when possible func User(ctx context.Context) (*user.SignedInUser, error) { // Set by appcontext.WithUser u, ok := ctx.Value(ctxUserKey{}).(*user.SignedInUser) @@ -38,44 +37,32 @@ func User(ctx context.Context) (*user.SignedInUser, error) { return grpcCtx.SignedInUser, nil } - // Set by incoming HTTP request - c, ok := ctxkey.Get(ctx).(*contextmodel.ReqContext) - if ok && c.SignedInUser != nil { - return c.SignedInUser, nil - } - - // Find the kubernetes user info - k8sUserInfo, ok := request.UserFrom(ctx) - if ok { - for _, group := range k8sUserInfo.GetGroups() { - switch group { - case k8suser.APIServerUser: - fallthrough - case k8suser.SystemPrivilegedGroup: - orgId := int64(1) - return &user.SignedInUser{ - UserID: 1, - OrgID: orgId, - Name: k8sUserInfo.GetName(), - Login: k8sUserInfo.GetName(), - OrgRole: identity.RoleAdmin, - IsGrafanaAdmin: true, - Permissions: map[int64]map[string][]string{ - orgId: { - "*": {"*"}, // all resources, all scopes - - // Dashboards do not support wildcard action - dashboards.ActionDashboardsRead: {"*"}, - dashboards.ActionDashboardsCreate: {"*"}, - dashboards.ActionDashboardsWrite: {"*"}, - dashboards.ActionDashboardsDelete: {"*"}, - dashboards.ActionFoldersCreate: {"*"}, - dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}, // access to read all folders - }, - }, - }, nil - } - } + // If the identity was set via requester, but not appcontext, we can map values + // NOTE: this path + requester, _ := identity.GetRequester(ctx) + if requester != nil { + id := requester.GetID() + userId, _ := id.UserID() + orgId := requester.GetOrgID() + return &user.SignedInUser{ + NamespacedID: id, + UserID: userId, + UserUID: requester.GetUID().ID(), + OrgID: orgId, + OrgName: requester.GetOrgName(), + OrgRole: requester.GetOrgRole(), + Login: requester.GetLogin(), + Email: requester.GetEmail(), + IsGrafanaAdmin: requester.GetIsGrafanaAdmin(), + Teams: requester.GetTeams(), + AuthID: requester.GetAuthID(), + AuthenticatedBy: requester.GetAuthenticatedBy(), + IDToken: requester.GetIDToken(), + Permissions: map[int64]map[string][]string{ + 0: requester.GetGlobalPermissions(), + orgId: requester.GetPermissions(), + }, + }, nil } return nil, fmt.Errorf("a SignedInUser was not found in the context") diff --git a/pkg/infra/appcontext/user_test.go b/pkg/infra/appcontext/user_test.go index 63300b13d17..55c9e0ac9a8 100644 --- a/pkg/infra/appcontext/user_test.go +++ b/pkg/infra/appcontext/user_test.go @@ -11,8 +11,6 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" - contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" grpccontext "github.com/grafana/grafana/pkg/services/grpcserver/context" "github.com/grafana/grafana/pkg/services/user" ) @@ -52,11 +50,9 @@ func TestUserFromContext(t *testing.T) { require.Equal(t, expected.UserID, actual.UserID) }) - t.Run("should return user set by HTTP ReqContext", func(t *testing.T) { + t.Run("should return user set as a requester", func(t *testing.T) { expected := testUser() - ctx := ctxkey.Set(context.Background(), &contextmodel.ReqContext{ - SignedInUser: expected, - }) + ctx := identity.WithRequester(context.Background(), expected) actual, err := appcontext.User(ctx) require.NoError(t, err) require.Equal(t, expected.UserID, actual.UserID) diff --git a/pkg/registry/apis/dashboard/authorizer.go b/pkg/registry/apis/dashboard/authorizer.go index 54a772591d5..f98c57d1383 100644 --- a/pkg/registry/apis/dashboard/authorizer.go +++ b/pkg/registry/apis/dashboard/authorizer.go @@ -5,8 +5,8 @@ import ( "k8s.io/apiserver/pkg/authorization/authorizer" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" @@ -19,7 +19,7 @@ func (b *DashboardsAPIBuilder) GetAuthorizer() authorizer.Authorizer { return authorizer.DecisionNoOpinion, "", nil } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return authorizer.DecisionDeny, "", err } @@ -27,7 +27,7 @@ func (b *DashboardsAPIBuilder) GetAuthorizer() authorizer.Authorizer { if attr.GetName() == "" { // Discourage use of the "list" command for non super admin users if attr.GetVerb() == "list" && attr.GetResource() == v0alpha1.DashboardResourceInfo.GroupResource().Resource { - if !user.IsGrafanaAdmin { + if !user.GetIsGrafanaAdmin() { return authorizer.DecisionDeny, "list summary objects (or connect as GrafanaAdmin)", err } } diff --git a/pkg/registry/apis/dashboardsnapshot/sql_storage.go b/pkg/registry/apis/dashboardsnapshot/sql_storage.go index b21231a8613..26fdb8fff57 100644 --- a/pkg/registry/apis/dashboardsnapshot/sql_storage.go +++ b/pkg/registry/apis/dashboardsnapshot/sql_storage.go @@ -9,8 +9,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" + "github.com/grafana/grafana/pkg/apimachinery/identity" dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" ) @@ -73,7 +73,7 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } diff --git a/pkg/registry/apis/datasource/authorizer.go b/pkg/registry/apis/datasource/authorizer.go index 354325a65b1..570b438d808 100644 --- a/pkg/registry/apis/datasource/authorizer.go +++ b/pkg/registry/apis/datasource/authorizer.go @@ -6,7 +6,7 @@ import ( "k8s.io/apiserver/pkg/authorization/authorizer" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/datasources" ) @@ -17,7 +17,7 @@ func (b *DataSourceAPIBuilder) GetAuthorizer() authorizer.Authorizer { if !attr.IsResourceRequest() { return authorizer.DecisionNoOpinion, "", nil } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return authorizer.DecisionDeny, "valid user is required", err } diff --git a/pkg/registry/apis/datasource/plugincontext.go b/pkg/registry/apis/datasource/plugincontext.go index 5871b45ab4c..aa9c01954da 100644 --- a/pkg/registry/apis/datasource/plugincontext.go +++ b/pkg/registry/apis/datasource/plugincontext.go @@ -7,9 +7,9 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" @@ -84,7 +84,7 @@ func (q *scopedDatasourceProvider) Get(ctx context.Context, uid string) (*v0alph if err != nil { return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } diff --git a/pkg/registry/apis/datasource/querier.go b/pkg/registry/apis/datasource/querier.go index 5811dbb8378..0a5806a4616 100644 --- a/pkg/registry/apis/datasource/querier.go +++ b/pkg/registry/apis/datasource/querier.go @@ -7,8 +7,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/datasources" @@ -108,7 +108,7 @@ func (q *DefaultQuerier) Datasource(ctx context.Context, name string) (*v0alpha1 if err != nil { return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } diff --git a/pkg/registry/apis/example/register.go b/pkg/registry/apis/example/register.go index edb2a90bbad..6dc5644bffa 100644 --- a/pkg/registry/apis/example/register.go +++ b/pkg/registry/apis/example/register.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" + "github.com/prometheus/client_golang/prometheus" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -20,12 +21,11 @@ import ( "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/validation/spec" + "github.com/grafana/grafana/pkg/apimachinery/identity" example "github.com/grafana/grafana/pkg/apis/example/v0alpha1" "github.com/grafana/grafana/pkg/apiserver/builder" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/prometheus/client_golang/prometheus" ) var _ builder.APIGroupBuilder = (*TestingAPIBuilder)(nil) @@ -216,7 +216,7 @@ func (b *TestingAPIBuilder) GetAuthorizer() authorizer.Authorizer { } // require a user - _, err = appcontext.User(ctx) + _, err = identity.GetRequester(ctx) if err != nil { return authorizer.DecisionDeny, "valid user is required", err } diff --git a/pkg/registry/apis/example/subresource.go b/pkg/registry/apis/example/subresource.go index 63fdcab5f02..3fa5807a9e9 100644 --- a/pkg/registry/apis/example/subresource.go +++ b/pkg/registry/apis/example/subresource.go @@ -8,8 +8,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" + "github.com/grafana/grafana/pkg/apimachinery/identity" example "github.com/grafana/grafana/pkg/apis/example/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" ) @@ -38,14 +38,14 @@ func (r *dummySubresourceREST) Connect(ctx context.Context, name string, opts ru return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } // This response object format is negotiated by k8s dummy := &example.DummySubresource{ - Info: fmt.Sprintf("%s/%s", info.Value, user.Login), + Info: fmt.Sprintf("%s/%s", info.Value, user.GetLogin()), } return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { diff --git a/pkg/registry/apis/folders/legacy_storage.go b/pkg/registry/apis/folders/legacy_storage.go index 5baedfdcefa..4802f2a2c86 100644 --- a/pkg/registry/apis/folders/legacy_storage.go +++ b/pkg/registry/apis/folders/legacy_storage.go @@ -11,9 +11,9 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/apiserver/storage/entity" "github.com/grafana/grafana/pkg/services/dashboards" @@ -83,7 +83,7 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } @@ -116,7 +116,7 @@ func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.Ge return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } @@ -146,7 +146,7 @@ func (s *legacyStorage) Create(ctx context.Context, return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } @@ -195,7 +195,7 @@ func (s *legacyStorage) Update(ctx context.Context, return nil, false, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, false, err } @@ -267,7 +267,7 @@ func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidatio if err != nil { return v, false, err // includes the not-found error } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, false, err } diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index d658c68db49..5b652dfb209 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/prometheus/client_golang/prometheus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -15,11 +16,11 @@ import ( common "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/apiserver/builder" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" @@ -27,7 +28,6 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/client_golang/prometheus" ) var _ builder.APIGroupBuilder = (*FolderAPIBuilder)(nil) @@ -190,7 +190,7 @@ func (b *FolderAPIBuilder) GetAuthorizer() authorizer.Authorizer { } // require a user - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return authorizer.DecisionDeny, "valid user is required", err } diff --git a/pkg/registry/apis/folders/sub_access.go b/pkg/registry/apis/folders/sub_access.go index be506be9ece..3e7319f88a0 100644 --- a/pkg/registry/apis/folders/sub_access.go +++ b/pkg/registry/apis/folders/sub_access.go @@ -7,8 +7,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" @@ -49,7 +49,7 @@ func (r *subAccessREST) Connect(ctx context.Context, name string, opts runtime.O if err != nil { return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } diff --git a/pkg/registry/apis/folders/sub_count.go b/pkg/registry/apis/folders/sub_count.go index f3b7d7e61b1..9f89b6a933d 100644 --- a/pkg/registry/apis/folders/sub_count.go +++ b/pkg/registry/apis/folders/sub_count.go @@ -7,8 +7,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/folder" ) @@ -46,7 +46,7 @@ func (r *subCountREST) NewConnectOptions() (runtime.Object, bool, string) { } func (r *subCountREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } diff --git a/pkg/services/apiserver/auth/authenticator/signedinuser.go b/pkg/services/apiserver/auth/authenticator/signedinuser.go index fceecd02e8a..2c3e568f0e8 100644 --- a/pkg/services/apiserver/auth/authenticator/signedinuser.go +++ b/pkg/services/apiserver/auth/authenticator/signedinuser.go @@ -8,38 +8,38 @@ import ( k8suser "k8s.io/apiserver/pkg/authentication/user" "k8s.io/klog/v2" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" ) var _ authenticator.RequestFunc = signedInUserAuthenticator func signedInUserAuthenticator(req *http.Request) (*authenticator.Response, bool, error) { ctx := req.Context() - signedInUser, err := appcontext.User(ctx) + signedInUser, err := identity.GetRequester(ctx) if err != nil { klog.V(5).Info("failed to get signed in user", "err", err) return nil, false, nil } userInfo := &k8suser.DefaultInfo{ - Name: signedInUser.Login, - UID: signedInUser.UserUID, + Name: signedInUser.GetLogin(), + UID: signedInUser.GetUID().ID(), Groups: []string{}, // In order to faithfully round-trip through an impersonation flow, Extra keys MUST be lowercase. // see: https://pkg.go.dev/k8s.io/apiserver@v0.27.1/pkg/authentication/user#Info Extra: map[string][]string{}, } - for _, v := range signedInUser.Teams { + for _, v := range signedInUser.GetTeams() { userInfo.Groups = append(userInfo.Groups, strconv.FormatInt(v, 10)) } // - if signedInUser.IDToken != "" { - userInfo.Extra["id-token"] = []string{signedInUser.IDToken} + if signedInUser.GetIDToken() != "" { + userInfo.Extra["id-token"] = []string{signedInUser.GetIDToken()} } - if signedInUser.OrgRole.IsValid() { - userInfo.Extra["user-instance-role"] = []string{string(signedInUser.OrgRole)} + if signedInUser.GetOrgRole().IsValid() { + userInfo.Extra["user-instance-role"] = []string{string(signedInUser.GetOrgRole())} } return &authenticator.Response{ diff --git a/pkg/services/apiserver/auth/authorizer/org_id.go b/pkg/services/apiserver/auth/authorizer/org_id.go index f73d3b59b32..f2630922280 100644 --- a/pkg/services/apiserver/auth/authorizer/org_id.go +++ b/pkg/services/apiserver/auth/authorizer/org_id.go @@ -6,7 +6,7 @@ import ( "k8s.io/apiserver/pkg/authorization/authorizer" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" grafanarequest "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/org" @@ -27,7 +27,7 @@ func newOrgIDAuthorizer(orgService org.Service) *orgIDAuthorizer { } func (auth orgIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { - signedInUser, err := appcontext.User(ctx) + signedInUser, err := identity.GetRequester(ctx) if err != nil { return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil } @@ -51,12 +51,16 @@ func (auth orgIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attribut } // Quick check that the same org is used - if signedInUser.OrgID == info.OrgID { + if signedInUser.GetOrgID() == info.OrgID { return authorizer.DecisionNoOpinion, "", nil } // Check if the user has access to the specified org - query := org.GetUserOrgListQuery{UserID: signedInUser.UserID} + userId, err := signedInUser.GetID().UserID() + if err != nil { + return authorizer.DecisionDeny, "unable to get userId", err + } + query := org.GetUserOrgListQuery{UserID: userId} result, err := auth.org.GetUserOrgList(ctx, &query) if err != nil { return authorizer.DecisionDeny, "error getting user org list", err @@ -68,5 +72,5 @@ func (auth orgIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attribut } } - return authorizer.DecisionDeny, fmt.Sprintf("user %d is not a member of org %d", signedInUser.UserID, info.OrgID), nil + return authorizer.DecisionDeny, fmt.Sprintf("user %d is not a member of org %d", userId, info.OrgID), nil } diff --git a/pkg/services/apiserver/auth/authorizer/org_role.go b/pkg/services/apiserver/auth/authorizer/org_role.go index 4717741a8d5..9c03f3ba795 100644 --- a/pkg/services/apiserver/auth/authorizer/org_role.go +++ b/pkg/services/apiserver/auth/authorizer/org_role.go @@ -6,7 +6,7 @@ import ( "k8s.io/apiserver/pkg/authorization/authorizer" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/org" ) @@ -22,12 +22,13 @@ func newOrgRoleAuthorizer(orgService org.Service) *orgRoleAuthorizer { } func (auth orgRoleAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { - signedInUser, err := appcontext.User(ctx) + signedInUser, err := identity.GetRequester(ctx) if err != nil { return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil } - switch signedInUser.OrgRole { + orgRole := signedInUser.GetOrgRole() + switch orgRole { case org.RoleAdmin: return authorizer.DecisionAllow, "", nil case org.RoleEditor: @@ -35,21 +36,21 @@ func (auth orgRoleAuthorizer) Authorize(ctx context.Context, a authorizer.Attrib case "get", "list", "watch", "create", "update", "patch", "delete", "put", "post": return authorizer.DecisionAllow, "", nil default: - return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(string(signedInUser.OrgRole), a), nil + return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(orgRole, a), nil } case org.RoleViewer: switch a.GetVerb() { case "get", "list", "watch": return authorizer.DecisionAllow, "", nil default: - return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(string(signedInUser.OrgRole), a), nil + return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(orgRole, a), nil } case org.RoleNone: - return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(string(signedInUser.OrgRole), a), nil + return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(orgRole, a), nil } return authorizer.DecisionDeny, "", nil } -func errorMessageForGrafanaOrgRole(grafanaOrgRole string, a authorizer.Attributes) string { - return fmt.Sprintf("Grafana org role (%s) didn't allow %s access on requested resource=%s, path=%s", grafanaOrgRole, a.GetVerb(), a.GetResource(), a.GetPath()) +func errorMessageForGrafanaOrgRole(orgRole identity.RoleType, a authorizer.Attributes) string { + return fmt.Sprintf("Grafana org role (%s) didn't allow %s access on requested resource=%s, path=%s", orgRole, a.GetVerb(), a.GetResource(), a.GetPath()) } diff --git a/pkg/services/apiserver/auth/authorizer/stack_id.go b/pkg/services/apiserver/auth/authorizer/stack_id.go index da63c25d5ba..af4c309e226 100644 --- a/pkg/services/apiserver/auth/authorizer/stack_id.go +++ b/pkg/services/apiserver/auth/authorizer/stack_id.go @@ -6,7 +6,7 @@ import ( "k8s.io/apiserver/pkg/authorization/authorizer" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" grafanarequest "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/setting" @@ -27,7 +27,7 @@ func newStackIDAuthorizer(cfg *setting.Cfg) *stackIDAuthorizer { } func (auth stackIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { - signedInUser, err := appcontext.User(ctx) + signedInUser, err := identity.GetRequester(ctx) if err != nil { return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil } @@ -48,7 +48,7 @@ func (auth stackIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attrib if info.OrgID != 1 { return authorizer.DecisionDeny, "cloud instance requires org 1", nil } - if signedInUser.OrgID != 1 { + if signedInUser.GetOrgID() != 1 { return authorizer.DecisionDeny, "user must be in org 1", nil } diff --git a/pkg/services/apiserver/endpoints/request/namespace.go b/pkg/services/apiserver/endpoints/request/namespace.go index 899e342dc9f..2a4c768673b 100644 --- a/pkg/services/apiserver/endpoints/request/namespace.go +++ b/pkg/services/apiserver/endpoints/request/namespace.go @@ -8,7 +8,7 @@ import ( "k8s.io/apiserver/pkg/endpoints/request" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/setting" ) @@ -82,9 +82,9 @@ func ParseNamespace(ns string) (NamespaceInfo, error) { func OrgIDForList(ctx context.Context) (int64, error) { ns := request.NamespaceValue(ctx) if ns == "" { - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if user != nil { - return user.OrgID, err + return user.GetOrgID(), err } return -1, err } diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 44d2af2aa67..ace51eb5bb0 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -160,6 +160,11 @@ func ProvideService( req.URL.Path = "/" } + if c.SignedInUser != nil { + ctx := appcontext.WithUser(req.Context(), c.SignedInUser) + req = req.WithContext(ctx) + } + resp := responsewriter.WrapForHTTP1Or2(c.Resp) s.handler.ServeHTTP(resp, req) } diff --git a/pkg/services/datasources/service/legacy.go b/pkg/services/datasources/service/legacy.go index db5b008b13e..b138a906cd6 100644 --- a/pkg/services/datasources/service/legacy.go +++ b/pkg/services/datasources/service/legacy.go @@ -8,7 +8,7 @@ import ( data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/datasources" ) @@ -56,11 +56,11 @@ func (s *cachingLegacyDataSourceLookup) GetDataSourceFromDeprecatedFields(ctx co if id == 0 && name == "" { return nil, fmt.Errorf("either name or ID must be set") } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } - key := fmt.Sprintf("%d/%s/%d", user.OrgID, name, id) + key := fmt.Sprintf("%d/%s/%d", user.GetOrgID(), name, id) s.cacheMu.Lock() defer s.cacheMu.Unlock() @@ -70,13 +70,13 @@ func (s *cachingLegacyDataSourceLookup) GetDataSourceFromDeprecatedFields(ctx co } ds, err := s.retriever.GetDataSource(ctx, &datasources.GetDataSourceQuery{ - OrgID: user.OrgID, + OrgID: user.GetOrgID(), Name: name, ID: id, }) if errors.Is(err, datasources.ErrDataSourceNotFound) && name != "" { ds, err = s.retriever.GetDataSource(ctx, &datasources.GetDataSourceQuery{ - OrgID: user.OrgID, + OrgID: user.GetOrgID(), UID: name, // Sometimes name is actually the UID :( }) } diff --git a/pkg/services/libraryelements/accesscontrol.go b/pkg/services/libraryelements/accesscontrol.go index 4760f9cf2ae..68d789cad06 100644 --- a/pkg/services/libraryelements/accesscontrol.go +++ b/pkg/services/libraryelements/accesscontrol.go @@ -5,7 +5,7 @@ import ( "errors" "strings" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" @@ -47,7 +47,7 @@ func LibraryPanelUIDScopeResolver(l *LibraryElementService, folderSvc folder.Ser return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } diff --git a/pkg/services/pluginsintegration/plugincontext/plugincontext.go b/pkg/services/pluginsintegration/plugincontext/plugincontext.go index 4d170d8867f..578e863c5d5 100644 --- a/pkg/services/pluginsintegration/plugincontext/plugincontext.go +++ b/pkg/services/pluginsintegration/plugincontext/plugincontext.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" @@ -95,7 +94,7 @@ func (p *Provider) GetWithDataSource(ctx context.Context, pluginID string, user } func (p *Provider) GetDataSourceInstanceSettings(ctx context.Context, uid string) (*backend.DataSourceInstanceSettings, error) { - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } @@ -115,7 +114,7 @@ func (p *Provider) PluginContextForDataSource(ctx context.Context, datasourceSet return backend.PluginContext{}, plugins.ErrPluginNotRegistered } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return backend.PluginContext{}, err } diff --git a/pkg/services/store/entity/grpc/authenticator.go b/pkg/services/store/entity/grpc/authenticator.go index c8e52721f92..5f0f3dd996d 100644 --- a/pkg/services/store/entity/grpc/authenticator.go +++ b/pkg/services/store/entity/grpc/authenticator.go @@ -5,11 +5,13 @@ import ( "fmt" "strconv" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/grpcserver/interceptors" "github.com/grafana/grafana/pkg/services/user" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" ) type Authenticator struct{} @@ -82,16 +84,17 @@ func StreamClientInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grp var _ grpc.StreamClientInterceptor = StreamClientInterceptor func WrapContext(ctx context.Context) (context.Context, error) { - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return ctx, err } // set grpc metadata into the context to pass to the grpc server return metadata.NewOutgoingContext(ctx, metadata.Pairs( - "grafana-idtoken", user.IDToken, - "grafana-userid", strconv.FormatInt(user.UserID, 10), - "grafana-orgid", strconv.FormatInt(user.OrgID, 10), - "grafana-login", user.Login, + "grafana-idtoken", user.GetIDToken(), + "grafana-userid", user.GetID().ID(), + "grafana-useruid", user.GetUID().ID(), + "grafana-orgid", strconv.FormatInt(user.GetOrgID(), 10), + "grafana-login", user.GetLogin(), )), nil } diff --git a/pkg/services/store/entity/sqlstash/sql_storage_server.go b/pkg/services/store/entity/sqlstash/sql_storage_server.go index 046ed69e4c7..a63bde240c4 100644 --- a/pkg/services/store/entity/sqlstash/sql_storage_server.go +++ b/pkg/services/store/entity/sqlstash/sql_storage_server.go @@ -17,7 +17,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" @@ -358,7 +358,7 @@ func (s *sqlEntityServer) History(ctx context.Context, r *entity.EntityHistoryRe return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { ctxLogger.Error("error getting user from ctx", "error", err) return nil, err @@ -573,7 +573,7 @@ func (s *sqlEntityServer) List(ctx context.Context, r *entity.EntityListRequest) return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { ctxLogger.Error("error getting user from ctx", "error", err) return nil, err @@ -803,7 +803,7 @@ func (s *sqlEntityServer) Watch(w entity.EntityStore_WatchServer) error { return err } - user, err := appcontext.User(w.Context()) + user, err := identity.GetRequester(w.Context()) if err != nil { ctxLogger.Error("error getting user from ctx", "error", err) return err @@ -1289,7 +1289,7 @@ func (s *sqlEntityServer) FindReferences(ctx context.Context, r *entity.Referenc return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { ctxLogger.Error("error getting user from ctx", "error", err) return nil, err diff --git a/pkg/services/store/entity/sqlstash/utils.go b/pkg/services/store/entity/sqlstash/utils.go index ade92f40374..3d4666b9649 100644 --- a/pkg/services/store/entity/sqlstash/utils.go +++ b/pkg/services/store/entity/sqlstash/utils.go @@ -8,7 +8,7 @@ import ( "fmt" "text/template" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/store/entity/db" "github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate" ) @@ -27,7 +27,7 @@ func createETag(body []byte, meta []byte, status []byte) string { // getCurrentUser returns a string identifying the user making a request with // the given context. func getCurrentUser(ctx context.Context) (string, error) { - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil || user == nil { return "", fmt.Errorf("%w: %w", ErrUserNotFoundInContext, err) } diff --git a/pkg/services/store/resolver/ds_cache.go b/pkg/services/store/resolver/ds_cache.go index 74e945b8beb..f0be2ed28d5 100644 --- a/pkg/services/store/resolver/ds_cache.go +++ b/pkg/services/store/resolver/ds_cache.go @@ -6,7 +6,7 @@ import ( "sync" "time" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/tsdb/grafanads" @@ -122,12 +122,12 @@ func (c *dsCache) getDS(ctx context.Context, uid string) (*dsVal, error) { } } - usr, err := appcontext.User(ctx) + usr, err := identity.GetRequester(ctx) if err != nil { return nil, nil // no user } - v, ok := c.cache[usr.OrgID] + v, ok := c.cache[usr.GetOrgID()] if !ok { return nil, nil // org not found } diff --git a/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go index bfb397b514d..4b3e3510266 100644 --- a/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go @@ -129,6 +129,9 @@ type TempoQuery struct { // Defines the maximum number of spans per spanset that are returned from Tempo Spss *int64 `json:"spss,omitempty"` + // For metric queries, the step size to use + Step *string `json:"step,omitempty"` + // The type of the table that is used to display the search results TableType *SearchTableType `json:"tableType,omitempty"` } diff --git a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx index 14a5db7e24b..8a9050c6355 100644 --- a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; import { sortBy } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useFormContext, FieldErrors, FieldValues, Controller } from 'react-hook-form'; +import { Controller, FieldErrors, FieldValues, useFormContext } from 'react-hook-form'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { Alert, Button, Field, Select, useStyles2 } from '@grafana/ui'; @@ -53,6 +53,7 @@ export function ChannelSubForm({ const { control, watch, register, trigger, formState, setValue } = useFormContext(); const selectedType = watch(fieldName('type')) ?? defaultValues.type; // nope, setting "default" does not work at all. + const parse_mode = watch(fieldName('settings.parse_mode')); const { loading: testingReceiver } = useUnifiedAlertingSelector((state) => state.testReceivers); // TODO I don't like integration specific code here but other ways require a bigger refactoring @@ -122,13 +123,14 @@ export function ChannelSubForm({ }; const notifier = notifiers.find(({ dto: { type } }) => type === selectedType); + const isTelegram = selectedType === 'telegram'; + const isParseModeNone = parse_mode === 'None'; // if there are mandatory options defined, optional options will be hidden by a collapse // if there aren't mandatory options, all options will be shown without collapse const mandatoryOptions = notifier?.dto.options.filter((o) => o.required); const optionalOptions = notifier?.dto.options.filter((o) => !o.required); const contactPointTypeInputId = `contact-point-type-${pathPrefix}`; - return (
@@ -188,6 +190,14 @@ export function ChannelSubForm({
{notifier && (
+ {isTelegram && !isParseModeNone && ( + + )} defaultValues={defaultValues} selectedChannelOptions={mandatoryOptions?.length ? mandatoryOptions! : optionalOptions!} diff --git a/public/app/plugins/datasource/tempo/dataquery.cue b/public/app/plugins/datasource/tempo/dataquery.cue index b47078559b0..69dd0f653e2 100644 --- a/public/app/plugins/datasource/tempo/dataquery.cue +++ b/public/app/plugins/datasource/tempo/dataquery.cue @@ -51,6 +51,8 @@ composableKinds: DataQuery: { groupBy?: [...#TraceqlFilter] // The type of the table that is used to display the search results tableType?: #SearchTableType + // For metric queries, the step size to use + step?: string } @cuetsy(kind="interface") @grafana(TSVeneer="type") #TempoQueryType: "traceql" | "traceqlSearch" | "serviceMap" | "upload" | "nativeSearch" | "traceId" | "clear" @cuetsy(kind="type") diff --git a/public/app/plugins/datasource/tempo/dataquery.gen.ts b/public/app/plugins/datasource/tempo/dataquery.gen.ts index 5e0bb9543ee..7086fcc5c63 100644 --- a/public/app/plugins/datasource/tempo/dataquery.gen.ts +++ b/public/app/plugins/datasource/tempo/dataquery.gen.ts @@ -56,6 +56,10 @@ export interface TempoQuery extends common.DataQuery { * Defines the maximum number of spans per spanset that are returned from Tempo */ spss?: number; + /** + * For metric queries, the step size to use + */ + step?: string; /** * The type of the table that is used to display the search results */ diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index bcf330a29ec..b7ae4f1c77d 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -438,7 +438,7 @@ export class TempoDatasource extends DataSourceWithBackend, queryValue: string ): Observable => { - return this._request('/api/metrics/query_range', { + const requestData = { query: queryValue, start: options.range.from.unix(), end: options.range.to.unix(), - }).pipe( + step: options.targets[0].step, + }; + + if (!requestData.step) { + delete requestData.step; + } + + return this._request('/api/metrics/query_range', requestData).pipe( map((response) => { return { data: formatTraceQLMetrics(queryValue, response.data), diff --git a/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx b/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx index 98658945e54..d4e40701ffb 100644 --- a/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx @@ -44,11 +44,15 @@ export const TempoQueryBuilderOptions = React.memo(({ onChange, query }) const onTableTypeChange = (val: SearchTableType) => { onChange({ ...query, tableType: val }); }; + const onStepChange = (e: React.FormEvent) => { + onChange({ ...query, step: e.currentTarget.value }); + }; const collapsedInfoList = [ `Limit: ${query.limit || DEFAULT_LIMIT}`, `Spans Limit: ${query.spss || DEFAULT_SPSS}`, `Table Format: ${query.tableType === SearchTableType.Traces ? 'Traces' : 'Spans'}`, + `Step: ${query.step || 'auto'}`, ]; return ( @@ -87,6 +91,19 @@ export const TempoQueryBuilderOptions = React.memo(({ onChange, query }) onChange={onTableTypeChange} /> + + +