Skip to content

Commit

Permalink
feat(dashboard): implement embedded chart cards (#856)
Browse files Browse the repository at this point in the history
* feat(dashboard): add basic embedded chart cards

* refactor iframe src construction

* add logical controller for all grafana chart cards

* release subscriptions when no controller subscribers

* cleanup

* replace px count with em

* wrap content in CardBody

* slow refresh rate

* add refresh and popout buttons, style plainly

* add TODO

* refactor, simplify controller timing

* cleanup controller instantiation and resource handling. button handling. recording creation integration with recording form.

* use embedded grafana builtin refresh capability

* cleanup, fix bug with no-recording target selection

* fixup! cleanup, fix bug with no-recording target selection

Signed-off-by: Andrew Azores <[email protected]>

* add leniency for loading no-longer-known card types from storage

* refactor cleanup

* reset iframe on target change to force data refresh

* add TODO

* update empty state

* re-add a refresh button to reset chart card

* only apply negative margin on wider cards

* cleanup

* react to recording start/stop/delete

* require recording to be running

* extract min refresh to settings

* break circular dep

* add checkbox for restarting existing recordings

* add basic snapshot test

* fix settings test

* add more tests

Signed-off-by: Andrew Azores <[email protected]>

* fix imports

* rebase fixup

Signed-off-by: Andrew Azores <[email protected]>

* use consistent card height

* fix array key warnings

* add missing set state call

* use function form set state

* remove redundant nullsafe nav check

* use array destructuring

* remove redundant first operator

* remove more redundant nullsafe navs

* replace <code> with <Label>

* set description empty to remove redundant explanation

* rename button

* memoize controller context

* reduce negative margin for easier dragging and resizing

* allow dynamic card height

* localize card content

* correct tests for localization

Signed-off-by: Andrew Azores <[email protected]>

* add typedefs for react-test-renderer

* remove redundant fragment wrapping

* tmp? loading view

* refactor cleanup/simplify, also fixes tests

* add snapshot for loading state

* controller initialization bugfix

* use Continuous template instead, seems to generally support most metrics cards

Signed-off-by: Andrew Azores <[email protected]>

* reset history and perform cleanup between tests

* set provider order to match real application

* experiment with generic provider render function

* move generic function into Common, expect localized view in test

* temporarily disable pointer events on iframes while resizing

* apply pointer events disabling during drag-and-drop

* downgrade to BETA

* lint fix

---------

Signed-off-by: Andrew Azores <[email protected]>
  • Loading branch information
andrewazores authored Feb 10, 2023
1 parent 2c3fed4 commit 8827aef
Show file tree
Hide file tree
Showing 26 changed files with 1,649 additions and 46 deletions.
22 changes: 22 additions & 0 deletions locales/en/public.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,23 @@
"OPEN_SOURCE_LICENSE": "Open Source License",
"VERSION": "version"
},
"CHART_CARD": {
"BUTTONS": {
"CREATE": {
"LABEL": "Create"
},
"POPOUT": {
"LABEL": "Pop out {{chartKind}} chart"
},
"SYNC": {
"LABEL": "Synchronize {{chartKind}} chart"
}
},
"NO_RECORDING": {
"DESCRIPTION": "Metrics cards display data taken from running flight recordings with the label <label>origin={{recordingName}}</label>. No such recordings are currently available.",
"TITLE": "No source recording"
}
},
"DashboardCardActionMenu": {
"RESET_SIZE": "Reset Size"
},
Expand Down Expand Up @@ -55,6 +72,11 @@
"LANGUAGE_REGION": "Language & Region",
"NOTIFICATION_MESSAGE": "Notifications & Messages"
},
"CHARTS_CONFIG": {
"DESCRIPTION": "",
"REFRESH_RATE_SETTING": "Configure the minimum time to wait between data refreshes. Individual metrics cards may still request updates on a faster cycle, but the client application instance will throttle update requests to the server according to this setting.",
"TITLE": "Dashboard Metrics Configuration"
},
"CREDENTIALS_STORAGE": {
"BACKEND": {
"DESCRIPTION": "Keep credentials in encrypted Cryostat backend storage. These credentials will be available to other users and will be used for Automated Rules.",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@types/enzyme-adapter-react-16": "^1.0.6",
"@types/jest": "^27.0.2",
"@types/js-base64": "3.3.1",
"@types/react-test-renderer": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"@vue/preload-webpack-plugin": "^2.0.0",
Expand Down
13 changes: 13 additions & 0 deletions src/app/CreateRecording/CreateRecording.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { RecordingLabel } from '@app/RecordingMetadata/RecordingLabel';
import { TemplateType } from '@app/Shared/Services/Api.service';
import { TargetView } from '@app/TargetView/TargetView';
import { Card, CardBody, Tab, Tabs } from '@patternfly/react-core';
Expand All @@ -45,8 +46,14 @@ import { CustomRecordingForm } from './CustomRecordingForm';
import { SnapshotRecordingForm } from './SnapshotRecordingForm';

export interface CreateRecordingProps {
restartExisting?: boolean;
name?: string;
templateName?: string;
templateType?: TemplateType;
labels?: RecordingLabel[];
duration?: number;
maxAge?: number;
maxSize?: number;
}

export interface EventTemplate {
Expand All @@ -63,8 +70,14 @@ const Comp: React.FC<RouteComponentProps<Record<string, never>, StaticContext, C

const prefilled = React.useMemo(
() => ({
restartExisting: props.location?.state?.restartExisting,
name: props.location?.state?.name,
templateName: props.location?.state?.templateName,
templateType: props.location?.state?.templateType,
labels: props.location?.state?.labels,
duration: props.location?.state?.duration,
maxAge: props.location?.state?.maxAge,
maxSize: props.location?.state?.maxSize,
}),
[props.location]
);
Expand Down
53 changes: 42 additions & 11 deletions src/app/CreateRecording/CustomRecordingForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,14 @@ import { EventTemplate } from './CreateRecording';

export interface CustomRecordingFormProps {
prefilled?: {
restartExisting?: boolean;
name?: string;
templateName?: string;
templateType?: TemplateType;
labels?: RecordingLabel[];
duration?: number;
maxAge?: number;
maxSize?: number;
};
}

Expand All @@ -89,22 +95,29 @@ export const CustomRecordingForm: React.FC<CustomRecordingFormProps> = ({ prefil
const history = useHistory();
const addSubscription = useSubscriptions();

const [recordingName, setRecordingName] = React.useState('');
const [nameValid, setNameValid] = React.useState(ValidatedOptions.default);
const [continuous, setContinuous] = React.useState(false);
const [recordingName, setRecordingName] = React.useState(prefilled?.name || '');
const [nameValid, setNameValid] = React.useState(
prefilled?.name
? RecordingNamePattern.test(recordingName)
? ValidatedOptions.success
: ValidatedOptions.error
: ValidatedOptions.default
);
const [restartExisting, setRestartExisting] = React.useState(prefilled?.restartExisting || false);
const [continuous, setContinuous] = React.useState((prefilled?.duration || 30) < 1);
const [archiveOnStop, setArchiveOnStop] = React.useState(true);
const [duration, setDuration] = React.useState(30);
const [duration, setDuration] = React.useState(prefilled?.duration || 30);
const [durationUnit, setDurationUnit] = React.useState(1000);
const [durationValid, setDurationValid] = React.useState(ValidatedOptions.success);
const [templates, setTemplates] = React.useState([] as EventTemplate[]);
const [templateName, setTemplateName] = React.useState<string | undefined>(prefilled?.templateName);
const [templateType, setTemplateType] = React.useState<TemplateType | undefined>(prefilled?.templateType);
const [maxAge, setMaxAge] = React.useState(0);
const [maxAge, setMaxAge] = React.useState(prefilled?.maxAge || 0);
const [maxAgeUnits, setMaxAgeUnits] = React.useState(1);
const [maxSize, setMaxSize] = React.useState(0);
const [maxSize, setMaxSize] = React.useState(prefilled?.maxSize || 0);
const [maxSizeUnits, setMaxSizeUnits] = React.useState(1);
const [toDisk, setToDisk] = React.useState(true);
const [labels, setLabels] = React.useState([] as RecordingLabel[]);
const [labels, setLabels] = React.useState(prefilled?.labels || []);
const [labelsValid, setLabelsValid] = React.useState(ValidatedOptions.default);
const [loading, setLoading] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState('');
Expand All @@ -119,14 +132,21 @@ export const CustomRecordingForm: React.FC<CustomRecordingFormProps> = ({ prefil
.subscribe((resp) => {
setLoading(false);
if (resp && resp.ok) {
history.push('/recordings');
history.goBack();
}
})
);
},
[addSubscription, context.api, history, setLoading]
);

const handleRestartExistingChange = React.useCallback(
(checked) => {
setRestartExisting(checked);
},
[setRestartExisting]
);

const handleContinuousChange = React.useCallback(
(checked) => {
setContinuous(checked);
Expand Down Expand Up @@ -228,12 +248,12 @@ export const CustomRecordingForm: React.FC<CustomRecordingFormProps> = ({ prefil
const setRecordingOptions = React.useCallback(
(options: RecordingOptions) => {
// toDisk is not set, and defaults to true because of https://github.com/cryostatio/cryostat/issues/263
setMaxAge(options.maxAge || 0);
setMaxAge(prefilled?.maxAge || options.maxAge || 0);
setMaxAgeUnits(1);
setMaxSize(options.maxSize || 0);
setMaxSize(prefilled?.maxSize || options.maxSize || 0);
setMaxSizeUnits(1);
},
[setMaxAge, setMaxAgeUnits, setMaxSize, setMaxSizeUnits]
[setMaxAge, setMaxAgeUnits, setMaxSize, setMaxSizeUnits, prefilled]
);

const handleSubmit = React.useCallback(() => {
Expand All @@ -249,6 +269,7 @@ export const CustomRecordingForm: React.FC<CustomRecordingFormProps> = ({ prefil
}

const options: RecordingOptions = {
restart: restartExisting,
toDisk: toDisk,
maxAge: toDisk ? (continuous ? maxAge * maxAgeUnits : undefined) : undefined,
maxSize: toDisk ? maxSize * maxSizeUnits : undefined,
Expand Down Expand Up @@ -276,6 +297,7 @@ export const CustomRecordingForm: React.FC<CustomRecordingFormProps> = ({ prefil
nameValid,
notifications,
recordingName,
restartExisting,
toDisk,
handleCreateRecording,
]);
Expand Down Expand Up @@ -397,6 +419,15 @@ export const CustomRecordingForm: React.FC<CustomRecordingFormProps> = ({ prefil
onChange={handleRecordingNameChange}
validated={nameValid}
/>
<Checkbox
label="Restart if recording already exists"
isChecked={restartExisting}
isDisabled={loading}
onChange={handleRestartExistingChange}
aria-label="restartExisting checkbox"
id="recording-restart-existing"
name="recording-restart-existing"
/>
</FormGroup>
<FormGroup
label="Duration"
Expand Down
2 changes: 2 additions & 0 deletions src/app/Dashboard/AddCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ const PropsConfigForm = ({ onChange, ...props }: PropsConfigFormProps) => {
onChange={handleNumeric(ctrl.key)}
onPlus={handleNumericStep(ctrl.key, 1)}
onMinus={handleNumericStep(ctrl.key, -1)}
min={ctrl.extras?.min}
max={ctrl.extras?.max}
/>
);
break;
Expand Down
Loading

0 comments on commit 8827aef

Please sign in to comment.