Merge remote-tracking branch 'origin/main' into drclau/unistor/replace-authenticators-3

drclau/unistor/namespace_authorizer
gamab 9 months ago
commit a1b6408127
No known key found for this signature in database
GPG Key ID: 88D8810B587562C1
  1. 14
      .betterer.results
  2. 1
      .github/CODEOWNERS
  3. 2
      docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md
  4. 10
      docs/sources/alerting/set-up/configure-high-availability/_index.md
  5. 1
      packages/grafana-data/src/types/trace.ts
  6. 2
      packages/grafana-ui/src/components/Combobox/Combobox.test.tsx
  7. 2
      pkg/services/accesscontrol/resourcepermissions/store.go
  8. 8
      pkg/tsdb/tempo/trace_transform.go
  9. 26
      pkg/tsdb/tempo/trace_transform_test.go
  10. 2
      public/app/app.ts
  11. 8
      public/app/features/alerting/unified/Silences.test.tsx
  12. 4
      public/app/features/alerting/unified/api/alertmanagerApi.ts
  13. 7
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx
  14. 2
      public/app/features/alerting/unified/components/silences/SilencedInstancesPreview.tsx
  15. 31
      public/app/features/alerting/unified/mocks/server/handlers/alertmanagers.ts
  16. 2
      public/app/features/all.ts
  17. 2
      public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx
  18. 24
      public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx
  19. 2
      public/app/features/dashboard-scene/scene/DashboardScene.test.tsx
  20. 4
      public/app/features/dashboard-scene/scene/DashboardScene.tsx
  21. 97
      public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx
  22. 27
      public/app/features/dashboard/components/DeleteDashboard/DeleteDashboardModal.tsx
  23. 7
      public/app/features/dashboard/index.ts
  24. 20
      public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx
  25. 44
      public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.test.tsx
  26. 24
      public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx
  27. 3
      public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx
  28. 1
      public/app/features/explore/TraceView/components/types/trace.ts
  29. 5
      public/app/features/explore/TraceView/components/utils/filter-spans.test.ts
  30. 3
      public/app/features/explore/TraceView/components/utils/filter-spans.tsx
  31. 44
      public/app/features/manage-dashboards/state/actions.ts
  32. 1
      public/app/features/plugins/all.ts
  33. 1
      public/app/plugins/datasource/jaeger/_importedDependencies/types/trace.ts
  34. 1
      public/app/plugins/datasource/jaeger/types.ts
  35. 4
      public/app/plugins/datasource/tempo/resultTransformer.ts
  36. 25
      public/app/plugins/datasource/tempo/test/testResponse.ts
  37. 5
      public/locales/en-US/grafana.json
  38. 5
      public/locales/pseudo-LOCALE/grafana.json
  39. 6
      yarn.lock

@ -2886,8 +2886,7 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"]
], ],
"public/app/features/dashboard-scene/settings/JsonModelEditView.tsx:5381": [ "public/app/features/dashboard-scene/settings/JsonModelEditView.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
@ -4097,7 +4096,9 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"] [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"]
], ],
"public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx:5381": [ "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "0"],
@ -4679,12 +4680,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "5"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"], [0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"], [0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"], [0, 0, 0, "Unexpected any. Specify a different type.", "8"]
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
[0, 0, 0, "Unexpected any. Specify a different type.", "13"]
], ],
"public/app/features/manage-dashboards/state/reducers.ts:5381": [ "public/app/features/manage-dashboards/state/reducers.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "0"],

@ -403,7 +403,6 @@ playwright.config.ts @grafana/plugins-platform-frontend
/public/app/core/components/TimelineChart/ @grafana/dataviz-squad /public/app/core/components/TimelineChart/ @grafana/dataviz-squad
/public/app/core/components/Form/ @grafana/grafana-frontend-platform /public/app/core/components/Form/ @grafana/grafana-frontend-platform
/public/app/core/history/ @grafana/explore-squad /public/app/core/history/ @grafana/explore-squad
/public/app/features/all.ts @grafana/grafana-frontend-platform
/public/app/features/admin/ @grafana/identity-access-team /public/app/features/admin/ @grafana/identity-access-team
# Temp owners until Enterprise team takes over # Temp owners until Enterprise team takes over

@ -83,7 +83,7 @@ Grafana-managed rules are the most flexible alert rule type. They allow you to c
Multiple alert instances can be created as a result of one alert rule (also known as a multi-dimensional alerting). Multiple alert instances can be created as a result of one alert rule (also known as a multi-dimensional alerting).
{{% admonition type="note" %}} {{% admonition type="note" %}}
For Grafana Cloud, you can create 100 free Grafana-managed alert rules. For Grafana Cloud Free Forever, you can create up to 100 free Grafana-managed alert rules with each alert rule having a maximum of 1000 alert instances.
{{% /admonition %}} {{% /admonition %}}
Grafana managed alert rules can only be edited or deleted by users with Edit permissions for the folder storing the rules. Grafana managed alert rules can only be edited or deleted by users with Edit permissions for the folder storing the rules.

@ -190,3 +190,13 @@ Note that these alerting high availability metrics are exposed via the `/metrics
``` ```
For more information on monitoring alerting metrics, refer to [Alerting meta-monitoring](ref:meta-monitoring). For a demo, see [alerting high availability examples using Docker Compose](https://github.com/grafana/alerting-ha-docker-examples/). For more information on monitoring alerting metrics, refer to [Alerting meta-monitoring](ref:meta-monitoring). For a demo, see [alerting high availability examples using Docker Compose](https://github.com/grafana/alerting-ha-docker-examples/).
## Prevent duplicate notifications
In high-availability mode, each Grafana instance runs its own pre-configured alertmanager to handle alert notifications.
When multiple Grafana instances are running, all alert rules are evaluated on each instance. By default, each instance sends firing alerts to its respective alertmanager. This results in notification handling being duplicated across all running Grafana instances.
Alertmanagers in HA mode communicate with each other to coordinate notification delivery. However, this setup can sometimes lead to duplicated or out-of-order notifications. By design, HA prioritizes sending duplicate notifications over the risk of missing notifications.
To avoid duplicate notifications, you can configure a shared alertmanager to manage notifications for all Grafana instances. For more information, refer to [add an external alertmanager](/docs/grafana/<GRAFANA_VERSION>/alerting/set-up/configure-alertmanager/).

@ -13,6 +13,7 @@ export type TraceLog = {
// Millisecond epoch time // Millisecond epoch time
timestamp: number; timestamp: number;
fields: TraceKeyValuePair[]; fields: TraceKeyValuePair[];
name?: string;
}; };
export type TraceSpanReference = { export type TraceSpanReference = {

@ -37,7 +37,7 @@ describe('Combobox', () => {
render(<Combobox options={options} onChange={onChangeHandler} value={null} />); render(<Combobox options={options} onChange={onChangeHandler} value={null} />);
const input = screen.getByRole('combobox'); const input = screen.getByRole('combobox');
userEvent.click(input); await userEvent.click(input);
const item = await screen.findByRole('option', { name: 'Option 1' }); const item = await screen.findByRole('option', { name: 'Option 1' });
await userEvent.click(item); await userEvent.click(item);

@ -725,7 +725,7 @@ func (s *store) createPermissions(sess *db.Session, roleID int64, cmd SetResourc
} }
func (s *store) shouldStoreActionSet(resource, permission string) bool { func (s *store) shouldStoreActionSet(resource, permission string) bool {
if !(s.features.IsEnabled(context.TODO(), featuremgmt.FlagAccessActionSets) && permission != "") { if permission == "" {
return false return false
} }
actionSetName := GetActionSetName(resource, permission) actionSetName := GetActionSetName(resource, permission)

@ -22,6 +22,7 @@ type TraceLog struct {
// Millisecond epoch time // Millisecond epoch time
Timestamp float64 `json:"timestamp"` Timestamp float64 `json:"timestamp"`
Fields []*KeyValue `json:"fields"` Fields []*KeyValue `json:"fields"`
Name string `json:"name,omitempty"`
} }
type TraceReference struct { type TraceReference struct {
@ -260,12 +261,6 @@ func spanEventsToLogs(events ptrace.SpanEventSlice) []*TraceLog {
for i := 0; i < events.Len(); i++ { for i := 0; i < events.Len(); i++ {
event := events.At(i) event := events.At(i)
fields := make([]*KeyValue, 0, event.Attributes().Len()+1) fields := make([]*KeyValue, 0, event.Attributes().Len()+1)
if event.Name() != "" {
fields = append(fields, &KeyValue{
Key: TagMessage,
Value: event.Name(),
})
}
event.Attributes().Range(func(key string, attr pcommon.Value) bool { event.Attributes().Range(func(key string, attr pcommon.Value) bool {
fields = append(fields, &KeyValue{Key: key, Value: getAttributeVal(attr)}) fields = append(fields, &KeyValue{Key: key, Value: getAttributeVal(attr)})
return true return true
@ -273,6 +268,7 @@ func spanEventsToLogs(events ptrace.SpanEventSlice) []*TraceLog {
logs = append(logs, &TraceLog{ logs = append(logs, &TraceLog{
Timestamp: float64(event.Timestamp()) / 1_000_000, Timestamp: float64(event.Timestamp()) / 1_000_000,
Fields: fields, Fields: fields,
Name: event.Name(),
}) })
} }

@ -9,6 +9,7 @@ import (
"go.opentelemetry.io/collector/pdata/ptrace" "go.opentelemetry.io/collector/pdata/ptrace"
"github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -57,7 +58,30 @@ func TestTraceToFrame(t *testing.T) {
require.Equal(t, json.RawMessage("[{\"value\":\"loki-all\",\"key\":\"service.name\"},{\"value\":\"Jaeger-Go-2.25.0\",\"key\":\"opencensus.exporterversion\"},{\"value\":\"4d019a031941\",\"key\":\"host.hostname\"},{\"value\":\"172.18.0.6\",\"key\":\"ip\"},{\"value\":\"4b19ace06df8e4de\",\"key\":\"client-uuid\"}]"), span["serviceTags"]) require.Equal(t, json.RawMessage("[{\"value\":\"loki-all\",\"key\":\"service.name\"},{\"value\":\"Jaeger-Go-2.25.0\",\"key\":\"opencensus.exporterversion\"},{\"value\":\"4d019a031941\",\"key\":\"host.hostname\"},{\"value\":\"172.18.0.6\",\"key\":\"ip\"},{\"value\":\"4b19ace06df8e4de\",\"key\":\"client-uuid\"}]"), span["serviceTags"])
require.Equal(t, 1616072924072.852, span["startTime"]) require.Equal(t, 1616072924072.852, span["startTime"])
require.Equal(t, 0.094, span["duration"]) require.Equal(t, 0.094, span["duration"])
require.Equal(t, "[{\"timestamp\":1616072924072.856,\"fields\":[{\"value\":\"test event\",\"key\":\"message\"},{\"value\":1,\"key\":\"chunks requested\"}]},{\"timestamp\":1616072924072.9448,\"fields\":[{\"value\":1,\"key\":\"chunks fetched\"}]}]", string(span["logs"].(json.RawMessage))) expectedLogs := `
[
{
"timestamp": 1616072924072.856,
"name": "test event",
"fields": [
{
"value": 1,
"key": "chunks requested"
}
]
},
{
"timestamp": 1616072924072.9448,
"fields": [
{
"value": 1,
"key": "chunks fetched"
}
]
}
]
`
assert.JSONEq(t, expectedLogs, string(span["logs"].(json.RawMessage)))
}) })
t.Run("should transform correct traceID", func(t *testing.T) { t.Run("should transform correct traceID", func(t *testing.T) {

@ -6,8 +6,6 @@ import 'file-saver';
import 'jquery'; import 'jquery';
import 'vendor/bootstrap/bootstrap'; import 'vendor/bootstrap/bootstrap';
import 'app/features/all';
import _ from 'lodash'; // eslint-disable-line lodash/import-scope import _ from 'lodash'; // eslint-disable-line lodash/import-scope
import { createElement } from 'react'; import { createElement } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';

@ -304,6 +304,14 @@ describe('Silence create/edit', () => {
TEST_TIMEOUT TEST_TIMEOUT
); );
it('works when previewing alerts with spaces in label name', async () => {
renderSilences(`${baseUrlPath}?alertmanager=${GRAFANA_RULES_SOURCE_NAME}`);
await enterSilenceLabel(0, 'label with spaces', MatcherOperator.equal, 'value with spaces');
expect((await screen.findAllByTestId('row'))[0]).toBeInTheDocument();
});
it('shows an error when existing silence cannot be found', async () => { it('shows an error when existing silence cannot be found', async () => {
renderSilences('/alerting/silence/foo-bar/edit'); renderSilences('/alerting/silence/foo-bar/edit');

@ -72,7 +72,9 @@ export const alertmanagerApi = alertingApi.injectEndpoints({
// TODO Add support for active, silenced, inhibited, unprocessed filters // TODO Add support for active, silenced, inhibited, unprocessed filters
const filterMatchers = filter?.matchers const filterMatchers = filter?.matchers
?.filter((matcher) => matcher.name && matcher.value) ?.filter((matcher) => matcher.name && matcher.value)
.map((matcher) => `${matcher.name}${matcherToOperator(matcher)}${wrapWithQuotes(matcher.value)}`); .map(
(matcher) => `${wrapWithQuotes(matcher.name)}${matcherToOperator(matcher)}${wrapWithQuotes(matcher.value)}`
);
const { silenced, inhibited, unprocessed, active } = filter || {}; const { silenced, inhibited, unprocessed, active } = filter || {};

@ -14,6 +14,7 @@ import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedR
import { import {
getRuleGroupLocationFromFormValues, getRuleGroupLocationFromFormValues,
getRuleGroupLocationFromRuleWithLocation, getRuleGroupLocationFromRuleWithLocation,
isCloudRulerRule,
isGrafanaManagedRuleByType, isGrafanaManagedRuleByType,
isGrafanaRulerRule, isGrafanaRulerRule,
isGrafanaRulerRulePaused, isGrafanaRulerRulePaused,
@ -42,7 +43,7 @@ import {
formValuesToRulerGrafanaRuleDTO, formValuesToRulerGrafanaRuleDTO,
formValuesToRulerRuleDTO, formValuesToRulerRuleDTO,
} from '../../../utils/rule-form'; } from '../../../utils/rule-form';
import { fromRulerRuleAndRuleGroupIdentifier } from '../../../utils/rule-id'; import { fromRulerRule, fromRulerRuleAndRuleGroupIdentifier, stringifyIdentifier } from '../../../utils/rule-id';
import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter'; import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter';
import { AlertRuleNameAndMetric } from '../AlertRuleNameInput'; import { AlertRuleNameAndMetric } from '../AlertRuleNameInput';
import AnnotationsStep from '../AnnotationsStep'; import AnnotationsStep from '../AnnotationsStep';
@ -167,6 +168,10 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
if (exitOnSave && returnTo) { if (exitOnSave && returnTo) {
locationService.push(returnTo); locationService.push(returnTo);
} else if (isCloudRulerRule(ruleDefinition)) {
const { dataSourceName, namespaceName, groupName } = getRuleGroupLocationFromFormValues(values);
const updatedRuleIdentifier = fromRulerRule(dataSourceName, namespaceName, groupName, ruleDefinition);
locationService.replace(`/alerting/${encodeURIComponent(stringifyIdentifier(updatedRuleIdentifier))}/edit`);
} }
}; };

@ -66,7 +66,7 @@ export const SilencedInstancesPreview = ({ amSourceName, matchers: inputMatchers
if (isError) { if (isError) {
return ( return (
<Alert title="Preview not available" severity="error"> <Alert title="Preview not available" severity="error">
Error occured when generating preview of affected alerts. Are your matchers valid? Error occurred when generating preview of affected alerts. Are your matchers valid?
</Alert> </Alert>
); );
} }

@ -12,8 +12,37 @@ export const grafanaAlertingConfigurationStatusHandler = (
response = defaultGrafanaAlertingConfigurationStatusResponse response = defaultGrafanaAlertingConfigurationStatusResponse
) => http.get('/api/v1/ngalert', () => HttpResponse.json(response)); ) => http.get('/api/v1/ngalert', () => HttpResponse.json(response));
const getInvalidMatcher = (matchers: string[]) => {
return matchers.find((matcher) => {
const split = matcher.split('=');
try {
// Try and parse as JSON, as this will fail if
// we've failed to wrap the label value in quotes
// (e.g. `foo space` can't be parsed, but `"foo space"` can)
JSON.parse(split[0]);
return false;
} catch (e) {
return true;
}
});
};
export const alertmanagerAlertsListHandler = () => export const alertmanagerAlertsListHandler = () =>
http.get<{ datasourceUid: string }>('/api/alertmanager/:datasourceUid/api/v2/alerts', ({ params }) => { http.get<{ datasourceUid: string }>('/api/alertmanager/:datasourceUid/api/v2/alerts', ({ params, request }) => {
const matchers = new URL(request.url).searchParams.getAll('filter');
const invalidMatcher = getInvalidMatcher(matchers);
if (invalidMatcher) {
return HttpResponse.json(
{
message: `bad matcher format: ${invalidMatcher}: unable to retrieve alerts`,
traceID: '',
},
{ status: 400 }
);
}
if (params.datasourceUid === MOCK_DATASOURCE_UID_BROKEN_ALERTMANAGER) { if (params.datasourceUid === MOCK_DATASOURCE_UID_BROKEN_ALERTMANAGER) {
return HttpResponse.json({ traceId: '' }, { status: 502 }); return HttpResponse.json({ traceId: '' }, { status: 502 });
} }

@ -1,2 +0,0 @@
import './plugins/all';
import './dashboard';

@ -143,7 +143,7 @@ const dashboard = {
from: 'now-6h', from: 'now-6h',
to: 'now', to: 'now',
}, },
timepicker: { refresh_intervals: 5 }, timepicker: { refresh_intervals: ['5s', '30s', '1m'] },
meta: { meta: {
canSave: true, canSave: true,
folderId: 1, folderId: 1,

@ -3,7 +3,8 @@ import { useEffect, useMemo, useState } from 'react';
import { useLocalStorage } from 'react-use'; import { useLocalStorage } from 'react-use';
import { GrafanaTheme2, PanelData, SelectableValue } from '@grafana/data'; import { GrafanaTheme2, PanelData, SelectableValue } from '@grafana/data';
import { CustomScrollbar, Field, FilterInput, RadioButtonGroup, useStyles2 } from '@grafana/ui'; import { selectors } from '@grafana/e2e-selectors';
import { Button, CustomScrollbar, Field, FilterInput, RadioButtonGroup, useStyles2 } from '@grafana/ui';
import { LS_VISUALIZATION_SELECT_TAB_KEY, LS_WIDGET_SELECT_TAB_KEY } from 'app/core/constants'; import { LS_VISUALIZATION_SELECT_TAB_KEY, LS_WIDGET_SELECT_TAB_KEY } from 'app/core/constants';
import { VisualizationSelectPaneTab } from 'app/features/dashboard/components/PanelEditor/types'; import { VisualizationSelectPaneTab } from 'app/features/dashboard/components/PanelEditor/types';
import { VisualizationSuggestions } from 'app/features/panel/components/VizTypePicker/VisualizationSuggestions'; import { VisualizationSuggestions } from 'app/features/panel/components/VizTypePicker/VisualizationSuggestions';
@ -61,8 +62,13 @@ export function PanelVizTypePicker({ vizManager, data, onChange }: Props) {
onChange(); onChange();
}; };
const onCloseVizPicker = () => {
onChange();
};
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<div className={styles.searchRow}>
<FilterInput <FilterInput
className={styles.filter} className={styles.filter}
value={searchQuery} value={searchQuery}
@ -70,6 +76,15 @@ export function PanelVizTypePicker({ vizManager, data, onChange }: Props) {
autoFocus={true} autoFocus={true}
placeholder="Search for..." placeholder="Search for..."
/> />
<Button
title="Close"
variant="secondary"
icon="angle-up"
className={styles.closeButton}
data-testid={selectors.components.PanelEditor.toggleVizPicker}
onClick={onCloseVizPicker}
/>
</div>
<Field className={styles.customFieldMargin}> <Field className={styles.customFieldMargin}>
<RadioButtonGroup options={radioOptions} value={listMode} onChange={setListMode} fullWidth /> <RadioButtonGroup options={radioOptions} value={listMode} onChange={setListMode} fullWidth />
</Field> </Field>
@ -106,6 +121,13 @@ const getStyles = (theme: GrafanaTheme2) => ({
borderBottom: 'none', borderBottom: 'none',
borderTopLeftRadius: theme.shape.radius.default, borderTopLeftRadius: theme.shape.radius.default,
}), }),
searchRow: css({
display: 'flex',
marginBottom: theme.spacing(1),
}),
closeButton: css({
marginLeft: theme.spacing(1),
}),
customFieldMargin: css({ customFieldMargin: css({
marginBottom: theme.spacing(1), marginBottom: theme.spacing(1),
}), }),

@ -1000,7 +1000,7 @@ describe('DashboardScene', () => {
scene.setState({ isDirty: true }); scene.setState({ isDirty: true });
locationService.push('/d/adsdas'); locationService.push('/d/adsdas');
await scene.deleteDashboard(); await scene.onDashboardDelete();
expect(scene.state.isDirty).toBe(false); expect(scene.state.isDirty).toBe(false);
}); });

@ -34,7 +34,6 @@ import store from 'app/core/store';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { deleteDashboard } from 'app/features/manage-dashboards/state/actions';
import { getClosestScopesFacade, ScopesFacade } from 'app/features/scopes'; import { getClosestScopesFacade, ScopesFacade } from 'app/features/scopes';
import { VariablesChanged } from 'app/features/variables/types'; import { VariablesChanged } from 'app/features/variables/types';
import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types'; import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types';
@ -891,8 +890,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
this._initialSaveModel = saveModel; this._initialSaveModel = saveModel;
} }
public async deleteDashboard() { public async onDashboardDelete() {
await deleteDashboard(this.state.uid!, true);
// Need to mark it non dirty to navigate away without unsaved changes warning // Need to mark it non dirty to navigate away without unsaved changes warning
this.setState({ isDirty: false }); this.setState({ isDirty: false });
locationService.replace('/'); locationService.replace('/');

@ -2,17 +2,56 @@ import { useAsyncFn, useToggle } from 'react-use';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { config, reportInteraction } from '@grafana/runtime'; import { config, reportInteraction } from '@grafana/runtime';
import { Button, ConfirmModal, Modal } from '@grafana/ui'; import { Button, ConfirmModal, Modal, Space, Text } from '@grafana/ui';
import { Trans } from 'app/core/internationalization'; import { t, Trans } from 'app/core/internationalization';
import { useDeleteItemsMutation } from '../../browse-dashboards/api/browseDashboardsAPI';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
interface ButtonProps { interface ButtonProps {
dashboard: DashboardScene; dashboard: DashboardScene;
} }
interface ProvisionedDeleteModalProps {
dashboardId: string | undefined;
onClose: () => void;
}
interface DeleteModalProps {
dashboardTitle: string;
onConfirm: () => void;
onClose: () => void;
}
export function DeleteDashboardButton({ dashboard }: ButtonProps) { export function DeleteDashboardButton({ dashboard }: ButtonProps) {
const [showModal, toggleModal] = useToggle(false); const [showModal, toggleModal] = useToggle(false);
const [deleteItems] = useDeleteItemsMutation();
const [, onConfirm] = useAsyncFn(async () => {
reportInteraction('grafana_manage_dashboards_delete_clicked', {
item_counts: {
dashboard: 1,
},
source: 'dashboard_scene_settings',
restore_enabled: config.featureToggles.dashboardRestoreUI,
});
toggleModal();
if (dashboard.state.uid) {
await deleteItems({
selectedItems: {
dashboard: {
[dashboard.state.uid]: true,
},
folder: {},
},
});
}
await dashboard.onDashboardDelete();
}, [dashboard, toggleModal]);
if (dashboard.state.meta.provisioned && showModal) {
return <ProvisionedDeleteModal dashboardId={dashboard.state.meta.provisionedExternalId} onClose={toggleModal} />;
}
return ( return (
<> <>
@ -24,52 +63,48 @@ export function DeleteDashboardButton({ dashboard }: ButtonProps) {
<Trans i18nKey="dashboard-settings.dashboard-delete-button">Delete dashboard</Trans> <Trans i18nKey="dashboard-settings.dashboard-delete-button">Delete dashboard</Trans>
</Button> </Button>
{showModal && <DeleteDashboardModal dashboard={dashboard} onClose={toggleModal} />} {showModal && (
<DeleteDashboardModal dashboardTitle={dashboard.state.title} onConfirm={onConfirm} onClose={toggleModal} />
)}
</> </>
); );
} }
interface ModalProps { export function DeleteDashboardModal({ dashboardTitle, onConfirm, onClose }: DeleteModalProps) {
dashboard: DashboardScene;
onClose: () => void;
}
function DeleteDashboardModal({ dashboard, onClose }: ModalProps) {
const [, onConfirm] = useAsyncFn(async () => {
reportInteraction('grafana_manage_dashboards_delete_clicked', {
item_counts: {
dashboard: 1,
},
source: 'dashboard_scene_settings',
restore_enabled: config.featureToggles.dashboardRestoreUI,
});
onClose();
await dashboard.deleteDashboard();
}, [dashboard, onClose]);
if (dashboard.state.meta.provisioned) {
return <ProvisionedDeleteModal dashboard={dashboard} onClose={onClose} />;
}
return ( return (
<ConfirmModal <ConfirmModal
isOpen={true} isOpen={true}
body={ body={
<> <>
<p>Do you want to delete this dashboard?</p> {config.featureToggles.dashboardRestore && (
<p>{dashboard.state.title}</p> <>
<Text element="p">
<Trans i18nKey="dashboard-settings.delete-modal-restore-dashboards-text">
This action will mark the dashboard for deletion in 30 days. Your organization administrator can
restore it anytime before the 30 days expire.
</Trans>
</Text>
<Space v={1} />
</>
)}
<Text element="p">
<Trans i18nKey="dashboard-settings.delete-modal-text">Do you want to delete this dashboard?</Trans>
</Text>
<Text element="p">{dashboardTitle}</Text>
<Space v={2} />
</> </>
} }
onConfirm={onConfirm} onConfirm={onConfirm}
onDismiss={onClose} onDismiss={onClose}
title="Delete" title={t('dashboard-settings.delete-modal.title', 'Delete')}
icon="trash-alt" icon="trash-alt"
confirmText="Delete" confirmText={t('dashboard-settings.delete-modal.delete-button', 'Delete')}
confirmationText={t('dashboard-settings.delete-modal.confirmation-text', 'Delete')}
/> />
); );
} }
function ProvisionedDeleteModal({ dashboard, onClose }: ModalProps) { function ProvisionedDeleteModal({ dashboardId, onClose }: ProvisionedDeleteModalProps) {
return ( return (
<Modal isOpen={true} title="Cannot delete provisioned dashboard" icon="trash-alt" onDismiss={onClose}> <Modal isOpen={true} title="Cannot delete provisioned dashboard" icon="trash-alt" onDismiss={onClose}>
<p> <p>
@ -90,7 +125,7 @@ function ProvisionedDeleteModal({ dashboard, onClose }: ModalProps) {
for more information about provisioning. for more information about provisioning.
</i> </i>
<br /> <br />
File path: {dashboard.state.meta.provisionedExternalId} File path: {dashboardId}
</p> </p>
<Modal.ButtonRow> <Modal.ButtonRow>
<Button variant="primary" onClick={onClose}> <Button variant="primary" onClick={onClose}>

@ -3,12 +3,13 @@ import { connect, ConnectedProps } from 'react-redux';
import useAsyncFn from 'react-use/lib/useAsyncFn'; import useAsyncFn from 'react-use/lib/useAsyncFn';
import { locationService, config, reportInteraction } from '@grafana/runtime'; import { locationService, config, reportInteraction } from '@grafana/runtime';
import { Modal, ConfirmModal, Button, Text, Space, TextLink } from '@grafana/ui'; import { Modal, Button, Text, Space, TextLink } from '@grafana/ui';
import { DashboardModel } from 'app/features/dashboard/state'; import { DashboardModel } from 'app/features/dashboard/state';
import { cleanUpDashboardAndVariables } from 'app/features/dashboard/state/actions'; import { cleanUpDashboardAndVariables } from 'app/features/dashboard/state/actions';
import { Trans, t } from '../../../../core/internationalization'; import { Trans, t } from '../../../../core/internationalization';
import { useDeleteItemsMutation } from '../../../browse-dashboards/api/browseDashboardsAPI'; import { useDeleteItemsMutation } from '../../../browse-dashboards/api/browseDashboardsAPI';
import { DeleteDashboardModal as DeleteModal } from '../../../dashboard-scene/settings/DeleteDashboardButton';
type DeleteDashboardModalProps = { type DeleteDashboardModalProps = {
hideModal(): void; hideModal(): void;
@ -52,29 +53,7 @@ const DeleteDashboardModalUnconnected = ({ hideModal, cleanUpDashboardAndVariabl
return <ProvisionedDeleteModal hideModal={hideModal} provisionedId={dashboard.meta.provisionedExternalId!} />; return <ProvisionedDeleteModal hideModal={hideModal} provisionedId={dashboard.meta.provisionedExternalId!} />;
} }
return ( return <DeleteModal onConfirm={onConfirm} onClose={hideModal} dashboardTitle={dashboard.title} />;
<ConfirmModal
isOpen={true}
body={
<>
<Text element="p">
<Trans i18nKey="dashboard-settings.dashboard-delete-modal.text">
Do you want to delete this dashboard?
</Trans>
</Text>
<Space v={1} />
<Text element="p">{dashboard.title}</Text>
<Space v={2} />
</>
}
onConfirm={onConfirm}
onDismiss={hideModal}
title={t('dashboard-settings.dashboard-delete-modal.title', 'Delete')}
icon="trash-alt"
confirmText={t('dashboard-settings.dashboard-delete-modal.delete-button', 'Delete')}
confirmationText={t('dashboard-settings.dashboard-delete-modal.confirmation-text', 'Delete')}
/>
);
}; };
const ProvisionedDeleteModal = ({ hideModal, provisionedId }: { hideModal(): void; provisionedId: string }) => ( const ProvisionedDeleteModal = ({ hideModal, provisionedId }: { hideModal(): void; provisionedId: string }) => (

@ -1,7 +0,0 @@
// Services
import './services/DashboardLoaderSrv';
import './services/DashboardSrv';
// Components
import './components/DashExportModal';
import './components/DashNav';
import './components/DashboardSettings';

@ -67,7 +67,6 @@ export const getStyles = (theme: GrafanaTheme2) => {
summaryItem: css` summaryItem: css`
label: summaryItem; label: summaryItem;
display: inline; display: inline;
margin-left: 0.7em;
padding-right: 0.5rem; padding-right: 0.5rem;
border-right: 1px solid ${autoColor(theme, '#ddd')}; border-right: 1px solid ${autoColor(theme, '#ddd')};
&:last-child { &:last-child {
@ -90,10 +89,11 @@ export const getStyles = (theme: GrafanaTheme2) => {
export type AccordianKeyValuesProps = { export type AccordianKeyValuesProps = {
className?: string | TNil; className?: string | TNil;
data: TraceKeyValuePair[]; data: TraceKeyValuePair[];
logName?: string;
highContrast?: boolean; highContrast?: boolean;
interactive?: boolean; interactive?: boolean;
isOpen: boolean; isOpen: boolean;
label: string; label: string | React.ReactNode;
linksGetter?: ((pairs: TraceKeyValuePair[], index: number) => TraceLink[]) | TNil; linksGetter?: ((pairs: TraceKeyValuePair[], index: number) => TraceLink[]) | TNil;
onToggle?: null | (() => void); onToggle?: null | (() => void);
}; };
@ -127,6 +127,7 @@ export function KeyValuesSummary({ data = null }: KeyValuesSummaryProps) {
export default function AccordianKeyValues({ export default function AccordianKeyValues({
className = null, className = null,
data, data,
logName,
highContrast = false, highContrast = false,
interactive = true, interactive = true,
isOpen, isOpen,
@ -134,11 +135,12 @@ export default function AccordianKeyValues({
linksGetter, linksGetter,
onToggle = null, onToggle = null,
}: AccordianKeyValuesProps) { }: AccordianKeyValuesProps) {
const isEmpty = !Array.isArray(data) || !data.length; const isEmpty = (!Array.isArray(data) || !data.length) && !logName;
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const iconCls = cx(alignIcon, { [styles.emptyIcon]: isEmpty }); const iconCls = cx(alignIcon, { [styles.emptyIcon]: isEmpty });
let arrow: React.ReactNode | null = null; let arrow: React.ReactNode | null = null;
let headerProps: {} | null = null; let headerProps: {} | null = null;
const tableFields = logName ? [{ key: 'event name', value: logName }, ...data] : data;
if (interactive) { if (interactive) {
arrow = isOpen ? ( arrow = isOpen ? (
<Icon name={'angle-down'} className={iconCls} /> <Icon name={'angle-down'} className={iconCls} />
@ -152,6 +154,8 @@ export default function AccordianKeyValues({
}; };
} }
const showDataSummaryFields = data.length > 0 && !isOpen;
return ( return (
<div className={cx(className, styles.container)}> <div className={cx(className, styles.container)}>
<div <div
@ -165,11 +169,15 @@ export default function AccordianKeyValues({
{arrow} {arrow}
<strong data-test={markers.LABEL}> <strong data-test={markers.LABEL}>
{label} {label}
{isOpen || ':'} {showDataSummaryFields && ':'}
</strong> </strong>
{!isOpen && <KeyValuesSummary data={data} />} {showDataSummaryFields && (
<span className={css({ marginLeft: '0.7em' })}>
<KeyValuesSummary data={data} />
</span>
)}
</div> </div>
{isOpen && <KeyValuesTable data={data} linksGetter={linksGetter} />} {isOpen && <KeyValuesTable data={tableFields} linksGetter={linksGetter} />}
</div> </div>
); );
} }

@ -30,6 +30,7 @@ const logs = [
{ key: 'message', value: 'oh the next log message' }, { key: 'message', value: 'oh the next log message' },
{ key: 'more', value: 'stuff' }, { key: 'more', value: 'stuff' },
], ],
name: 'foo event name',
}, },
]; ];
@ -72,4 +73,47 @@ describe('AccordianLogs tests', () => {
expect(screen.getByText(/^something$/)).toBeInTheDocument(); expect(screen.getByText(/^something$/)).toBeInTheDocument();
expect(screen.getByText(/^else$/)).toBeInTheDocument(); expect(screen.getByText(/^else$/)).toBeInTheDocument();
}); });
it('shows log entries and long event name when expanded', () => {
const longNameLog = {
timestamp: 20,
name: 'This is a very very very very very very very long name',
fields: [{ key: 'foo', value: 'test' }],
};
setup({
isOpen: true,
logs: [longNameLog],
openedItems: new Set([longNameLog]),
} as AccordianLogsProps);
expect(
screen.getByRole('switch', {
name: '15μs (This is a very very ...)',
})
).toBeInTheDocument();
expect(screen.getByRole('table')).toBeInTheDocument();
expect(screen.queryAllByRole('cell')).toHaveLength(6);
expect(screen.getByText(/^event name$/)).toBeInTheDocument();
expect(screen.getByText(/This is a very very very very very very very long name/)).toBeInTheDocument();
});
it('renders event name and duration when events list is closed', () => {
setup({ isOpen: true, openedItems: new Set() } as AccordianLogsProps);
expect(
screen.getByRole('switch', {
name: '15μs (foo event name) : message = oh the next log message more = stuff',
})
).toBeInTheDocument();
expect(
screen.getByRole('switch', { name: '5μs: message = oh the log message something = else' })
).toBeInTheDocument();
});
it('renders event name and duration when events list is open', () => {
setup({ isOpen: true, openedItems: new Set(logs) } as AccordianLogsProps);
expect(screen.getByRole('switch', { name: '15μs (foo event name)' })).toBeInTheDocument();
expect(screen.getByRole('switch', { name: '5μs' })).toBeInTheDocument();
});
}); });

@ -59,6 +59,9 @@ const getStyles = (theme: GrafanaTheme2) => {
AccordianKeyValuesItem: css({ AccordianKeyValuesItem: css({
marginBottom: theme.spacing(0.5), marginBottom: theme.spacing(0.5),
}), }),
parenthesis: css({
color: `${autoColor(theme, '#777')}`,
}),
}; };
}; };
@ -108,22 +111,35 @@ export default function AccordianLogs({
</HeaderComponent> </HeaderComponent>
{isOpen && ( {isOpen && (
<div className={styles.AccordianLogsContent}> <div className={styles.AccordianLogsContent}>
{_sortBy(logs, 'timestamp').map((log, i) => ( {_sortBy(logs, 'timestamp').map((log, i) => {
const formattedDuration = formatDuration(log.timestamp - timestamp);
const truncateLogNameInSummary = log.name && log.name.length > 20;
const formattedLogName = log.name && truncateLogNameInSummary ? log.name.slice(0, 20) + '...' : log.name;
const label = formattedLogName ? (
<span>
{formattedDuration} <span>({formattedLogName})</span>
</span>
) : (
formattedDuration
);
return (
<AccordianKeyValues <AccordianKeyValues
// `i` is necessary in the key because timestamps can repeat // `i` is necessary in the key because timestamps can repeat
key={`${log.timestamp}-${i}`} key={`${log.timestamp}-${i}`}
className={i < logs.length - 1 ? styles.AccordianKeyValuesItem : null} className={i < logs.length - 1 ? styles.AccordianKeyValuesItem : null}
data={log.fields || []} data={log.fields || []}
logName={truncateLogNameInSummary ? log.name : undefined}
highContrast highContrast
interactive={interactive} interactive={interactive}
isOpen={openedItems ? openedItems.has(log) : false} isOpen={openedItems ? openedItems.has(log) : false}
label={`${formatDuration(log.timestamp - timestamp)}`} label={label}
linksGetter={linksGetter} linksGetter={linksGetter}
onToggle={interactive && onItemToggle ? () => onItemToggle(log) : null} onToggle={interactive && onItemToggle ? () => onItemToggle(log) : null}
/> />
))} );
})}
<small className={styles.AccordianLogsFooter}> <small className={styles.AccordianLogsFooter}>
Log timestamps are relative to the start time of the full trace. Event timestamps are relative to the start time of the full trace.
</small> </small>
</div> </div>
)} )}

@ -48,7 +48,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
row: css` row: css`
label: row; label: row;
& > td { & > td {
padding: 0rem 0.5rem; padding: 0.5rem 0.5rem;
height: 30px; height: 30px;
} }
&:nth-child(2n) > td { &:nth-child(2n) > td {
@ -63,6 +63,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
color: ${autoColor(theme, '#888')}; color: ${autoColor(theme, '#888')};
white-space: pre; white-space: pre;
width: 125px; width: 125px;
vertical-align: top;
`, `,
copyColumn: css` copyColumn: css`
label: copyColumn; label: copyColumn;

@ -31,6 +31,7 @@ export type TraceLink = {
export type TraceLog = { export type TraceLog = {
timestamp: number; timestamp: number;
fields: TraceKeyValuePair[]; fields: TraceKeyValuePair[];
name?: string;
}; };
export type TraceProcess = { export type TraceProcess = {

@ -55,6 +55,7 @@ describe('filterSpans', () => {
], ],
logs: [ logs: [
{ {
name: 'logName0',
fields: [ fields: [
{ {
key: 'logFieldKey0', key: 'logFieldKey0',
@ -316,6 +317,10 @@ describe('filterSpans', () => {
).toEqual(new Set([spanID0])); ).toEqual(new Set([spanID0]));
}); });
it('it should return logs have a name which matches the filter', () => {
expect(filterSpans({ ...defaultFilters, query: 'logName0' }, spans)).toEqual(new Set([spanID0]));
});
it('should return no spans when logs is null', () => { it('should return no spans when logs is null', () => {
const nullSpan = { ...span0, logs: null }; const nullSpan = { ...span0, logs: null };
expect( expect(

@ -90,7 +90,8 @@ export function getQueryMatches(query: string, spans: TraceSpan[] | TNil) {
(span.instrumentationLibraryName && isTextInQuery(queryParts, span.instrumentationLibraryName)) || (span.instrumentationLibraryName && isTextInQuery(queryParts, span.instrumentationLibraryName)) ||
(span.instrumentationLibraryVersion && isTextInQuery(queryParts, span.instrumentationLibraryVersion)) || (span.instrumentationLibraryVersion && isTextInQuery(queryParts, span.instrumentationLibraryVersion)) ||
(span.traceState && isTextInQuery(queryParts, span.traceState)) || (span.traceState && isTextInQuery(queryParts, span.traceState)) ||
(span.logs !== null && span.logs.some((log) => isTextInKeyValues(log.fields))) || (span.logs !== null &&
span.logs.some((log) => (log.name && isTextInQuery(queryParts, log.name)) || isTextInKeyValues(log.fields))) ||
isTextInKeyValues(span.process.tags) || isTextInKeyValues(span.process.tags) ||
queryParts.some((queryPart) => queryPart === span.spanID); queryParts.some((queryPart) => queryPart === span.spanID);

@ -3,7 +3,6 @@ import { getBackendSrv, getDataSourceSrv, isFetchError } from '@grafana/runtime'
import { notifyApp } from 'app/core/actions'; import { notifyApp } from 'app/core/actions';
import { createErrorNotification } from 'app/core/copy/appNotification'; import { createErrorNotification } from 'app/core/copy/appNotification';
import { browseDashboardsAPI, ImportInputs } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { browseDashboardsAPI, ImportInputs } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
import { FolderInfo, PermissionLevelString, SearchQueryType, ThunkResult } from 'app/types'; import { FolderInfo, PermissionLevelString, SearchQueryType, ThunkResult } from 'app/types';
import { import {
@ -281,39 +280,6 @@ export async function moveFolders(folderUIDs: string[], toFolder: FolderInfo) {
return result; return result;
} }
function createTask(fn: (...args: any[]) => Promise<any>, ignoreRejections: boolean, ...args: any[]) {
return async (result: any) => {
try {
const res = await fn(...args);
return Array.prototype.concat(result, [res]);
} catch (err) {
if (ignoreRejections) {
return result;
}
throw err;
}
};
}
export function deleteFoldersAndDashboards(folderUids: string[], dashboardUids: string[]) {
const tasks = [];
for (const folderUid of folderUids) {
tasks.push(createTask(deleteFolder, true, folderUid, true));
}
for (const dashboardUid of dashboardUids) {
tasks.push(createTask(deleteDashboard, true, dashboardUid, true));
}
return executeInOrder(tasks);
}
function deleteFolder(uid: string, showSuccessAlert: boolean) {
return getBackendSrv().delete(`/api/folders/${uid}?forceDeleteRules=false`, undefined, { showSuccessAlert });
}
export function createFolder(payload: any) { export function createFolder(payload: any) {
return getBackendSrv().post('/api/folders', payload); return getBackendSrv().post('/api/folders', payload);
} }
@ -346,13 +312,3 @@ export function getFolderByUid(uid: string): Promise<{ uid: string; title: strin
export function getFolderById(id: number): Promise<{ id: number; title: string }> { export function getFolderById(id: number): Promise<{ id: number; title: string }> {
return getBackendSrv().get(`/api/folders/id/${id}`); return getBackendSrv().get(`/api/folders/id/${id}`);
} }
export function deleteDashboard(uid: string, showSuccessAlert: boolean) {
return getDashboardAPI().deleteDashboard(uid, showSuccessAlert);
}
function executeInOrder(tasks: any[]): Promise<unknown> {
return tasks.reduce((acc, task) => {
return Promise.resolve(acc).then(task);
}, []);
}

@ -1 +0,0 @@
import './datasource_srv';

@ -31,6 +31,7 @@ export type TraceLink = {
export type TraceLog = { export type TraceLog = {
timestamp: number; timestamp: number;
fields: TraceKeyValuePair[]; fields: TraceKeyValuePair[];
name?: string;
}; };
export type TraceProcess = { export type TraceProcess = {

@ -14,6 +14,7 @@ export type TraceLink = {
export type TraceLog = { export type TraceLog = {
timestamp: number; timestamp: number;
fields: TraceKeyValuePair[]; fields: TraceKeyValuePair[];
name?: string;
}; };
export type TraceProcess = { export type TraceProcess = {

@ -132,7 +132,7 @@ function getLogs(span: collectorTypes.opentelemetryProto.trace.v1.Span) {
fields.push({ key: attribute.key, value: getAttributeValue(attribute.value) }); fields.push({ key: attribute.key, value: getAttributeValue(attribute.value) });
} }
} }
logs.push({ fields, timestamp: event.timeUnixNano / 1000000 }); logs.push({ fields, timestamp: event.timeUnixNano / 1000000, name: event.name });
} }
} }
@ -364,7 +364,7 @@ function getOTLPEvents(logs: TraceLog[]): collectorTypes.opentelemetryProto.trac
timeUnixNano: log.timestamp * 1000000, timeUnixNano: log.timestamp * 1000000,
attributes: [], attributes: [],
droppedAttributesCount: 0, droppedAttributesCount: 0,
name: '', name: log.name || '',
}; };
for (const field of log.fields) { for (const field of log.fields) {
event.attributes!.push({ event.attributes!.push({

@ -1920,7 +1920,7 @@ export const otlpDataFrameFromResponse = new MutableDataFrame({
name: 'logs', name: 'logs',
type: FieldType.other, type: FieldType.other,
config: {}, config: {},
values: [[]], values: [[{ name: 'DNSDone', fields: [{ key: 'addr', value: '172.18.0.6' }] }]],
}, },
{ {
name: 'references', name: 'references',
@ -2138,7 +2138,20 @@ export const otlpDataFrameToResponse = new MutableDataFrame({
name: 'logs', name: 'logs',
type: FieldType.other, type: FieldType.other,
config: {}, config: {},
values: [[]], values: [
[
{
fields: [
{
key: 'addr',
value: '172.18.0.6',
},
],
timestamp: 1627471657255.809,
name: 'DNSDone',
},
],
],
state: { state: {
displayName: 'logs', displayName: 'logs',
}, },
@ -2240,6 +2253,14 @@ export const otlpResponse = {
{ key: 'http.url', value: { stringValue: '/' } }, { key: 'http.url', value: { stringValue: '/' } },
{ key: 'component', value: { stringValue: 'net/http' } }, { key: 'component', value: { stringValue: 'net/http' } },
], ],
events: [
{
name: 'DNSDone',
attributes: [{ key: 'addr', value: { stringValue: '172.18.0.6' } }],
droppedAttributesCount: 0,
timeUnixNano: 1627471657255809000,
},
],
links: [ links: [
{ {
spanId: 'spanId', spanId: 'spanId',

@ -607,12 +607,13 @@
"title": "Annotations" "title": "Annotations"
}, },
"dashboard-delete-button": "Delete dashboard", "dashboard-delete-button": "Delete dashboard",
"dashboard-delete-modal": { "delete-modal": {
"confirmation-text": "Delete", "confirmation-text": "Delete",
"delete-button": "Delete", "delete-button": "Delete",
"text": "Do you want to delete this dashboard?",
"title": "Delete" "title": "Delete"
}, },
"delete-modal-restore-dashboards-text": "This action will mark the dashboard for deletion in 30 days. Your organization administrator can restore it anytime before the 30 days expire.",
"delete-modal-text": "Do you want to delete this dashboard?",
"general": { "general": {
"auto-refresh-description": "Define the auto refresh intervals that should be available in the auto refresh list. Use the format '5s' for seconds, '1m' for minutes, '1h' for hours, and '1d' for days (e.g.: '5s,10s,30s,1m,5m,15m,30m,1h,2h,1d').", "auto-refresh-description": "Define the auto refresh intervals that should be available in the auto refresh list. Use the format '5s' for seconds, '1m' for minutes, '1h' for hours, and '1d' for days (e.g.: '5s,10s,30s,1m,5m,15m,30m,1h,2h,1d').",
"auto-refresh-label": "Auto refresh", "auto-refresh-label": "Auto refresh",

@ -607,12 +607,13 @@
"title": "Åʼnʼnőŧäŧįőʼnş" "title": "Åʼnʼnőŧäŧįőʼnş"
}, },
"dashboard-delete-button": "Đęľęŧę đäşĥþőäřđ", "dashboard-delete-button": "Đęľęŧę đäşĥþőäřđ",
"dashboard-delete-modal": { "delete-modal": {
"confirmation-text": "Đęľęŧę", "confirmation-text": "Đęľęŧę",
"delete-button": "Đęľęŧę", "delete-button": "Đęľęŧę",
"text": "Đő yőū ŵäʼnŧ ŧő đęľęŧę ŧĥįş đäşĥþőäřđ?",
"title": "Đęľęŧę" "title": "Đęľęŧę"
}, },
"delete-modal-restore-dashboards-text": "Ŧĥįş äčŧįőʼn ŵįľľ mäřĸ ŧĥę đäşĥþőäřđ ƒőř đęľęŧįőʼn įʼn 30 đäyş. Ÿőūř őřģäʼnįžäŧįőʼn äđmįʼnįşŧřäŧőř čäʼn řęşŧőřę įŧ äʼnyŧįmę þęƒőřę ŧĥę 30 đäyş ęχpįřę.",
"delete-modal-text": "Đő yőū ŵäʼnŧ ŧő đęľęŧę ŧĥįş đäşĥþőäřđ?",
"general": { "general": {
"auto-refresh-description": "Đęƒįʼnę ŧĥę äūŧő řęƒřęşĥ įʼnŧęřväľş ŧĥäŧ şĥőūľđ þę äväįľäþľę įʼn ŧĥę äūŧő řęƒřęşĥ ľįşŧ. Ůşę ŧĥę ƒőřmäŧ '5ş' ƒőř şęčőʼnđş, '1m' ƒőř mįʼnūŧęş, '1ĥ' ƒőř ĥőūřş, äʼnđ '1đ' ƒőř đäyş (ę.ģ.: '5ş,10ş,30ş,1m,5m,15m,30m,1ĥ,2ĥ,1đ').", "auto-refresh-description": "Đęƒįʼnę ŧĥę äūŧő řęƒřęşĥ įʼnŧęřväľş ŧĥäŧ şĥőūľđ þę äväįľäþľę įʼn ŧĥę äūŧő řęƒřęşĥ ľįşŧ. Ůşę ŧĥę ƒőřmäŧ '5ş' ƒőř şęčőʼnđş, '1m' ƒőř mįʼnūŧęş, '1ĥ' ƒőř ĥőūřş, äʼnđ '1đ' ƒőř đäyş (ę.ģ.: '5ş,10ş,30ş,1m,5m,15m,30m,1ĥ,2ĥ,1đ').",
"auto-refresh-label": "Åūŧő řęƒřęşĥ", "auto-refresh-label": "Åūŧő řęƒřęşĥ",

@ -3933,8 +3933,8 @@ __metadata:
linkType: soft linkType: soft
"@grafana/scenes@npm:^5.11.1": "@grafana/scenes@npm:^5.11.1":
version: 5.11.1 version: 5.11.2
resolution: "@grafana/scenes@npm:5.11.1" resolution: "@grafana/scenes@npm:5.11.2"
dependencies: dependencies:
"@floating-ui/react": "npm:0.26.16" "@floating-ui/react": "npm:0.26.16"
"@grafana/e2e-selectors": "npm:^11.0.0" "@grafana/e2e-selectors": "npm:^11.0.0"
@ -3951,7 +3951,7 @@ __metadata:
"@grafana/ui": ">=10.4" "@grafana/ui": ">=10.4"
react: ^18.0.0 react: ^18.0.0
react-dom: ^18.0.0 react-dom: ^18.0.0
checksum: 10/15ec8bee9aa2aa8f5c64ed9fcaf4bd7c835162e0e63814556e7561e62462d5485f098131e411893e54ae3247692b348c8773a9459c30e45e936c5b0ef1a9d789 checksum: 10/1f6cded27acac813b1f039fa656efa476bcb2a444217c78c707441698d8d2dc053745fadcbad2dbe94a252d2613f1b32ac120fb11d887bb14f08a0bbea4c423b
languageName: node languageName: node
linkType: hard linkType: hard

Loading…
Cancel
Save