Skip to content

Row grouping & aggregation ​

Rows can be grouped by one or more columns. Group rows can be expanded and collapsed and show aggregated values of their rows.

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <button @click="groups = ['country']">Group by country</button>
      <button @click="groups = ['country', 'department']">Country › Department</button>
      <button @click="groups = []">No grouping</button>
    </div>
    <Datagrid
      :columns="columns"
      :rows="rows"
      :options="options"
      v-model:columnGroups="groups"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";

const rows = createEmployees(1000);
const groups = ref(["country", "department"]);

const currency = formatters.currency("EUR", { maximumFractionDigits: 0 });

const columns: Array<ColumnConfig<Employee>> = [
  { field: "country", text: "Country", groupable: true },
  { field: "department", text: "Department", groupable: true },
  { field: "role", text: "Role", groupable: true },
  { field: "lastName", text: "Name" },
  { field: "salary", text: "Salary (sum)", type: "number", aggFunc: "sum", valueFormatter: currency },
  { field: "bonus", text: "Bonus (avg)", type: "number", aggFunc: "avg", valueFormatter: currency },
  { field: "rating", text: "Rating (max)", type: "number", aggFunc: "max" },
  {
    field: "remote",
    text: "Remote (%)",
    type: "number",
    // custom aggregation function
    aggFunc: (values) => values.filter(Boolean).length / Math.max(1, values.length),
    valueFormatter: ({ value }) =>
      typeof value === "number" ? `${Math.round(value * 100)} %` : value ? "yes" : "no",
  },
];

const options: GridOptions<Employee> = {
  groupDefaultExpanded: 1,
  totalRow: "bottom",
  columnMenu: true,
  defaultColumn: { sortable: true, resizeable: true, width: 150 },
};
</script>
tsx
import { useState } from "react";
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";

const rows = createEmployees(1000);

const currency = formatters.currency("EUR", { maximumFractionDigits: 0 });

const columns: Array<ColumnConfig<Employee>> = [
  { field: "country", text: "Country", groupable: true },
  { field: "department", text: "Department", groupable: true },
  { field: "role", text: "Role", groupable: true },
  { field: "lastName", text: "Name" },
  { field: "salary", text: "Salary (sum)", type: "number", aggFunc: "sum", valueFormatter: currency },
  { field: "bonus", text: "Bonus (avg)", type: "number", aggFunc: "avg", valueFormatter: currency },
  { field: "rating", text: "Rating (max)", type: "number", aggFunc: "max" },
  {
    field: "remote",
    text: "Remote (%)",
    type: "number",
    // custom aggregation function
    aggFunc: (values) => values.filter(Boolean).length / Math.max(1, values.length),
    valueFormatter: ({ value }) =>
      typeof value === "number" ? `${Math.round(value * 100)} %` : value ? "yes" : "no",
  },
];

const options: GridOptions<Employee> = {
  groupDefaultExpanded: 1,
  totalRow: "bottom",
  columnMenu: true,
  defaultColumn: { sortable: true, resizeable: true, width: 150 },
};

export default function Grouping() {
  const [groups, setGroups] = useState(["country", "department"]);

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <button onClick={() => setGroups(["country"])}>Group by country</button>
        <button onClick={() => setGroups(["country", "department"])}>Country › Department</button>
        <button onClick={() => setGroups([])}>No grouping</button>
      </div>
      <Datagrid
        columns={columns}
        rows={rows}
        options={options}
        columnGroups={groups}
        onColumnGroupsChange={setGroups}
      />
    </div>
  );
}

Grouping rows ​

Columns with groupable: true can be dragged into the group area above the header (the area is shown as soon as one column is groupable). The column menu offers "Group by this column" as well.

  • Click a group chip to toggle the sort direction of the groups.
  • Drag a chip to change the order of the grouping levels.
  • Drag a chip out of the group area (or click ×) to remove the grouping.

Programmatically:

vue
<Datagrid v-model:columnGroups="groups" ... />
tsx
<Datagrid columnGroups={groups} onColumnGroupsChange={setGroups} ... />
ts

api.setRowGrouping(["country"]);
api.addRowGrouping("department");
api.deleteRowGrouping("country");
api.moveRowGrouping("department", 0);
api.toggleRowGroupingSort("country");
api.clearRowGrouping();

Groups are sorted ascending by their key. Empty values are grouped in a group displayed as "–".

Letting users group ​

Leave the grouping empty and mark the columns users may group by as groupable. This is the typical setup for ad-hoc analysis: users build their own report by dragging headers into the group area. The bound columnGroups model shows the current grouping.

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <span>Drag a header (Country, Department, Role, City) into the area above the header.</span>
      <button @click="groups = []">Clear grouping</button>
    </div>
    <Datagrid :columns="columns" :rows="rows" :options="options" v-model:columnGroups="groups" />
    <code class="demo-log">columnGroups: {{ JSON.stringify(groups) }}</code>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";

const rows = createEmployees(500);
// no initial grouping: the user decides
const groups = ref<Array<string>>([]);

const columns: Array<ColumnConfig<Employee>> = [
  // only groupable columns can be dropped into the group area
  { field: "country", text: "Country", groupable: true },
  { field: "department", text: "Department", groupable: true },
  { field: "role", text: "Role", groupable: true },
  { field: "city", text: "City", groupable: true },
  { field: "lastName", text: "Name" },
  { field: "salary", text: "Salary", type: "number", aggFunc: "sum" },
];

const options: GridOptions<Employee> = {
  columnMenu: true, // the column menu offers "Group by this column" too
  defaultColumn: { sortable: true, moveable: true, flex: 1 },
};
</script>
tsx
import { useState } from "react";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";

const rows = createEmployees(500);

const columns: Array<ColumnConfig<Employee>> = [
  // only groupable columns can be dropped into the group area
  { field: "country", text: "Country", groupable: true },
  { field: "department", text: "Department", groupable: true },
  { field: "role", text: "Role", groupable: true },
  { field: "city", text: "City", groupable: true },
  { field: "lastName", text: "Name" },
  { field: "salary", text: "Salary", type: "number", aggFunc: "sum" },
];

const options: GridOptions<Employee> = {
  columnMenu: true, // the column menu offers "Group by this column" too
  defaultColumn: { sortable: true, moveable: true, flex: 1 },
};

export default function GroupingDragDrop() {
  // no initial grouping: the user decides
  const [groups, setGroups] = useState<Array<string>>([]);

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <span>Drag a header (Country, Department, Role, City) into the area above the header.</span>
        <button onClick={() => setGroups([])}>Clear grouping</button>
      </div>
      <Datagrid
        columns={columns}
        rows={rows}
        options={options}
        columnGroups={groups}
        onColumnGroupsChange={setGroups}
      />
      <code className="demo-log">columnGroups: {JSON.stringify(groups)}</code>
    </div>
  );
}

Expanding and collapsing ​

ts
groupDefaultExpanded: 1  // grid option: number of expanded levels for new groups, -1 = all
ts
api.expandAll();
api.collapseAll();
api.setExpandedKeys(keys);   // or v-model:expandedItems

Group rows are toggled by clicking them. The expanded state of a group is kept when the data changes.

Choose 0 for a compact overview of many groups, 1 to show the next level of nested groups and -1 when users mainly read the rows. groupShowCount: false hides the number of rows behind the group key. groupDefaultExpanded only applies to groups which are created, so the demo recreates the grid when it changes.

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <label>
        groupDefaultExpanded
        <select v-model.number="expanded">
          <option :value="0">0 – all collapsed</option>
          <option :value="1">1 – first level expanded</option>
          <option :value="-1">-1 – all expanded</option>
        </select>
      </label>
      <label><input v-model="showCount" type="checkbox" /> groupShowCount</label>
      <button @click="grid?.expandAll()">Expand all</button>
      <button @click="grid?.collapseAll()">Collapse all</button>
    </div>
    <!-- groupDefaultExpanded applies to new groups, the key recreates the grid -->
    <Datagrid
      ref="grid"
      :key="expanded"
      :columns="columns"
      :rows="rows"
      :options="options"
      :columnGroups="groups"
    />
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createTasks, type Task } from "./data";

const rows = createTasks(150);
const groups = ["project", "status"];
const grid = ref<InstanceType<typeof Datagrid>>();
const expanded = ref(1);
const showCount = ref(true);

const columns: Array<ColumnConfig<Task>> = [
  { field: "project", text: "Project", groupable: true },
  { field: "status", text: "Status", groupable: true },
  { field: "title", text: "Task" },
  { field: "assignee", text: "Assignee" },
  { field: "estimate", text: "Estimate (h)", type: "number", aggFunc: "sum" },
];

const options = computed<GridOptions<Task>>(() => ({
  groupDefaultExpanded: expanded.value,
  groupShowCount: showCount.value,
  defaultColumn: { flex: 1 },
}));
</script>
tsx
import { useMemo, useRef, useState } from "react";
import { Datagrid, type ColumnConfig, type DatagridHandle, type GridOptions } from "@datagrid/react-ui";
import { createTasks, type Task } from "../data";

const rows = createTasks(150);
const groups = ["project", "status"];

const columns: Array<ColumnConfig<Task>> = [
  { field: "project", text: "Project", groupable: true },
  { field: "status", text: "Status", groupable: true },
  { field: "title", text: "Task" },
  { field: "assignee", text: "Assignee" },
  { field: "estimate", text: "Estimate (h)", type: "number", aggFunc: "sum" },
];

export default function GroupingExpanded() {
  const grid = useRef<DatagridHandle>(null);
  const [expanded, setExpanded] = useState(1);
  const [showCount, setShowCount] = useState(true);

  const options = useMemo<GridOptions<Task>>(
    () => ({
      groupDefaultExpanded: expanded,
      groupShowCount: showCount,
      defaultColumn: { flex: 1 },
    }),
    [expanded, showCount]
  );

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <label>
          groupDefaultExpanded{" "}
          <select value={expanded} onChange={(e) => setExpanded(Number(e.target.value))}>
            <option value={0}>0 – all collapsed</option>
            <option value={1}>1 – first level expanded</option>
            <option value={-1}>-1 – all expanded</option>
          </select>
        </label>
        <label>
          <input type="checkbox" checked={showCount} onChange={(e) => setShowCount(e.target.checked)} /> groupShowCount
        </label>
        <button onClick={() => grid.current?.expandAll()}>Expand all</button>
        <button onClick={() => grid.current?.collapseAll()}>Collapse all</button>
      </div>
      {/* groupDefaultExpanded applies to new groups, the key recreates the grid */}
      <Datagrid
        ref={grid}
        key={expanded}
        columns={columns}
        rows={rows}
        options={options}
        columnGroups={groups}
      />
    </div>
  );
}

Aggregation ​

Columns with an aggFunc show aggregated values in group rows and in the total row.

aggFuncResult
"sum"sum of all numbers
"avg"average of all numbers
"min", "max"smallest / largest value (numbers, dates, strings)
"count"number of rows
"first", "last"value of the first / last row
function(values, rows) => result

The aggregated value is formatted with the valueFormatter of the column.

The demo groups tasks by project and uses one aggregation per column. Progress uses a custom function which receives the values and the rows, so it can weight the progress by the estimate of each task. The same column can be added twice with different ids (here the due date with min and max). Switch the position of the total row with the dropdown.

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <label>
        Total row
        <select v-model="totalRow">
          <option value="bottom">bottom</option>
          <option value="top">top</option>
          <option value="none">none</option>
        </select>
      </label>
    </div>
    <Datagrid :columns="columns" :rows="rows" :options="options" :columnGroups="groups" />
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createTasks, type Task } from "./data";

const rows = createTasks(120);
const groups = ["project"];
const totalRow = ref<"bottom" | "top" | "none">("bottom");

const hours = ({ value }: { value: unknown }) => (typeof value === "number" ? `${Math.round(value)} h` : "");

const columns: Array<ColumnConfig<Task>> = [
  { field: "project", text: "Project", groupable: true, visible: false },
  { field: "title", text: "Task", width: 170 },
  { id: "tasks", field: "id", text: "Tasks (count)", type: "number", aggFunc: "count", width: 120 },
  { field: "estimate", text: "Estimate (sum)", type: "number", aggFunc: "sum", valueFormatter: hours },
  { field: "spent", text: "Spent (avg)", type: "number", aggFunc: "avg", valueFormatter: hours },
  {
    field: "progress",
    text: "Progress (weighted)",
    type: "number",
    width: 150,
    // custom aggregation: average progress weighted by the estimate of each task
    aggFunc: (values, rows) => {
      const total = rows.reduce((sum, row) => sum + row.data.estimate, 0);
      const done = rows.reduce((sum, row) => sum + row.data.progress * row.data.estimate, 0);
      return total ? done / total : 0;
    },
    valueFormatter: formatters.percent({ maximumFractionDigits: 0 }),
  },
  { field: "due", text: "First due (min)", type: "date", aggFunc: "min", valueFormatter: formatters.date() },
  { id: "lastDue", field: "due", text: "Last due (max)", type: "date", aggFunc: "max", valueFormatter: formatters.date() },
];

const options = computed<GridOptions<Task>>(() => ({
  totalRow: totalRow.value === "none" ? undefined : totalRow.value,
  groupDefaultExpanded: 0,
  defaultColumn: { sortable: true, width: 125 },
}));
</script>
tsx
import { useMemo, useState } from "react";
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createTasks, type Task } from "../data";

const rows = createTasks(120);
const groups = ["project"];

const hours = ({ value }: { value: unknown }) => (typeof value === "number" ? `${Math.round(value)} h` : "");

const columns: Array<ColumnConfig<Task>> = [
  { field: "project", text: "Project", groupable: true, visible: false },
  { field: "title", text: "Task", width: 170 },
  { id: "tasks", field: "id", text: "Tasks (count)", type: "number", aggFunc: "count", width: 120 },
  { field: "estimate", text: "Estimate (sum)", type: "number", aggFunc: "sum", valueFormatter: hours },
  { field: "spent", text: "Spent (avg)", type: "number", aggFunc: "avg", valueFormatter: hours },
  {
    field: "progress",
    text: "Progress (weighted)",
    type: "number",
    width: 150,
    // custom aggregation: average progress weighted by the estimate of each task
    aggFunc: (values, rows) => {
      const total = rows.reduce((sum, row) => sum + row.data.estimate, 0);
      const done = rows.reduce((sum, row) => sum + row.data.progress * row.data.estimate, 0);
      return total ? done / total : 0;
    },
    valueFormatter: formatters.percent({ maximumFractionDigits: 0 }),
  },
  { field: "due", text: "First due (min)", type: "date", aggFunc: "min", valueFormatter: formatters.date() },
  { id: "lastDue", field: "due", text: "Last due (max)", type: "date", aggFunc: "max", valueFormatter: formatters.date() },
];

export default function GroupingAggregation() {
  const [totalRow, setTotalRow] = useState<"bottom" | "top" | "none">("bottom");

  const options = useMemo<GridOptions<Task>>(
    () => ({
      totalRow: totalRow === "none" ? undefined : totalRow,
      groupDefaultExpanded: 0,
      defaultColumn: { sortable: true, width: 125 },
    }),
    [totalRow]
  );

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <label>
          Total row{" "}
          <select value={totalRow} onChange={(e) => setTotalRow(e.target.value as "bottom" | "top" | "none")}>
            <option value="bottom">bottom</option>
            <option value="top">top</option>
            <option value="none">none</option>
          </select>
        </label>
      </div>
      <Datagrid columns={columns} rows={rows} options={options} columnGroups={groups} />
    </div>
  );
}
ts
{
  field: "remote",
  aggFunc: (values) => values.filter(Boolean).length / values.length,
  valueFormatter: ({ value }) => `${Math.round(value * 100)} %`,
}

Groups can be sorted by aggregated values: sorting an aggregated column sorts the groups by their aggregated value.

Total row ​

totalRow: "bottom" or "top" adds a pinned row with the aggregations over all filtered rows.

Group footers ​

groupFooter: true adds a footer row below the rows of each expanded group, like the subtotals of a report. It shows the same aggregated values as the group row and the label "Total" with the group key (translation key group_total). A collapsed group has no footer, its group row shows the aggregations.

Footer rows scroll with the rows, they are not part of the data (api.getData()) and are not selectable or editable. With nested grouping every level gets its own footer, so the last rows of the demo below show the department subtotal followed by the country total.

Sticky group rows ​

With stickyGroupRows: true the group rows of the rows at the top of the viewport stay visible below the header while scrolling – one row per grouping level. You always see which group the visible rows belong to. Clicking a sticky group row collapses the group and scrolls it into view.

Source
vue
<template>
  <Datagrid :columns="columns" :rows="rows" :options="options" v-model:columnGroups="groups" />
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";

const rows = createEmployees(300);
const groups = ref(["country", "department"]);

const currency = formatters.currency("EUR", { maximumFractionDigits: 0 });

const columns: Array<ColumnConfig<Employee>> = [
  { field: "country", text: "Country", groupable: true, visible: false },
  { field: "department", text: "Department", groupable: true, visible: false },
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "salary", text: "Salary", type: "number", aggFunc: "sum", valueFormatter: currency },
  { field: "bonus", text: "Bonus", type: "number", aggFunc: "sum", valueFormatter: currency },
];

const options: GridOptions<Employee> = {
  groupDefaultExpanded: -1, // expand all levels
  groupFooter: true, // sums below each group
  stickyGroupRows: true, // group rows stay visible while scrolling
  defaultColumn: { flex: 1 },
};
</script>
tsx
import { useState } from "react";
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";

const rows = createEmployees(300);

const currency = formatters.currency("EUR", { maximumFractionDigits: 0 });

const columns: Array<ColumnConfig<Employee>> = [
  { field: "country", text: "Country", groupable: true, visible: false },
  { field: "department", text: "Department", groupable: true, visible: false },
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "salary", text: "Salary", type: "number", aggFunc: "sum", valueFormatter: currency },
  { field: "bonus", text: "Bonus", type: "number", aggFunc: "sum", valueFormatter: currency },
];

const options: GridOptions<Employee> = {
  groupDefaultExpanded: -1, // expand all levels
  groupFooter: true, // sums below each group
  stickyGroupRows: true, // group rows stay visible while scrolling
  defaultColumn: { flex: 1 },
};

export default function GroupFooterSticky() {
  const [groups, setGroups] = useState(["country", "department"]);

  return (
    <Datagrid columns={columns} rows={rows} options={options} columnGroups={groups} onColumnGroupsChange={setGroups} />
  );
}
OptionDefaultDescription
groupFooterfalsefooter row with the aggregations below each expanded group
stickyGroupRowsfalsegroup rows of the top visible rows stay at the top of the grid

Customizing group rows ​

The group slot replaces the content of group rows:

vue
<Datagrid ...>
  <template #group="{ group, key, text, count, column }">
    <strong>{{ text }}</strong> – {{ count }} employees
  </template>
</Datagrid>

renderGroup replaces the content of group rows:

tsx
<Datagrid
  renderGroup={({ group, key, text, count, column }) => (
    <>
      <strong>{text}</strong> – {count} employees
    </>
  )}
  ...
/>

Options: groupShowCount: false hides the number of rows.

Use a custom group row when the group key needs more context than a text, e.g. a status badge and a summary of the rows. group.rows contains all data rows of the group (including nested groups), so any summary can be calculated. For values which are shown in the columns, prefer aggFunc.

Source
vue
<template>
  <Datagrid :columns="columns" :rows="rows" :options="options" :columnGroups="groups">
    <!-- content of the group rows -->
    <template #group="{ group, key, count }">
      <span class="gcr-badge" :class="`gcr-badge--${String(key).replace(' ', '-')}`">{{ key }}</span>
      <span class="gcr-info">{{ count }} tasks · {{ summary(group).hours }} h estimated</span>
      <span v-if="summary(group).overdue" class="gcr-overdue">{{ summary(group).overdue }} overdue</span>
    </template>
  </Datagrid>
</template>

<script setup lang="ts">
import { Datagrid, formatters, type ColumnConfig, type GridOptions, type Group } from "@datagrid/vue-ui";
import { createTasks, TODAY, type Task } from "./data";

const rows = createTasks(80);
const groups = ["status"];

// group.rows contains all data rows of the group
function summary(group: Group) {
  const tasks = group.rows.map((row) => row.data as Task);
  return {
    hours: tasks.reduce((sum, task) => sum + task.estimate, 0),
    overdue: tasks.filter((task) => !task.done && task.due < TODAY).length,
  };
}

const columns: Array<ColumnConfig<Task>> = [
  { field: "status", text: "Status", groupable: true, visible: false },
  { field: "title", text: "Task", flex: 2 },
  { field: "assignee", text: "Assignee", flex: 1 },
  { field: "estimate", text: "Estimate (h)", type: "number", width: 120 },
  { field: "due", text: "Due", type: "date", valueFormatter: formatters.date(), width: 140 },
];

const options: GridOptions<Task> = {
  groupDefaultExpanded: 1,
};
</script>

<style>
.gcr-badge {
  padding: 1px 8px;
  border-radius: 10px;
  font-size: 12px;
  font-weight: 600;
  text-transform: capitalize;
  color: white;
  background: #6e7781;
}
.gcr-badge--in-progress {
  background: #0969da;
}
.gcr-badge--review {
  background: #8250df;
}
.gcr-badge--done {
  background: #1a7f37;
}
.gcr-info {
  margin-left: 8px;
  color: var(--ut-muted-color);
}
.gcr-overdue {
  margin-left: 8px;
  color: #cf222e;
  font-weight: 600;
}
</style>
tsx
import {
  Datagrid,
  formatters,
  type ColumnConfig,
  type DatagridProps,
  type GridOptions,
  type Group,
} from "@datagrid/react-ui";
import { createTasks, TODAY, type Task } from "../data";

const rows = createTasks(80);
const groups = ["status"];

// group.rows contains all data rows of the group
function summary(group: Group) {
  const tasks = group.rows.map((row) => row.data as Task);
  return {
    hours: tasks.reduce((sum, task) => sum + task.estimate, 0),
    overdue: tasks.filter((task) => !task.done && task.due < TODAY).length,
  };
}

const columns: Array<ColumnConfig<Task>> = [
  { field: "status", text: "Status", groupable: true, visible: false },
  { field: "title", text: "Task", flex: 2 },
  { field: "assignee", text: "Assignee", flex: 1 },
  { field: "estimate", text: "Estimate (h)", type: "number", width: 120 },
  { field: "due", text: "Due", type: "date", valueFormatter: formatters.date(), width: 140 },
];

const options: GridOptions<Task> = {
  groupDefaultExpanded: 1,
};

// content of the group rows
const renderGroup: DatagridProps<Task>["renderGroup"] = ({ group, key, count }) => {
  const { hours, overdue } = summary(group);
  return (
    <>
      <span className={`gcr-badge gcr-badge--${String(key).replace(" ", "-")}`}>{String(key)}</span>
      <span className="gcr-info">
        {count} tasks · {hours} h estimated
      </span>
      {overdue > 0 && <span className="gcr-overdue">{overdue} overdue</span>}
    </>
  );
};

export default function GroupingCustomRow() {
  return (
    <>
      <Datagrid columns={columns} rows={rows} options={options} columnGroups={groups} renderGroup={renderGroup} />
      <style>{`
        .gcr-badge {
          padding: 1px 8px;
          border-radius: 10px;
          font-size: 12px;
          font-weight: 600;
          text-transform: capitalize;
          color: white;
          background: #6e7781;
        }
        .gcr-badge--in-progress {
          background: #0969da;
        }
        .gcr-badge--review {
          background: #8250df;
        }
        .gcr-badge--done {
          background: #1a7f37;
        }
        .gcr-info {
          margin-left: 8px;
          color: var(--ut-muted-color);
        }
        .gcr-overdue {
          margin-left: 8px;
          color: #cf222e;
          font-weight: 600;
        }
      `}</style>
    </>
  );
}

Keyboard ​

Group rows can be focused with ↑ / ↓. Enter or Space toggles a group, → expands and ← collapses it (or moves to the parent group).

Released under the ISC License.