Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 30 additions & 0 deletions src/components/Pega_Extensions_ProgressBar/Docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Meta, Primary, Controls, Story } from '@storybook/addon-docs/blocks';
import * as DemoStories from './demo.stories';

<Meta of={DemoStories} />

# Progress Bar

`Pega_Extensions_ProgressBar` is a theme-aware, accessible progress widget built for jobs that report rapid, frequent progress — for example once a second — rather than slow-moving milestones. It has no bound property of its own — it reads its `value`/`max` from the configured `dataPage`, and the PCore messaging service is the only mechanism that triggers a refresh.

On mount, the widget fetches `dataPage` via `PCore.getDataApiUtils().getData()` and subscribes to case updates with `PCore.getMessagingServiceManager().subscribe({ matcher: 'CASE', criteria: { caseId } }, ...)`. Every time the messaging service reports the case changed, the widget refetches the data page, so it can keep up with updates arriving multiple times per second; it unsubscribes on unmount. Until the first fetch resolves, the widget renders an indeterminate state.

The data page can also drive the visual tone: if the record includes a valid `toneProperty` value (`accent`, `success`, `warning`, or `danger`), it overrides the static `tone` prop — useful for flipping to `danger` the moment a job reports a failure.

Set `indeterminateOnly` to skip the data page and messaging service entirely and just show a never-ending progress animation — useful when work is happening but there's nothing to track it against.

<Primary />

## Props

<Controls />

## Examples

<Story of={DemoStories.Complete} />

<Story of={DemoStories.AtRisk} />

<Story of={DemoStories.Loading} />

<Story of={DemoStories.Continuous} />
183 changes: 183 additions & 0 deletions src/components/Pega_Extensions_ProgressBar/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
{
"name": "Pega_Extensions_ProgressBar",
"label": "Progress Bar",
"description": "Accessible progress widget for jobs that push rapid updates (for example once a second); it reads its value from a data page and refreshes only in response to PCore messaging service updates, never on a timer or by polling.",
"organization": "Pega",
"version": "5.0.4",
"library": "Extensions",
"allowedApplications": [],
"componentKey": "Pega_Extensions_ProgressBar",
"type": "Widget",
"subtype": [
"PAGE",
"CASE"
],
"properties": [
{
"name": "label",
"label": "Label",
"format": "TEXT",
"required": true
},
{
"name": "indeterminateOnly",
"label": "Indeterminate only",
"format": "BOOLEAN",
"defaultValue": false
},
{
"name": "dataPage",
"label": "Data page name",
"format": "TEXT",
"visibility": "($this.indeterminateOnly = false)"
},
{
"name": "valueProperty",
"label": "Value property name",
"format": "TEXT",
"defaultValue": "Value",
"visibility": "($this.indeterminateOnly = false)"
},
{
"name": "minProperty",
"label": "Minimum property name",
"format": "TEXT",
"defaultValue": "Min",
"visibility": "($this.indeterminateOnly = false)"
},
{
"name": "maxProperty",
"label": "Maximum property name",
"format": "TEXT",
"defaultValue": "Max",
"visibility": "($this.indeterminateOnly = false)"
},
{
"name": "toneProperty",
"label": "Tone property name",
"format": "TEXT",
"defaultValue": "Tone",
"visibility": "($this.indeterminateOnly = false)"
},
{
"name": "tone",
"label": "Default progress tone",
"format": "SELECT",
"defaultValue": "accent",
"source": [
{
"key": "accent",
"value": "Accent"
},
{
"key": "success",
"value": "Success"
},
{
"key": "warning",
"value": "Warning"
},
{
"key": "danger",
"value": "Danger"
}
]
},
{
"name": "size",
"label": "Bar size",
"format": "SELECT",
"defaultValue": "regular",
"source": [
{
"key": "compact",
"value": "Compact"
},
{
"key": "regular",
"value": "Regular"
},
{
"key": "large",
"value": "Large"
}
]
},
{
"name": "showValue",
"label": "Show value",
"format": "BOOLEAN",
"defaultValue": true,
"visibility": "($this.indeterminateOnly = false)"
},
{
"name": "showMarkers",
"label": "Show milestone markers",
"format": "BOOLEAN",
"defaultValue": true,
"visibility": "($this.indeterminateOnly = false)"
},
{
"label": "Input settings",
"format": "GROUP",
"properties": [
{
"name": "helperText",
"label": "Helper text",
"format": "TEXT"
}
]
},
{
"label": "Conditions",
"format": "GROUP",
"properties": [
{
"name": "visibility",
"label": "Visibility",
"format": "VISIBILITY"
}
]
},
{
"label": "Advanced",
"format": "GROUP",
"collapsible": true,
"properties": [
{
"name": "testId",
"label": "Test ID",
"format": "TEXT",
"ignorePattern": "[^-_\\p{N}\\p{L}]",
"includeAnnotations": false
}
]
},
{
"format": "LABEL",
"label": "About",
"name": "PegaAboutLabel",
"variant": "h3"
},
{
"format": "LABEL",
"label": "Pega_Extensions 5.0.4",
"name": "PegaAboutData",
"variant": "primary"
}
],
"defaultConfig": {
"label": "@L $this.label",
"indeterminateOnly": false,
"valueProperty": "Value",
"minProperty": "Min",
"maxProperty": "Max",
"toneProperty": "Tone",
"tone": "accent",
"size": "regular",
"showValue": true,
"showMarkers": true
},
"infinityVersion": "26.1.1",
"packageCosmosVersion": "9.25.0"
}
141 changes: 141 additions & 0 deletions src/components/Pega_Extensions_ProgressBar/demo.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import type { Meta, StoryObj } from '@storybook/react-webpack5';

import { configProps, rapidProgressSteps, mockCompleteDataPageResponse } from './mock';
import { PegaExtensionsProgressBar, type PegaExtensionsProgressBarProps } from './index';

type StoryArgs = Omit<PegaExtensionsProgressBarProps, 'getPConnect'>;

const renderProgressBar = (args: StoryArgs) => {
const props = {
...args,
getPConnect: () =>
({
getValue: () => 'WORK-1',
getLocalizedValue: (text: string) => text,
getContextName: () => 'primary',
}) as unknown as typeof PConnect,
};

return <PegaExtensionsProgressBar {...props} />;
};

/* Fake PCore so the widget can fetch and subscribe via the messaging service in Storybook */
const stubPCore = (getData: () => Promise<unknown>) => {
window.PCore = {
getConstants: () => ({ CASE_INFO: { CASE_INFO_ID: 'caseInfoID' } }),
getDataApiUtils: () => ({ getData }),
getMessagingServiceManager: () => ({
subscribe: () => 'subscription-id',
unsubscribe: () => {},
}),
} as unknown as typeof PCore;
};

/* Simulates the messaging service pushing a rapid succession of progress updates, roughly once a second */
const stubPCoreWithRapidUpdates = (steps: number[], intervalMs = 1000) => {
let step = 0;
window.PCore = {
getConstants: () => ({ CASE_INFO: { CASE_INFO_ID: 'caseInfoID' } }),
getDataApiUtils: () => ({
getData: () => Promise.resolve({ data: { data: [{ Value: steps[step], Max: 100 }] } }),
}),
getMessagingServiceManager: () => ({
subscribe: (_filter: unknown, handler: () => void) => {
const intervalId = window.setInterval(() => {
if (step < steps.length - 1) step += 1;
handler();
}, intervalMs);
return String(intervalId);
},
unsubscribe: (subscriptionId: string) => window.clearInterval(Number(subscriptionId)),
}),
} as unknown as typeof PCore;
};

const StoryComponent = (args: StoryArgs) => {
stubPCoreWithRapidUpdates(rapidProgressSteps);
return renderProgressBar(args);
};

const meta = {
title: 'Widgets/Progress Bar',
component: StoryComponent,
argTypes: {
tone: {
options: ['accent', 'success', 'warning', 'danger'],
control: {
type: 'select',
},
},
size: {
options: ['compact', 'regular', 'large'],
control: {
type: 'inline-radio',
},
},
},
} satisfies Meta<StoryArgs>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
render: (args) => {
stubPCoreWithRapidUpdates(rapidProgressSteps);
return renderProgressBar(args);
},
args: configProps,
};

export const Complete: Story = {
render: (args) => {
stubPCore(() => Promise.resolve(mockCompleteDataPageResponse));
return renderProgressBar(args);
},
args: {
...configProps,
label: 'Backup job progress',
tone: 'success',
helperText: 'The backup job finished processing all records.',
},
};

export const AtRisk: Story = {
render: (args) => {
stubPCore(() => Promise.resolve({ data: { data: [{ Value: 42, Max: 100 }] } }));
return renderProgressBar(args);
},
args: {
...configProps,
label: 'Sync job progress',
tone: 'warning',
size: 'large',
helperText: 'Updates have slowed; the sync job may be stalled.',
},
};

export const Loading: Story = {
render: (args) => {
/* Never resolves, so the widget stays indeterminate until the messaging service delivers the first update */
stubPCore(() => new Promise(() => {}));
return renderProgressBar(args);
},
args: {
...configProps,
label: 'Import job progress',
helperText: 'Waiting for the import job to report its first update.',
},
};

export const Continuous: Story = {
/* indeterminateOnly mode never fetches or subscribes */
render: (args) => renderProgressBar(args),
args: {
...configProps,
label: 'Indexing job',
indeterminateOnly: true,
dataPage: undefined,
helperText: 'Runs continuously while the indexing job is in progress.',
},
};
Loading
Loading