Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5a988cb
feat: Add chart color tokens and getCSSVar utility
talissoncosta Apr 10, 2026
8ae1fe2
feat: Add ColorSwatch component
talissoncosta Apr 10, 2026
686b18b
feat: Add reusable BarChart and ChartTooltip components
talissoncosta Apr 10, 2026
824aa57
feat: Add label-based grouping to Feature Analytics charts
talissoncosta Apr 10, 2026
31fa05f
feat: Add BarChart and MultiSelect Storybook stories
talissoncosta Apr 10, 2026
ea963de
fix: Use natural sort order for token generation
talissoncosta Apr 10, 2026
da0ff36
fix: Remove hooks-in-loop violation, add unit tests
talissoncosta Apr 10, 2026
29b39c4
refactor: Add useChartColors hook, remove direct getCSSVars calls
talissoncosta Apr 10, 2026
3ba0103
fix: Use useChartColorMap hook in stories, replace inline styles
talissoncosta Apr 10, 2026
2abf581
refactor: Inline getCSSVar into useChartColors, add explanatory comments
talissoncosta Apr 10, 2026
d688b51
refactor: Move recharts overrides to dedicated 3rdParty partial
talissoncosta Apr 13, 2026
51eb99c
refactor: Adopt inline var() token constants for charts, drop useChar…
talissoncosta Apr 13, 2026
ddc7bf4
chore: Address self-review nits on Feature Analytics PR
talissoncosta Apr 13, 2026
0583838
fix: Labelled chart now matches env-path date axis + guards against n…
talissoncosta Apr 13, 2026
900fe38
fix: Restore env-colour parity with old prod chart
talissoncosta Apr 13, 2026
4f0fe14
fix: Tooltip shows env names + restore readable label contrast
talissoncosta Apr 13, 2026
d63943f
refactor: Extract useEnvChartProps, drop BarChart dead props, switch …
talissoncosta Apr 13, 2026
9d311ef
chore: Address review feedback — O(1) day lookup + readable var names
talissoncosta Apr 13, 2026
a5a0564
chore: Standardise projectId/featureId to number, use nullish coalescing
talissoncosta Apr 14, 2026
ec081f3
fix: SDK filter always visible to prevent layout shift
talissoncosta Apr 14, 2026
819d9df
refactor: Use EmptyState component, drop inline styles
talissoncosta Apr 14, 2026
50e3bc9
fix: Make BarChart story data deterministic for Chromatic stability
talissoncosta Apr 15, 2026
0ec66cd
chore: Remove MultiSelect Storybook stories (defer to dedicated refac…
talissoncosta Apr 15, 2026
040e3d1
feat: Gate labelled chart behind sdk_usage_charts feature flag
talissoncosta Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions frontend/common/services/useFeatureAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,37 +42,52 @@ export const featureAnalyticsService = service
}),
)

const error = responses.find((v) => v.isError)?.error
const error = responses.find((response) => response.isError)?.error
const today = moment().startOf('day')
const startDate = moment(today).subtract(query.period - 1, 'days')
const preBuiltData: Res['featureAnalytics'] = []

// Pre-build one bucket per day with zero counts for each env, and
// keep a `day → bucket` map so per-entry lookups below are O(1)
// instead of O(days) via Array.find (matters for long periods and
// high entry counts).
type DayBucket = { day: string } & { [envId: string]: number }
const preBuiltData: DayBucket[] = []
const bucketByDay = new Map<string, DayBucket>()
for (
let date = startDate.clone();
date.isSameOrBefore(today);
date.add(1, 'days')
) {
const dayObj: Res['featureAnalytics'][number] = {
day: date.format('Do MMM'),
}
const day = date.format('Do MMM')
const bucket: DayBucket = { day }
query.environment_ids.forEach((envId) => {
dayObj[envId] = 0
bucket[envId] = 0
})
preBuiltData.push(dayObj)
preBuiltData.push(bucket)
bucketByDay.set(day, bucket)
}

// Collect raw entries with labels intact for label-based grouping.
// chartData aggregates by environment (existing behaviour),
Comment thread
talissoncosta marked this conversation as resolved.
// rawEntries preserves per-SDK labels for stacked charts (#6067).
const rawEntries: Res['environmentAnalytics'] = []

responses.forEach((response, i) => {
const environment_id = query.environment_ids[i]
const environmentId = query.environment_ids[i]

response.data?.forEach((entry) => {
const date = moment(entry.day).format('Do MMM')
const dayEntry = preBuiltData.find((d) => d.day === date)
if (dayEntry) {
dayEntry[environment_id] = entry.count // Set count for specific environment ID
rawEntries.push(entry)
const day = moment(entry.day).format('Do MMM')
const bucket = bucketByDay.get(day)
if (bucket) {
bucket[environmentId] = entry.count
}
})
})
return {
data: error ? [] : preBuiltData,
data: error
? { chartData: [], rawEntries: [] }
: { chartData: preBuiltData, rawEntries },
error,
}
},
Expand Down
8 changes: 0 additions & 8 deletions frontend/common/theme/index.ts

This file was deleted.

12 changes: 12 additions & 0 deletions frontend/common/theme/tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,18 @@
"info": { "cssVar": "--color-icon-info", "light": "#0aaddf", "dark": "#0aaddf" }
}
},
"chart": {
"1": { "cssVar": "--color-chart-1", "light": "#0aaddf", "dark": "#45bce0", "description": "First series in charts. Blue." },
"2": { "cssVar": "--color-chart-2", "light": "#ef4d56", "dark": "#f57c78", "description": "Second series. Red." },
"3": { "cssVar": "--color-chart-3", "light": "#27ab95", "dark": "#56ccad", "description": "Third series. Green." },
"4": { "cssVar": "--color-chart-4", "light": "#ff9f43", "dark": "#ffc08a", "description": "Fourth series. Orange." },
"5": { "cssVar": "--color-chart-5", "light": "#7a4dfc", "dark": "#906af6", "description": "Fifth series. Purple." },
"6": { "cssVar": "--color-chart-6", "light": "#0b8bb2", "dark": "#7ecde2", "description": "Sixth series. Blue dark." },
"7": { "cssVar": "--color-chart-7", "light": "#e61b26", "dark": "#f5a5a2", "description": "Seventh series. Red dark." },
"8": { "cssVar": "--color-chart-8", "light": "#13787b", "dark": "#87d4c4", "description": "Eighth series. Green dark." },
"9": { "cssVar": "--color-chart-9", "light": "#fa810c", "dark": "#ffd7b5", "description": "Ninth series. Orange dark." },
"10": { "cssVar": "--color-chart-10", "light": "#6837fc", "dark": "#b794ff", "description": "Tenth series. Purple dark." }
},
"radius": {
"none": { "cssVar": "--radius-none", "value": "0px", "description": "Sharp corners. Tables, dividers." },
"xs": { "cssVar": "--radius-xs", "value": "2px", "description": "Barely rounded. Badges, tags." },
Expand Down
182 changes: 127 additions & 55 deletions frontend/common/theme/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,6 @@
// Do not edit manually. Run: npm run generate:tokens
// =============================================================================

export const tokens = {
border: {
action: 'var(--color-border-action, #6837fc)',
danger: 'var(--color-border-danger, #ef4d56)',
default: 'var(--color-border-default)',
disabled: 'var(--color-border-disabled)',
info: 'var(--color-border-info, #0aaddf)',
strong: 'var(--color-border-strong)',
success: 'var(--color-border-success, #27ab95)',
warning: 'var(--color-border-warning, #ff9f43)',
},
icon: {
action: 'var(--color-icon-action, #6837fc)',
danger: 'var(--color-icon-danger, #ef4d56)',
default: 'var(--color-icon-default, #1a2634)',
disabled: 'var(--color-icon-disabled, #9da4ae)',
info: 'var(--color-icon-info, #0aaddf)',
secondary: 'var(--color-icon-secondary, #656d7b)',
success: 'var(--color-icon-success, #27ab95)',
warning: 'var(--color-icon-warning, #ff9f43)',
},
surface: {
action: 'var(--color-surface-action, #6837fc)',
actionActive: 'var(--color-surface-action-active, #3919b7)',
actionHover: 'var(--color-surface-action-hover, #4e25db)',
actionMuted: 'var(--color-surface-action-muted)',
actionSubtle: 'var(--color-surface-action-subtle)',
active: 'var(--color-surface-active)',
danger: 'var(--color-surface-danger)',
default: 'var(--color-surface-default, #ffffff)',
emphasis: 'var(--color-surface-emphasis, #e0e3e9)',
hover: 'var(--color-surface-hover)',
info: 'var(--color-surface-info)',
muted: 'var(--color-surface-muted, #eff1f4)',
subtle: 'var(--color-surface-subtle, #fafafb)',
success: 'var(--color-surface-success)',
warning: 'var(--color-surface-warning)',
},
text: {
action: 'var(--color-text-action, #6837fc)',
danger: 'var(--color-text-danger, #ef4d56)',
default: 'var(--color-text-default, #1a2634)',
disabled: 'var(--color-text-disabled, #9da4ae)',
info: 'var(--color-text-info, #0aaddf)',
secondary: 'var(--color-text-secondary, #656d7b)',
success: 'var(--color-text-success, #27ab95)',
tertiary: 'var(--color-text-tertiary, #9da4ae)',
warning: 'var(--color-text-warning, #ff9f43)',
},
} as const

export type TokenEntry = {
value: string
description: string
Expand Down Expand Up @@ -157,7 +106,130 @@ export const easing: Record<string, TokenEntry> = {
},
}

export type TokenCategory = keyof typeof tokens
export type TokenName<C extends TokenCategory> = keyof (typeof tokens)[C]
export type RadiusScale = keyof typeof radius
export type ShadowScale = keyof typeof shadow
// =============================================================================
// Flat token constants — semantic tokens as CSS value strings.
// Use directly in any context that accepts a CSS value:
// <Bar fill={colorChart1} /> (recharts prop)
// style={{ color: colorTextSecondary }} (inline style)
// border: `1px solid ${colorBorderDefault}` (template strings)
// var() resolves at render; theme toggle updates colours via CSS cascade.
// =============================================================================

// Border
export const colorBorderAction = 'var(--color-border-action, #6837fc)'
export const colorBorderDanger = 'var(--color-border-danger, #ef4d56)'
export const colorBorderDefault =
'var(--color-border-default, rgba(101, 109, 123, 0.16))'
export const colorBorderDisabled =
'var(--color-border-disabled, rgba(101, 109, 123, 0.08))'
export const colorBorderInfo = 'var(--color-border-info, #0aaddf)'
export const colorBorderStrong =
'var(--color-border-strong, rgba(101, 109, 123, 0.24))'
export const colorBorderSuccess = 'var(--color-border-success, #27ab95)'
export const colorBorderWarning = 'var(--color-border-warning, #ff9f43)'

// Icon
export const colorIconAction = 'var(--color-icon-action, #6837fc)'
export const colorIconDanger = 'var(--color-icon-danger, #ef4d56)'
export const colorIconDefault = 'var(--color-icon-default, #1a2634)'
export const colorIconDisabled = 'var(--color-icon-disabled, #9da4ae)'
export const colorIconInfo = 'var(--color-icon-info, #0aaddf)'
export const colorIconSecondary = 'var(--color-icon-secondary, #656d7b)'
export const colorIconSuccess = 'var(--color-icon-success, #27ab95)'
export const colorIconWarning = 'var(--color-icon-warning, #ff9f43)'

// Surface
export const colorSurfaceAction = 'var(--color-surface-action, #6837fc)'
export const colorSurfaceActionActive =
'var(--color-surface-action-active, #3919b7)'
export const colorSurfaceActionHover =
'var(--color-surface-action-hover, #4e25db)'
export const colorSurfaceActionMuted =
'var(--color-surface-action-muted, rgba(104, 55, 252, 0.16))'
export const colorSurfaceActionSubtle =
'var(--color-surface-action-subtle, rgba(104, 55, 252, 0.08))'
export const colorSurfaceActive =
'var(--color-surface-active, rgba(0, 0, 0, 0.16))'
export const colorSurfaceDanger =
'var(--color-surface-danger, rgba(239, 77, 86, 0.08))'
export const colorSurfaceDefault = 'var(--color-surface-default, #ffffff)'
export const colorSurfaceEmphasis = 'var(--color-surface-emphasis, #e0e3e9)'
export const colorSurfaceHover =
'var(--color-surface-hover, rgba(0, 0, 0, 0.08))'
export const colorSurfaceInfo =
'var(--color-surface-info, rgba(10, 173, 223, 0.08))'
export const colorSurfaceMuted = 'var(--color-surface-muted, #eff1f4)'
export const colorSurfaceSubtle = 'var(--color-surface-subtle, #fafafb)'
export const colorSurfaceSuccess =
'var(--color-surface-success, rgba(39, 171, 149, 0.08))'
export const colorSurfaceWarning =
'var(--color-surface-warning, rgba(255, 159, 67, 0.08))'

// Text
export const colorTextAction = 'var(--color-text-action, #6837fc)'
export const colorTextDanger = 'var(--color-text-danger, #ef4d56)'
export const colorTextDefault = 'var(--color-text-default, #1a2634)'
export const colorTextDisabled = 'var(--color-text-disabled, #9da4ae)'
export const colorTextInfo = 'var(--color-text-info, #0aaddf)'
export const colorTextSecondary = 'var(--color-text-secondary, #656d7b)'
export const colorTextSuccess = 'var(--color-text-success, #27ab95)'
export const colorTextTertiary = 'var(--color-text-tertiary, #9da4ae)'
export const colorTextWarning = 'var(--color-text-warning, #ff9f43)'

// Chart
export const colorChart1 = 'var(--color-chart-1, #0aaddf)'
export const colorChart2 = 'var(--color-chart-2, #ef4d56)'
export const colorChart3 = 'var(--color-chart-3, #27ab95)'
export const colorChart4 = 'var(--color-chart-4, #ff9f43)'
export const colorChart5 = 'var(--color-chart-5, #7a4dfc)'
export const colorChart6 = 'var(--color-chart-6, #0b8bb2)'
export const colorChart7 = 'var(--color-chart-7, #e61b26)'
export const colorChart8 = 'var(--color-chart-8, #13787b)'
export const colorChart9 = 'var(--color-chart-9, #fa810c)'
export const colorChart10 = 'var(--color-chart-10, #6837fc)'

// Chart palette — indexed access for building colour maps.
export const CHART_COLOURS = [
colorChart1,
colorChart2,
colorChart3,
colorChart4,
colorChart5,
colorChart6,
colorChart7,
colorChart8,
colorChart9,
colorChart10,
] as const

// Radius
export const radius2xl = 'var(--radius-2xl, 18px)'
export const radiusFull = 'var(--radius-full, 9999px)'
export const radiusLg = 'var(--radius-lg, 8px)'
export const radiusMd = 'var(--radius-md, 6px)'
export const radiusNone = 'var(--radius-none, 0px)'
export const radiusSm = 'var(--radius-sm, 4px)'
export const radiusXl = 'var(--radius-xl, 10px)'
export const radiusXs = 'var(--radius-xs, 2px)'

// Shadow
export const shadowLg =
'var(--shadow-lg, 0px 8px 16px oklch(from var(--slate-1000) l c h / 0.15))'
export const shadowMd =
'var(--shadow-md, 0px 4px 8px oklch(from var(--slate-1000) l c h / 0.12))'
export const shadowSm =
'var(--shadow-sm, 0px 1px 2px oklch(from var(--slate-1000) l c h / 0.05))'
export const shadowXl =
'var(--shadow-xl, 0px 12px 24px oklch(from var(--slate-1000) l c h / 0.20))'

// Duration
export const durationFast = 'var(--duration-fast, 100ms)'
export const durationNormal = 'var(--duration-normal, 200ms)'
export const durationSlow = 'var(--duration-slow, 300ms)'

// Easing
export const easingEntrance =
'var(--easing-entrance, cubic-bezier(0.0, 0, 0.38, 0.9))'
export const easingExit = 'var(--easing-exit, cubic-bezier(0.2, 0, 1, 0.9))'
export const easingStandard =
'var(--easing-standard, cubic-bezier(0.2, 0, 0.38, 0.9))'
18 changes: 13 additions & 5 deletions frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1210,14 +1210,22 @@ export type Res = {
releasePipeline: SingleReleasePipeline
pipelineStages: PagedResponse<PipelineStage>
featureCodeReferences: FeatureCodeReferences[]
featureAnalytics: ({
day: string
} & {
[environmentId: string]: number
})[]
featureAnalytics: {
chartData: ({
day: string
} & {
[environmentId: string]: number
})[]
rawEntries: Res['environmentAnalytics']
}
environmentAnalytics: {
day: string
count: number
labels?: {
user_agent?: string | null
client_application_name?: string | null
client_application_version?: string | null
} | null
}[]
featureList: {
results: ProjectFlag[]
Expand Down
6 changes: 3 additions & 3 deletions frontend/documentation/DecisionFramework.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ Run `git commit` — the pre-commit hook generates `_tokens.scss` and `tokens.ts
// Icons reference the icon token
<Icon fill="var(--color-icon-danger)" />

// Or via TS exports
import { tokens } from 'common/theme/tokens'
<Icon fill={tokens.icon.danger} />
// Or via TS exports — flat camelCase constants
import { colorIconDanger } from 'common/theme/tokens'
<Icon fill={colorIconDanger} />
```

Dark mode works automatically — no extra overrides needed.
Expand Down
Loading
Loading