Repository: grafana/scenes
Pull request #1122: GroupByVariable: Add support for default values
Author: login: mdvictor
Labels: name: minor
description: Increment the minor version when merged
name: release
description: Create a release when this pr is merged
name: released
Created: 2025-05-14T09:23:04Z | Merged: 2025-05-22T13:34:36Z

Adds support for dashboard level default values in the groupBy variable.

SImilar to AdHoc filters, these new values saved on the dashboard for the groupBy represent default values that one can revert back to. Editing the values will show a restore icon and on click will restore the groupBy to the dashboard level default values.

TODO:
- [x] tests
- [x] UI polishing
<!-- GITHUB_RELEASE PR BODY: canary-version -->
<details>
  <summary>📦 Published PR as canary version: <code>6.13.0--canary.1122.15186594626.0</code></summary>
  <br />

  :sparkles: Test out this PR locally via:
  
  ```bash
  npm install @grafana/scenes-react@6.13.0--canary.1122.15186594626.0
  npm install @grafana/scenes@6.13.0--canary.1122.15186594626.0
  # or 
  yarn add @grafana/scenes-react@6.13.0--canary.1122.15186594626.0
  yarn add @grafana/scenes@6.13.0--canary.1122.15186594626.0
  ```
</details>
<!-- GITHUB_RELEASE PR BODY: canary-version -->



Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3918137fa..0c3472ff8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@
 
 #### Scopes: Replace ScopesBridge with ScopesVariable ([#1121](https://github.com/grafana/scenes/pull/1121))
 
-SceneScopesBridge is now replaced by ScopesVariable, which now needs to be added to a SceneVariableSet attached to every SceneAppPage or EmbeddedScene (whatever suits your scenario) where you want to use scopes. This variable is not visible in the UI. You can control whether scopes are enabled or disabled on a specific page/scene using the ScopesVariable or control this on a higher level using ScopesContext. 
+SceneScopesBridge is now replaced by ScopesVariable, which now needs to be added to a SceneVariableSet attached to every SceneAppPage or EmbeddedScene (whatever suits your scenario) where you want to use scopes. This variable is not visible in the UI. You can control whether scopes are enabled or disabled on a specific page/scene using the ScopesVariable or control this on a higher level using ScopesContext.
 
 sceneGraph.getScopesBridge is replaced by sceneGraph.getScopes, which just returns the scopes (by looking up the ScopesVariable and getting it from it's value).
 
diff --git a/packages/scenes/CHANGELOG.md b/packages/scenes/CHANGELOG.md
index c8bea9e12..9f939f5a3 100644
--- a/packages/scenes/CHANGELOG.md
+++ b/packages/scenes/CHANGELOG.md
@@ -4,7 +4,7 @@
 
 #### Scopes: Replace ScopesBridge with ScopesVariable ([#1121](https://github.com/grafana/scenes/pull/1121))
 
-SceneScopesBridge is now replaced by ScopesVariable, which now needs to be added to a SceneVariableSet attached to every SceneAppPage or EmbeddedScene (whatever suits your scenario) where you want to use scopes. This variable is not visible in the UI. You can control whether scopes are enabled or disabled on a specific page/scene using the ScopesVariable or control this on a higher level using ScopesContext. 
+SceneScopesBridge is now replaced by ScopesVariable, which now needs to be added to a SceneVariableSet attached to every SceneAppPage or EmbeddedScene (whatever suits your scenario) where you want to use scopes. This variable is not visible in the UI. You can control whether scopes are enabled or disabled on a specific page/scene using the ScopesVariable or control this on a higher level using ScopesContext.
 
 sceneGraph.getScopesBridge is replaced by sceneGraph.getScopes, which just returns the scopes (by looking up the ScopesVariable and getting it from it's value).
 
diff --git a/packages/scenes/src/variables/groupby/DefaultGroupByCustomIndicatorContainer.tsx b/packages/scenes/src/variables/groupby/DefaultGroupByCustomIndicatorContainer.tsx
new file mode 100644
index 000000000..676c90a8a
--- /dev/null
+++ b/packages/scenes/src/variables/groupby/DefaultGroupByCustomIndicatorContainer.tsx
@@ -0,0 +1,100 @@
+import React from 'react';
+import { Icon, IconButton, Tooltip, getInputStyles, useTheme2 } from '@grafana/ui';
+import { GroupByVariable } from './GroupByVariable';
+import { isArray } from 'lodash';
+import { css, cx } from '@emotion/css';
+import { GrafanaTheme2 } from '@grafana/data';
+
+type DefaultGroupByCustomIndicatorProps = {
+  model: GroupByVariable;
+};
+
+export function DefaultGroupByCustomIndicatorContainer(props: DefaultGroupByCustomIndicatorProps) {
+  const { model } = props;
+  const theme = useTheme2();
+  const styles = getStyles(theme);
+  const inputStyles = getInputStyles({ theme, invalid: false });
+  const value = isArray(model.state.value) ? model.state.value : model.state.value ? [model.state.value] : [];
+
+  let buttons: React.ReactNode[] = [];
+
+  if (value && value.length) {
+    buttons.push(
+      <IconButton
+        aria-label="clear"
+        key="clear"
+        name="times"
+        size="md"
+        className={styles.clearIcon}
+        onClick={(e) => {
+          model.changeValueTo([], undefined, true);
+          if (model.checkIfRestorable([])) {
+            model.setState({ restorable: true });
+          }
+        }}
+      />
+    );
+  }
+
+  if (model.state.restorable) {
+    buttons.push(
+      <IconButton
+        onClick={(e) => {
+          props.model.restoreDefaultValues();
+        }}
+        onKeyDownCapture={(e) => {
+          if (e.key === 'Enter') {
+            props.model.restoreDefaultValues();
+          }
+        }}
+        key="restore"
+        name="history"
+        size="md"
+        className={styles.clearIcon}
+        tooltip="Restore groupby set by this dashboard."
+      />
+    );
+  }
+
+  if (!model.state.restorable) {
+    buttons.push(
+      <Tooltip
+        key="tooltip"
+        content="Applied by default in this dashboard. If edited, it carries over to other dashboards."
+        placement="bottom"
+      >
+        <Icon name="info-circle" size="md" />
+      </Tooltip>
+    );
+  }
+
+  return (
+    <div
+      onMouseDown={(e) => {
+        e.preventDefault();
+        e.stopPropagation();
+      }}
+      className={cx(
+        inputStyles.suffix,
+        css({
+          position: 'relative',
+        })
+      )}
+    >
+      {buttons}
+    </div>
+  );
+}
+
+const getStyles = (theme: GrafanaTheme2) => ({
+  clearIcon: css({
+    color: theme.colors.action.disabledText,
+    cursor: 'pointer',
+    '&:hover:before': {
+      backgroundColor: 'transparent',
+    },
+    '&:hover': {
+      color: theme.colors.text.primary,
+    },
+  }),
+});
diff --git a/packages/scenes/src/variables/groupby/GroupByVariable.test.tsx b/packages/scenes/src/variables/groupby/GroupByVariable.test.tsx
index 70f582ced..2f69f11ae 100644
--- a/packages/scenes/src/variables/groupby/GroupByVariable.test.tsx
+++ b/packages/scenes/src/variables/groupby/GroupByVariable.test.tsx
@@ -169,6 +169,95 @@ describe.each(['11.1.2', '11.1.1'])('GroupByVariable', (v) => {
       expect(variable.state.text).toEqual(['A,something', 'b,something', 'C,something']);
     });
 
+    it('should set restorable if value differs from defaultValue', async () => {
+      const { variable } = setupTest({
+        value: ['defaultVal1', 'normalVal'],
+        defaultValue: {
+          value: ['defaultVal1'],
+          text: ['defaultVal1'],
+        },
+      });
+
+      await act(async () => {
+        await lastValueFrom(variable.validateAndUpdate());
+      });
+
+      expect(variable.state.value).toEqual(['defaultVal1', 'normalVal']);
+      expect(locationService.getLocation().search).toBe(
+        '?var-test=defaultVal1&var-test=normalVal&restorable-var-test=true'
+      );
+    });
+
+    it('should restore to default', async () => {
+      const { variable } = setupTest({
+        value: ['normalVal'],
+        defaultValue: {
+          value: ['defaultVal1'],
+          text: ['defaultVal1'],
+        },
+      });
+
+      await act(async () => {
+        await lastValueFrom(variable.validateAndUpdate());
+      });
+
+      expect(locationService.getLocation().search).toBe('?var-test=normalVal&restorable-var-test=true');
+
+      act(() => {
+        variable.restoreDefaultValues();
+      });
+
+      expect(variable.state.value).toEqual(['defaultVal1']);
+      expect(locationService.getLocation().search).toBe('?var-test=defaultVal1');
+    });
+
+    it('should set default values as current values if none are set', () => {
+      const { variable } = setupTest({
+        defaultValue: {
+          value: ['defaultVal1', 'defaultVal2'],
+          text: ['defaultVal1', 'defaultVal2'],
+        },
+      });
+
+      expect(variable.state.value).toEqual(['defaultVal1', 'defaultVal2']);
+      expect(variable.state.text).toEqual(['defaultVal1', 'defaultVal2']);
+    });
+
+    it('should see default values and be able to restore them', () => {
+      const { variable } = setupTest({
+        value: ['val1'],
+        text: ['val1'],
+        defaultValue: {
+          value: ['defaultVal1', 'defaultVal2'],
+          text: ['defaultVal1', 'defaultVal2'],
+        },
+      });
+
+      expect(variable.state.value).toEqual(['val1']);
+      expect(variable.state.text).toEqual(['val1']);
+      expect(variable.state.restorable).toBe(true);
+
+      variable.restoreDefaultValues();
+
+      expect(variable.state.value).toEqual(['defaultVal1', 'defaultVal2']);
+      expect(variable.state.text).toEqual(['defaultVal1', 'defaultVal2']);
+      expect(variable.state.defaultValue!.value).toEqual(['defaultVal1', 'defaultVal2']);
+      expect(variable.state.defaultValue!.text).toEqual(['defaultVal1', 'defaultVal2']);
+      expect(variable.state.restorable).toBe(false);
+    });
+
+    it('should not set variable as restorable if values are the same as default ones', () => {
+      const { variable } = setupTest({
+        value: ['defaultVal1', 'defaultVal2'],
+        defaultValue: {
+          value: ['defaultVal1', 'defaultVal2'],
+          text: ['defaultVal1', 'defaultVal2'],
+        },
+      });
+
+      expect(variable.state.value).toEqual(['defaultVal1', 'defaultVal2']);
+    });
+
     it('should work with browser history action on user action', () => {
       const { variable } = setupTest({
         defaultOptions: [
@@ -384,6 +473,23 @@ describe.each(['11.1.2', '11.1.1'])('GroupByVariable', (v) => {
 
       expect(getTagKeysSpy).toHaveBeenCalledTimes(1);
     });
+
+    it('should restore to default', async () => {
+      const { variable } = setupTest({
+        value: ['val'],
+        defaultValue: {
+          value: ['defaultValue'],
+          text: ['defaultValue'],
+        },
+      });
+
+      const restore = screen.getByLabelText('Restore groupby set by this dashboard.');
+
+      await userEvent.click(restore);
+
+      expect(screen.queryByLabelText('Restore groupby set by this dashboard.')).not.toBeInTheDocument();
+      expect(variable.state.value).toEqual(['defaultValue']);
+    });
   });
 });
 
diff --git a/packages/scenes/src/variables/groupby/GroupByVariable.tsx b/packages/scenes/src/variables/groupby/GroupByVariable.tsx
index fcc6e3202..e6351e0aa 100644
--- a/packages/scenes/src/variables/groupby/GroupByVariable.tsx
+++ b/packages/scenes/src/variables/groupby/GroupByVariable.tsx
@@ -4,18 +4,19 @@ import { allActiveGroupByVariables } from './findActiveGroupByVariablesByUid';
 import { DataSourceRef, VariableType } from '@grafana/schema';
 import { SceneComponentProps, ControlsLayout, SceneObjectUrlSyncHandler } from '../../core/types';
 import { sceneGraph } from '../../core/sceneGraph';
-import { ValidateAndUpdateResult, VariableValueOption, VariableValueSingle } from '../types';
+import { ValidateAndUpdateResult, VariableValue, VariableValueOption, VariableValueSingle } from '../types';
 import { MultiValueVariable, MultiValueVariableState, VariableGetOptionsArgs } from '../variants/MultiValueVariable';
 import { from, lastValueFrom, map, mergeMap, Observable, of, take, tap } from 'rxjs';
 import { getDataSource } from '../../utils/getDataSource';
 import { InputActionMeta, MultiSelect, Select } from '@grafana/ui';
-import { isArray } from 'lodash';
+import { isArray, isEqual } from 'lodash';
 import { dataFromResponse, getQueriesForVariables, handleOptionGroups, responseHasError } from '../utils';
 import { OptionWithCheckbox } from '../components/VariableValueSelect';
 import { GroupByVariableUrlSyncHandler } from './GroupByVariableUrlSyncHandler';
 import { getOptionSearcher } from '../components/getOptionSearcher';
 import { getEnrichedFiltersRequest } from '../getEnrichedFiltersRequest';
 import { wrapInSafeSerializableSceneObject } from '../../utils/wrapInSafeSerializableSceneObject';
+import { DefaultGroupByCustomIndicatorContainer } from './DefaultGroupByCustomIndicatorContainer';
 
 export interface GroupByVariableState extends MultiValueVariableState {
   /** Defaults to "Group" */
@@ -27,6 +28,10 @@ export interface GroupByVariableState extends MultiValueVariableState {
   baseFilters?: AdHocVariableFilter[];
   /** Datasource to use for getTagKeys and also controls which scene queries the group by should apply to */
   datasource: DataSourceRef | null;
+  /** Default value set for this groupBy. When this field is set, changing value will allow the user to restore back to this default value */
+  defaultValue?: { text: VariableValue; value: VariableValue };
+  /** Needed for url sync when passing flag to another dashboard */
+  restorable?: boolean;
   /** Controls if the group by can be changed */
   readOnly?: boolean;
   /**
@@ -151,6 +156,61 @@ export class GroupByVariable extends MultiValueVariable<GroupByVariableState> {
         return () => allActiveGroupByVariables.delete(this);
       });
     }
+
+    this.addActivationHandler(this._activationHandler);
+  }
+
+  private _activationHandler = () => {
+    if (this.state.defaultValue) {
+      const partial: Partial<GroupByVariableState> = {};
+      if ((isArray(this.state.value) && !this.state.value.length) || !this.state.value) {
+        partial.value = this.state.defaultValue.value;
+        partial.text = this.state.defaultValue.text;
+        partial.restorable = false;
+      }
+
+      if (this.state.defaultValue && this.checkIfRestorable(this.state.value)) {
+        partial.restorable = true;
+      }
+
+      if (Object.keys(partial).length) {
+        this.setState(partial);
+      }
+    }
+
+    return () => {
+      if (this.state.defaultValue) {
+        this.restoreDefaultValues();
+      }
+    };
+  };
+
+  // This method is related to the defaultValue property. We check if the current value
+  // is different from the default value. If it is, the groupBy will show a button
+  // allowing the user to restore the default values.
+  public checkIfRestorable(values: VariableValue) {
+    const originalValues = isArray(this.state.defaultValue?.value)
+      ? this.state.defaultValue?.value
+      : this.state.defaultValue?.value
+      ? [this.state.defaultValue?.value]
+      : [];
+    const vals = isArray(values) ? values : [values];
+
+    if (vals.length !== originalValues.length) {
+      return true;
+    }
+
+    return !isEqual(vals, originalValues);
+  }
+
+  public restoreDefaultValues() {
+    this.setState({ restorable: false });
+
+    if (!this.state.defaultValue) {
+      return;
+    }
+
+    this.changeValueTo(this.state.defaultValue.value, this.state.defaultValue.text, true);
   }
 
   /**
@@ -219,6 +279,7 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps<GroupByVa
     options,
     includeAll,
     allowCustomValue = true,
+    defaultValue,
   } = model.useState();
 
   const values = useMemo<Array<SelectableValue<VariableValueSingle>>>(() => {
@@ -240,6 +301,8 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps<GroupByVa
 
   const optionSearcher = useMemo(() => getOptionSearcher(options, includeAll), [options, includeAll]);
 
+  const hasDefaultValue = defaultValue !== undefined;
+
   // Detect value changes outside
   useEffect(() => {
     setUncommittedValue(values);
@@ -272,7 +335,7 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps<GroupByVa
       aria-label="Group by selector"
       data-testid={`GroupBySelect-${key}`}
       id={key}
-      placeholder={'Select value'}
+      placeholder={'Group by label'}
       width="auto"
       allowCustomValue={allowCustomValue}
       inputValue={inputValue}
@@ -288,7 +351,14 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps<GroupByVa
       isClearable={true}
       hideSelectedOptions={false}
       isLoading={isFetchingOptions}
-      components={{ Option: OptionWithCheckbox }}
+      components={{
+        Option: OptionWithCheckbox,
+        ...(hasDefaultValue
+          ? {
+              IndicatorsContainer: () => <DefaultGroupByCustomIndicatorContainer model={model} />,
+            }
+          : {}),
+      }}
       onInputChange={onInputChange}
       onBlur={() => {
         model.changeValueTo(
@@ -296,6 +366,12 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps<GroupByVa
           uncommittedValue.map((x) => x.label!),
           true
         );
+
+        const restorable = model.checkIfRestorable(uncommittedValue.map((v) => v.value!));
+
+        if (restorable !== model.state.restorable) {
+          model.setState({ restorable: restorable });
+        }
       }}
       onChange={(newValue, action) => {
         if (action.action === 'clear' && noValueOnClear) {
@@ -318,7 +394,7 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps<GroupByVa
       aria-label="Group by selector"
       data-testid={`GroupBySelect-${key}`}
       id={key}
-      placeholder={'Select value'}
+      placeholder={'Group by label'}
       width="auto"
       inputValue={inputValue}
       value={uncommittedValue && uncommittedValue.length > 0 ? uncommittedValue : null}
diff --git a/packages/scenes/src/variables/groupby/GroupByVariableUrlSyncHandler.ts b/packages/scenes/src/variables/groupby/GroupByVariableUrlSyncHandler.ts
index 230811c86..0bced4c25 100644
--- a/packages/scenes/src/variables/groupby/GroupByVariableUrlSyncHandler.ts
+++ b/packages/scenes/src/variables/groupby/GroupByVariableUrlSyncHandler.ts
@@ -8,6 +8,10 @@ export class GroupByVariableUrlSyncHandler implements SceneObjectUrlSyncHandler
 
   protected _nextChangeShouldAddHistoryStep = false;
 
+  private getRestorableKey(): string {
+    return `restorable-var-${this._sceneObject.state.name}`;
+  }
+
   private getKey(): string {
     return `var-${this._sceneObject.state.name}`;
   }
@@ -17,7 +21,7 @@ export class GroupByVariableUrlSyncHandler implements SceneObjectUrlSyncHandler
       return [];
     }
 
-    return [this.getKey()];
+    return [this.getKey(), ...(this._sceneObject.state.defaultValue ? [this.getRestorableKey()] : [])];
   }
 
   public getUrlState(): SceneObjectUrlValues {
@@ -25,11 +29,19 @@ export class GroupByVariableUrlSyncHandler implements SceneObjectUrlSyncHandler
       return {};
     }
 
-    return { [this.getKey()]: toUrlValues(this._sceneObject.state.value, this._sceneObject.state.text) };
+    return {
+      [this.getKey()]: toUrlValues(this._sceneObject.state.value, this._sceneObject.state.text),
+      ...(this._sceneObject.state.defaultValue
+        ? {
+            [this.getRestorableKey()]: this._sceneObject.state.restorable ? 'true' : undefined,
+          }
+        : {}),
+    };
   }
 
   public updateFromUrl(values: SceneObjectUrlValues): void {
     let urlValue = values[this.getKey()];
+    let restorableValue = values[this.getRestorableKey()];
 
     if (urlValue != null) {
       /**
@@ -42,6 +54,15 @@ export class GroupByVariableUrlSyncHandler implements SceneObjectUrlSyncHandler
 
       const { values, texts } = fromUrlValues(urlValue);
 
+      if (this._sceneObject.state.defaultValue && !restorableValue) {
+        this._sceneObject.changeValueTo(
+          this._sceneObject.state.defaultValue?.value,
+          this._sceneObject.state.defaultValue?.text,
+          false
+        );
+        return;
+      }
+
       this._sceneObject.changeValueTo(values, texts);
     }
   }


Reviews:
user: login: torkelo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-15T14:08:55Z
user: login: mdvictor
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-15T14:50:52Z
user: login: mdvictor
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T07:43:13Z
user: login: bfmatei
body: lgtm
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T12:01:20Z
user: login: mdvictor
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T09:12:26Z
user: login: mdvictor
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T09:12:33Z
user: login: mdvictor
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T09:12:52Z
user: login: mdvictor
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T09:14:41Z

Review comments:
diff_hunk: @@ -154,6 +157,39 @@ export class GroupByVariable extends MultiValueVariable<GroupByVariableState> {
         return () => allActiveGroupByVariables.delete(this);
       });
     }
+
+    this.addActivationHandler(this._activationHandler);
+  }
+
+  private _activationHandler = () => {
+    if (this.state.defaultValues && this.checkIfRestorable(this.state.value)) {
+      this.setState({ restorable: true });
+    }
path: packages/scenes/src/variables/groupby/GroupByVariable.tsx
user: login: torkelo
body: not sure I understand this, looks like restorable is just calculated state (result of current value != default values). 

feels like this could be a simple useMemo function in the rendering of the group by 
created_at: 2025-05-15T14:08:55Z
author_association: COLLABORATOR
diff_hunk: @@ -154,6 +157,39 @@ export class GroupByVariable extends MultiValueVariable<GroupByVariableState> {
         return () => allActiveGroupByVariables.delete(this);
       });
     }
+
+    this.addActivationHandler(this._activationHandler);
+  }
+
+  private _activationHandler = () => {
+    if (this.state.defaultValues && this.checkIfRestorable(this.state.value)) {
+      this.setState({ restorable: true });
+    }
path: packages/scenes/src/variables/groupby/GroupByVariable.tsx
user: login: mdvictor
body: hmm, I think you are right! I'll look into that!
created_at: 2025-05-15T14:50:52Z
author_association: COLLABORATOR
diff_hunk: @@ -154,6 +157,39 @@ export class GroupByVariable extends MultiValueVariable<GroupByVariableState> {
         return () => allActiveGroupByVariables.delete(this);
       });
     }
+
+    this.addActivationHandler(this._activationHandler);
+  }
+
+  private _activationHandler = () => {
+    if (this.state.defaultValues && this.checkIfRestorable(this.state.value)) {
+      this.setState({ restorable: true });
+    }
path: packages/scenes/src/variables/groupby/GroupByVariable.tsx
user: login: mdvictor
body: changed to useMemo, thank you!
created_at: 2025-05-16T07:43:13Z
author_association: COLLABORATOR
diff_hunk: @@ -28,6 +29,7 @@ export interface GroupByVariableState extends MultiValueVariableState {
   baseFilters?: AdHocVariableFilter[];
   /** Datasource to use for getTagKeys and also controls which scene queries the group by should apply to */
   datasource: DataSourceRef | null;
+  defaultValues?: { text: VariableValue; value: VariableValue };
path: packages/scenes/src/variables/groupby/GroupByVariable.tsx
user: login: bfmatei
body: The name of this is a little confusing. We're using plural but expecting a single entity. Also don't forget to add a comment describing what this prop is.
created_at: 2025-05-20T11:40:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -154,6 +156,41 @@ export class GroupByVariable extends MultiValueVariable<GroupByVariableState> {
         return () => allActiveGroupByVariables.delete(this);
       });
     }
+
+    this.addActivationHandler(this._activationHandler);
+  }
+
+  private _activationHandler = () => {
+    if (this.state.defaultValues && ((isArray(this.state.value) && !this.state.value.length) || !this.state.value)) {
+      this.setState({
+        value: this.state.defaultValues.value,
+        text: this.state.defaultValues.text,
+      });
+      return;
+    }
+  };
+
+  public checkIfRestorable(values: VariableValue) {
path: packages/scenes/src/variables/groupby/GroupByVariable.tsx
user: login: bfmatei
body: A comment (explaining what restorable means and why we consider the values restorable if the values are different) for this would be really nice.
created_at: 2025-05-20T11:44:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -154,6 +156,41 @@ export class GroupByVariable extends MultiValueVariable<GroupByVariableState> {
         return () => allActiveGroupByVariables.delete(this);
       });
     }
+
+    this.addActivationHandler(this._activationHandler);
+  }
+
+  private _activationHandler = () => {
+    if (this.state.defaultValues && ((isArray(this.state.value) && !this.state.value.length) || !this.state.value)) {
+      this.setState({
+        value: this.state.defaultValues.value,
+        text: this.state.defaultValues.text,
+      });
+      return;
path: packages/scenes/src/variables/groupby/GroupByVariable.tsx
user: login: bfmatei
body: Nit: return here is not needed.
created_at: 2025-05-20T11:44:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -275,7 +315,7 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps<GroupByVa
       aria-label="Group by selector"
       data-testid={`GroupBySelect-${key}`}
       id={key}
-      placeholder={'Select value'}
+      placeholder={'Group by label'}
path: packages/scenes/src/variables/groupby/GroupByVariable.tsx
user: login: bfmatei
body: Do we want to change the placeholder no matter what?
created_at: 2025-05-20T11:45:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,6 +29,7 @@ export interface GroupByVariableState extends MultiValueVariableState {
   baseFilters?: AdHocVariableFilter[];
   /** Datasource to use for getTagKeys and also controls which scene queries the group by should apply to */
   datasource: DataSourceRef | null;
+  defaultValues?: { text: VariableValue; value: VariableValue };
path: packages/scenes/src/variables/groupby/GroupByVariable.tsx
user: login: mdvictor
body: Agree, have moved to singular naming. Also added a comment explaining the field. Thank you!
created_at: 2025-05-21T09:12:25Z
author_association: COLLABORATOR
diff_hunk: @@ -154,6 +156,41 @@ export class GroupByVariable extends MultiValueVariable<GroupByVariableState> {
         return () => allActiveGroupByVariables.delete(this);
       });
     }
+
+    this.addActivationHandler(this._activationHandler);
+  }
+
+  private _activationHandler = () => {
+    if (this.state.defaultValues && ((isArray(this.state.value) && !this.state.value.length) || !this.state.value)) {
+      this.setState({
+        value: this.state.defaultValues.value,
+        text: this.state.defaultValues.text,
+      });
+      return;
+    }
+  };
+
+  public checkIfRestorable(values: VariableValue) {
path: packages/scenes/src/variables/groupby/GroupByVariable.tsx
user: login: mdvictor
body: Done, thank you!
created_at: 2025-05-21T09:12:33Z
author_association: COLLABORATOR
diff_hunk: @@ -154,6 +156,41 @@ export class GroupByVariable extends MultiValueVariable<GroupByVariableState> {
         return () => allActiveGroupByVariables.delete(this);
       });
     }
+
+    this.addActivationHandler(this._activationHandler);
+  }
+
+  private _activationHandler = () => {
+    if (this.state.defaultValues && ((isArray(this.state.value) && !this.state.value.length) || !this.state.value)) {
+      this.setState({
+        value: this.state.defaultValues.value,
+        text: this.state.defaultValues.text,
+      });
+      return;
path: packages/scenes/src/variables/groupby/GroupByVariable.tsx
user: login: mdvictor
body: Removed return, good eye!
created_at: 2025-05-21T09:12:52Z
author_association: COLLABORATOR
diff_hunk: @@ -275,7 +315,7 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps<GroupByVa
       aria-label="Group by selector"
       data-testid={`GroupBySelect-${key}`}
       id={key}
-      placeholder={'Select value'}
+      placeholder={'Group by label'}
path: packages/scenes/src/variables/groupby/GroupByVariable.tsx
user: login: mdvictor
body: I would say so, this was the result of a design discussion with @Ijin08 where we agreed to make the placeholder more suggestive overall across the groupBy, not necessarily related to the default value.
created_at: 2025-05-21T09:14:41Z
author_association: COLLABORATOR

PR comments:
user: login: mdvictor
created_at: 2025-05-21T15:31:06Z
body: I refactored this a bit as it was failing some specific use-cases. We cannot use default flags on the values as we do not know what values were previously (e.g. if dashboard one has 2 default values and we remove one we still have one default and the next dashboard doesn't know that there were 2 before. It will see the one and think that those are all the previous dashboards default values, thus not setting it as restorable, when it should) 

I've reverted to my initial idea of having a restorable flag in the url for the groupBy, this works much better and accommodates all usecases, although we need the restorable property back in the state to be able to make it work properly with urlSync across dashboard changes and activation/reactivations of the groupBy.

I've tested this in both using the reload dashboard feature flag and without and works properly in both situations.

As a sidenote, we would really benefit from e2e tests here as well alongside the adhoc, will try to find the time to add some soon.
author_association: COLLABORATOR
user: login: scenes-repo-bot-access-token[bot]
created_at: 2025-05-22T13:39:16Z
body: <!-- GITHUB_RELEASE COMMENT: released -->
:rocket: PR was released in [`v6.13.0`](https://github.com/grafana/scenes/releases/tag/v6.13.0) :rocket:
author_association: NONE