The widget layout system uses a comprehensive data format for managing dashboard templates, widget positioning, and responsive layouts. This document details the complete data structure based on the actual implementation in the codebase.
The foundation of the layout system is the Layout type from react-grid-layout (version 1.5.1):
// From react-grid-layout package
interface Layout {
i: string; // Widget identifier (unique key)
x: number; // X position in grid columns
y: number; // Y position in grid rows
w: number; // Width in grid columns
h: number; // Height in grid rows
minW?: number; // Minimum width
maxW?: number; // Maximum width
minH?: number; // Minimum height
maxH?: number; // Maximum height
static?: boolean; // If true, widget cannot be moved/resized
isDraggable?: boolean; // Override draggable behavior
isResizable?: boolean; // Override resizable behavior
isBounded?: boolean; // If true, widget cannot be moved outside the grid
}The system extends the base Layout with a title property:
// From src/api/dashboard-templates.ts
export type LayoutWithTitle = Layout & {
title: string;
};The frontend tracks additional metadata not stored in the backend:
// From src/api/dashboard-templates.ts
export type ExtendedLayoutItem = LayoutWithTitle & {
widgetType: string; // Type of widget (e.g., 'complianceScoreCard')
config?: WidgetConfiguration; // Widget-specific configuration
locked?: boolean; // If true, widget cannot be moved/resized
};Widget identifiers follow a specific pattern:
// From src/api/dashboard-templates.ts
export const widgetIdSeparator = '#';
// Format: "widgetType#uniqueId"
// Example: "complianceScoreCard#550e8400-e29b-41d4-a716-446655440000"
export const getWidgetIdentifier = (widgetType: string, uniqueId: string = crypto.randomUUID()) => {
return `${widgetType}${widgetIdSeparator}${uniqueId}`;
};The system uses four responsive breakpoints:
// From src/Components/DnDLayout/GridLayout.tsx
export const breakpoints: {
[key in Variants]: number;
} = {
xl: 1550, // Extra large screens
lg: 1400, // Large screens
md: 1100, // Medium screens
sm: 800 // Small screens
};
// From src/api/dashboard-templates.ts
export type Variants = 'sm' | 'md' | 'lg' | 'xl';// From src/consts.ts
export const columns = {
xl: 4, // 4 columns on extra large screens
lg: 3, // 3 columns on large screens
md: 2, // 2 columns on medium screens
sm: 1 // 1 column on small screens
};// Fixed row height across all breakpoints
const rowHeight = 56; // pixelsThe backend stores layout configurations for all responsive breakpoints:
// From src/api/dashboard-templates.ts
export type TemplateConfig = {
[k in Variants]: LayoutWithTitle[];
};
// Example structure:
const exampleTemplateConfig: TemplateConfig = {
sm: [
{ i: "widget1#uuid", x: 0, y: 0, w: 1, h: 3, title: "System Status" },
{ i: "widget2#uuid", x: 0, y: 3, w: 1, h: 2, title: "Compliance Score" }
],
md: [
{ i: "widget1#uuid", x: 0, y: 0, w: 1, h: 3, title: "System Status" },
{ i: "widget2#uuid", x: 1, y: 0, w: 1, h: 2, title: "Compliance Score" }
],
lg: [
{ i: "widget1#uuid", x: 0, y: 0, w: 2, h: 3, title: "System Status" },
{ i: "widget2#uuid", x: 2, y: 0, w: 1, h: 2, title: "Compliance Score" }
],
xl: [
{ i: "widget1#uuid", x: 0, y: 0, w: 2, h: 3, title: "System Status" },
{ i: "widget2#uuid", x: 2, y: 0, w: 1, h: 2, title: "Compliance Score" }
]
};The frontend extends the template configuration with additional metadata:
// From src/api/dashboard-templates.ts
export type ExtendedTemplateConfig = {
[k in Variants]: ExtendedLayoutItem[];
};
// Example with extended properties:
const exampleExtendedConfig: ExtendedTemplateConfig = {
sm: [
{
i: "complianceScoreCard#uuid",
x: 0, y: 0, w: 1, h: 3,
title: "Compliance Score",
widgetType: "complianceScoreCard",
config: {
icon: "SecurityIcon",
title: "Compliance Score",
headerLink: { title: "View Details", href: "/compliance" }
},
locked: false
}
],
// ... other breakpoints
};Base template structure returned by the backend:
// From src/api/dashboard-templates.ts
export type BaseTemplate = {
name: string; // Template identifier
displayName: string; // Human-readable name
templateConfig: TemplateConfig; // Layout configuration
};
// Example API response:
{
"data": {
"name": "landingPage",
"displayName": "Landing Page",
"templateConfig": {
"sm": [...],
"md": [...],
"lg": [...],
"xl": [...]
}
}
}User-specific template with metadata:
// From src/api/dashboard-templates.ts
export type DashboardTemplate = {
id: number; // Template ID
createdAt: string; // ISO timestamp
updatedAt: string; // ISO timestamp
deletedAt: string | null; // ISO timestamp or null
userIdentityID: number; // User identifier
default: boolean; // If true, this is the user's default template
TemplateBase: {
name: string; // Template type (e.g., "landingPage")
displayName: string; // Human-readable name
};
templateConfig: TemplateConfig; // Layout configuration
};
// Example API response:
{
"data": [
{
"id": 123,
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T14:45:00Z",
"deletedAt": null,
"userIdentityID": 456,
"default": true,
"TemplateBase": {
"name": "landingPage",
"displayName": "Landing Page"
},
"templateConfig": {
"sm": [...],
"md": [...],
"lg": [...],
"xl": [...]
}
}
]
}Default sizing constraints for widgets:
// From src/api/dashboard-templates.ts
export type WidgetDefaults = {
w: number; // Default width in grid columns
h: number; // Default height in grid rows
maxH: number; // Maximum height
minH: number; // Minimum height
};
// Example widget defaults:
{
"smallWidget": {
"defaults": { "w": 1, "h": 2, "maxH": 4, "minH": 2 }
},
"mediumWidget": {
"defaults": { "w": 2, "h": 3, "maxH": 6, "minH": 2 }
},
"largeWidget": {
"defaults": { "w": 3, "h": 4, "maxH": 8, "minH": 3 }
}
}Widget-specific configuration and metadata:
// From src/api/dashboard-templates.ts
export type WidgetConfiguration = {
icon?: string; // Icon identifier
headerLink?: WidgetHeaderLink; // Header link configuration
title?: string; // Widget title
permissions?: WidgetPermission[]; // Permission requirements
};
export type WidgetHeaderLink = {
title?: string; // Link text
href?: string; // Link URL
};
export type WidgetPermission = {
method: keyof VisibilityFunctions; // Permission check method
args?: unknown[]; // Arguments for permission check
};Complete widget registry with federated module information:
// From src/api/dashboard-templates.ts
export type WidgetMapping = {
[key: string]: Pick<ScalprumComponentProps, 'scope' | 'module' | 'importName'> & {
defaults: WidgetDefaults;
config?: WidgetConfiguration;
};
};
// Example widget mapping:
{
"complianceScoreCard": {
"scope": "compliance",
"module": "./ComplianceScoreCard",
"importName": "ComplianceScoreCard",
"defaults": {
"w": 2,
"h": 3,
"maxH": 6,
"minH": 2
},
"config": {
"icon": "SecurityIcon",
"title": "Compliance Score",
"headerLink": {
"title": "View Details",
"href": "/compliance"
},
"permissions": [
{
"method": "hasPermissions",
"args": [["compliance:*:read"]]
}
]
}
}
}GET /api/chrome-service/v1/dashboard-templates/base-template
# Returns: BaseTemplate[]
GET /api/chrome-service/v1/dashboard-templates/base-template?dashboard=landingPage
# Returns: BaseTemplateGET /api/chrome-service/v1/dashboard-templates
# Returns: DashboardTemplate[]
GET /api/chrome-service/v1/dashboard-templates?dashboard=landingPage
# Returns: DashboardTemplate[]GET /api/chrome-service/v1/dashboard-templates/widget-mapping
# Returns: WidgetMapping# Update template configuration
PATCH /api/chrome-service/v1/dashboard-templates/{templateId}
Content-Type: application/json
{
"templateConfig": {
"sm": [...],
"md": [...],
"lg": [...],
"xl": [...]
}
}
# Reset template to base configuration
POST /api/chrome-service/v1/dashboard-templates/{templateId}/reset
# Delete template
DELETE /api/chrome-service/v1/dashboard-templates/{templateId}- User Authentication: System waits for
currentUserto be available - Template Fetch: Calls
getDashboardTemplates(layoutType)to get user templates - Default Selection: Uses
getDefaultTemplate()to find the template withdefault: true - Data Transformation: Converts
TemplateConfigtoExtendedTemplateConfigusingmapTemplateConfigToExtendedTemplateConfig() - Responsive Setup: Determines initial layout variant based on screen width
Layout changes are automatically persisted with a 1.5-second debounce:
// From src/Components/DnDLayout/GridLayout.tsx
const debouncedPatchDashboardTemplate = DebouncePromise(patchDashboardTemplate, 1500, {
onlyResolvesLast: true,
});
const onLayoutChange = async (currentLayout: Layout[]) => {
// Skip if initial render or layout is locked
if (isInitialRender || isLayoutLocked || templateId < 0) {
return;
}
// Transform layout data
const data = extendLayout({ ...template, [layoutVariant]: currentLayout });
// Persist changes
try {
await debouncedPatchDashboardTemplate(templateId, { templateConfig: data });
} catch (error) {
// Handle error with notification
}
};When a widget is dropped onto the grid:
- Widget Identification: Extract widget type from
event.dataTransfer.getData('text') - Widget Validation: Check if widget type exists in
widgetMapping - ID Generation: Create unique identifier using
getWidgetIdentifier() - Multi-breakpoint Update: Update layout for all responsive breakpoints
- Constraint Application: Apply widget defaults and column constraints
- Automatic Positioning: Push existing widgets down if needed
- Analytics Tracking: Track widget addition event
Widgets are automatically constrained to valid column ranges:
// From src/Components/DnDLayout/GridLayout.tsx
const newWidget = {
// Ensure width doesn't exceed columns for each breakpoint
w: size === layoutVariant ? layoutItem.w : Math.min(widgetMapping[data].defaults.w, columns[size as Variants]),
// Ensure X position doesn't exceed grid bounds
x: size === layoutVariant ? layoutItem.x : Math.min(layoutItem.x, columns[size as Variants]),
// ... other properties
};- Vertical Compaction: Enabled via
verticalCompactprop - CSS Transforms: Enabled via
useCSSTransformsfor better performance - Draggable Handle: Restricted to
.drag-handleclass elements - Resize Handles: Available on all corners (
['sw', 'nw', 'se', 'ne'])
{
"dashboardTemplate": {
"id": 123,
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T14:45:00Z",
"deletedAt": null,
"userIdentityID": 456,
"default": true,
"TemplateBase": {
"name": "landingPage",
"displayName": "Landing Page"
},
"templateConfig": {
"sm": [
{
"i": "complianceScoreCard#550e8400-e29b-41d4-a716-446655440000",
"x": 0,
"y": 0,
"w": 1,
"h": 3,
"title": "Compliance Score"
}
],
"md": [
{
"i": "complianceScoreCard#550e8400-e29b-41d4-a716-446655440000",
"x": 0,
"y": 0,
"w": 2,
"h": 3,
"title": "Compliance Score"
}
],
"lg": [
{
"i": "complianceScoreCard#550e8400-e29b-41d4-a716-446655440000",
"x": 0,
"y": 0,
"w": 2,
"h": 3,
"title": "Compliance Score"
}
],
"xl": [
{
"i": "complianceScoreCard#550e8400-e29b-41d4-a716-446655440000",
"x": 0,
"y": 0,
"w": 2,
"h": 3,
"title": "Compliance Score"
}
]
}
},
"widgetMapping": {
"complianceScoreCard": {
"scope": "compliance",
"module": "./ComplianceScoreCard",
"importName": "ComplianceScoreCard",
"defaults": {
"w": 2,
"h": 3,
"maxH": 6,
"minH": 2
},
"config": {
"icon": "SecurityIcon",
"title": "Compliance Score",
"headerLink": {
"title": "View Details",
"href": "/compliance"
},
"permissions": [
{
"method": "hasPermissions",
"args": [["compliance:*:read"]]
}
]
}
}
}
}// Complete type hierarchy
export type LayoutTypes = 'landingPage';
export type Variants = 'sm' | 'md' | 'lg' | 'xl';
export type LayoutWithTitle = Layout & { title: string };
export type ExtendedLayoutItem = LayoutWithTitle & {
widgetType: string;
config?: WidgetConfiguration;
locked?: boolean;
};
export type TemplateConfig = {
[k in Variants]: LayoutWithTitle[];
};
export type ExtendedTemplateConfig = {
[k in Variants]: ExtendedLayoutItem[];
};
export type DashboardTemplate = {
id: number;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
userIdentityID: number;
default: boolean;
TemplateBase: {
name: string;
displayName: string;
};
templateConfig: TemplateConfig;
};
export type WidgetMapping = {
[key: string]: Pick<ScalprumComponentProps, 'scope' | 'module' | 'importName'> & {
defaults: WidgetDefaults;
config?: WidgetConfiguration;
};
};This comprehensive data format enables responsive, persistent, and extensible widget layouts while maintaining a clean separation between frontend presentation logic and backend data storage.