@oneuptime/common 14.0.2 → 14.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/AlertFeed.ts +2 -0
- package/Models/DatabaseModels/IncidentAlert.ts +540 -0
- package/Models/DatabaseModels/IncidentFeed.ts +2 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/Project.ts +60 -0
- package/Models/DatabaseModels/RumApplication.ts +52 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/IncidentAlertService.ts +1811 -0
- package/Server/Services/IncidentService.ts +210 -10
- package/Server/Services/IncidentStateTimelineService.ts +24 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +24 -2
- package/Tests/App/Dashboard/BulkIncidentLinkActions.test.tsx +869 -0
- package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
- package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
- package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
- package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -0
- package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +39 -0
- package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
- package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
- package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -0
- package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +38 -18
- package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
- package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
- package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
- package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
- package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
- package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
- package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
- package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
- package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
- package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
- package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -0
- package/Tests/App/Dashboard/IoTMetricTooltips.test.tsx +1123 -0
- package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +837 -0
- package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
- package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
- package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
- package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
- package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
- package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
- package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
- package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
- package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
- package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -0
- package/Tests/App/Dashboard/NetworkDeviceTooltips.test.tsx +852 -0
- package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
- package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
- package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -0
- package/Tests/App/Dashboard/ProxmoxMetricTooltips.test.ts +592 -0
- package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
- package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -0
- package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +1066 -0
- package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -0
- package/Tests/App/Dashboard/ServerlessMetricTooltips.test.tsx +474 -0
- package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
- package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
- package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -0
- package/Tests/App/Dashboard/VMwareMetricTooltips.test.ts +801 -0
- package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
- package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
- package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -0
- package/Tests/Models/DatabaseModels/IncidentAlertModel.test.ts +554 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentAlertMigration.test.ts +341 -0
- package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +903 -0
- package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
- package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
- package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
- package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +103 -0
- package/Tests/Types/Rum/WebVitals.test.ts +173 -0
- package/Tests/UI/Components/FieldLabelInfoCardTooltip.test.tsx +413 -0
- package/Tests/UI/Components/InfoTooltip.test.tsx +242 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +207 -3
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
- package/Tests/UI/Components/TableHeaderTooltip.test.tsx +662 -0
- package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +166 -9
- package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
- package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
- package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
- package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
- package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
- package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
- package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
- package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
- package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
- package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Tests/UI/Utils/PermissionGate.test.ts +76 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +696 -0
- package/Tests/Utils/Rum/SessionTraceState.test.ts +239 -0
- package/Types/Incident/IncidentAlertLink.ts +38 -0
- package/Types/Permission.ts +48 -0
- package/Types/Rum/SessionReplay.ts +25 -6
- package/Types/Rum/WebVitals.ts +52 -0
- package/UI/Components/Detail/FieldLabel.tsx +8 -0
- package/UI/Components/InfoCard/InfoCard.tsx +47 -1
- package/UI/Components/ModelTable/BaseModelTable.tsx +95 -17
- package/UI/Components/ModelTable/Column.ts +5 -0
- package/UI/Components/Table/TableHeader.tsx +47 -2
- package/UI/Components/Table/Types/Column.ts +7 -0
- package/UI/Components/Tooltip/InfoTooltip.tsx +78 -0
- package/UI/Utils/PermissionGate.ts +11 -2
- package/Utils/Incident/IncidentFromAlerts.ts +516 -0
- package/Utils/Rum/SessionTraceState.ts +212 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentAlert.js +553 -0
- package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +62 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/IncidentAlertService.js +1370 -0
- package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
- package/build/dist/Server/Services/IncidentService.js +164 -12
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +21 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +8 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Types/Incident/IncidentAlertLink.js +34 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
- package/build/dist/Types/Permission.js +42 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rum/WebVitals.js +25 -0
- package/build/dist/Types/Rum/WebVitals.js.map +1 -1
- package/build/dist/UI/Components/Detail/FieldLabel.js +2 -0
- package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.js +22 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +66 -14
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/Table/TableHeader.js +18 -2
- package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js +28 -0
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
- package/build/dist/UI/Utils/PermissionGate.js +4 -2
- package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
- package/build/dist/Utils/Incident/IncidentFromAlerts.js +306 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
- package/build/dist/Utils/Rum/SessionTraceState.js +154 -0
- package/build/dist/Utils/Rum/SessionTraceState.js.map +1 -0
- package/package.json +1 -1
|
@@ -208,6 +208,19 @@ export interface BulkActionProps<T extends BaseModel | AnalyticsBaseModel> {
|
|
|
208
208
|
* cannot be undone - it cannot know what else goes with it.
|
|
209
209
|
*/
|
|
210
210
|
deleteConfirmationWarning?: string | undefined;
|
|
211
|
+
/**
|
|
212
|
+
* The word the default bulk Delete uses instead of "Delete", for a table
|
|
213
|
+
* whose rows are relationships rather than things - "Unlink" on a table of
|
|
214
|
+
* links, say. When set, the menu item, the confirmation's title and its
|
|
215
|
+
* sentence all use it ("Unlink 3 Alerts", "Are you sure you want to unlink
|
|
216
|
+
* 3 alerts?"), and the sentence no longer claims the action cannot be
|
|
217
|
+
* undone: removing a relationship is undone by adding it again. It is still
|
|
218
|
+
* the table's delete action - it deletes the selected rows, is gated on the
|
|
219
|
+
* model's delete permission, and stops a second Delete being added.
|
|
220
|
+
*/
|
|
221
|
+
deleteVerb?: string | undefined;
|
|
222
|
+
/* The menu icon to go with deleteVerb. Defaults to the trash can. */
|
|
223
|
+
deleteIcon?: IconProp | undefined;
|
|
211
224
|
}
|
|
212
225
|
|
|
213
226
|
/**
|
|
@@ -562,6 +575,29 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
|
|
562
575
|
|
|
563
576
|
const model: TBaseModel = new props.modelType();
|
|
564
577
|
|
|
578
|
+
/*
|
|
579
|
+
* The header's create button, "<verb> <noun>". Translating the two words
|
|
580
|
+
* separately reads the verb out of context - "Link" comes back as the noun
|
|
581
|
+
* in many languages, and word order differs - so the whole phrase is looked
|
|
582
|
+
* up first ("Link Alert") and the word-by-word join is only the fallback
|
|
583
|
+
* for a phrase with no entry of its own. In English both are the same.
|
|
584
|
+
*/
|
|
585
|
+
const getCreateButtonTitle: () => string = (): string => {
|
|
586
|
+
const verb: string = props.createVerb || "Create";
|
|
587
|
+
const noun: string = props.singularName || model.singularName || "";
|
|
588
|
+
|
|
589
|
+
if (noun) {
|
|
590
|
+
const phrase: string = `${verb} ${noun}`;
|
|
591
|
+
const translatedPhrase: string = tx(phrase);
|
|
592
|
+
|
|
593
|
+
if (translatedPhrase && translatedPhrase !== phrase) {
|
|
594
|
+
return translatedPhrase;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return `${tx(verb)} ${tx(noun)}`;
|
|
599
|
+
};
|
|
600
|
+
|
|
565
601
|
/*
|
|
566
602
|
* How a create / update / delete affordance should be rendered for this
|
|
567
603
|
* viewer. A user without the permission used to simply not see the button,
|
|
@@ -586,8 +622,18 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
|
|
586
622
|
const getActionGate: GetActionGateFunction = (
|
|
587
623
|
action: ModelAction,
|
|
588
624
|
): ActionGate => {
|
|
625
|
+
/*
|
|
626
|
+
* A table that calls its delete "Unlink" must not say the locked button
|
|
627
|
+
* would delete the row's noun ("permission to delete this Alert"). Keyed
|
|
628
|
+
* on deleteVerb alone: deleteButtonText is also set by tables such as
|
|
629
|
+
* "Remove from Project", which keep the default "delete" sentence.
|
|
630
|
+
*/
|
|
589
631
|
const result: PermissionGateResult = PermissionGate.check(model, action, {
|
|
590
632
|
singularName: props.singularName || model.singularName || undefined,
|
|
633
|
+
verb:
|
|
634
|
+
action === ModelAction.Delete
|
|
635
|
+
? props.bulkActions?.deleteVerb?.trim() || undefined
|
|
636
|
+
: undefined,
|
|
591
637
|
});
|
|
592
638
|
|
|
593
639
|
if (result.isAllowed) {
|
|
@@ -2446,9 +2492,7 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
|
|
2446
2492
|
showAs !== ShowAs.OrderedStatesList
|
|
2447
2493
|
) {
|
|
2448
2494
|
headerbuttons.push({
|
|
2449
|
-
title:
|
|
2450
|
-
props.singularName || model.singularName || "",
|
|
2451
|
-
)}`,
|
|
2495
|
+
title: getCreateButtonTitle(),
|
|
2452
2496
|
buttonStyle: ButtonStyleType.NORMAL,
|
|
2453
2497
|
buttonSize: ButtonSize.Normal,
|
|
2454
2498
|
className: "",
|
|
@@ -2908,10 +2952,16 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
|
|
2908
2952
|
|
|
2909
2953
|
const deleteGate: ActionGate = getActionGate(ModelAction.Delete);
|
|
2910
2954
|
|
|
2955
|
+
/* The row action wears the same icon as the bulk one it stands for. */
|
|
2956
|
+
const rowDeleteIcon: IconProp =
|
|
2957
|
+
(props.bulkActions?.deleteVerb?.trim() &&
|
|
2958
|
+
props.bulkActions?.deleteIcon) ||
|
|
2959
|
+
IconProp.Trash;
|
|
2960
|
+
|
|
2911
2961
|
if (props.isDeleteable && deleteGate.show) {
|
|
2912
2962
|
actionsSchema.push({
|
|
2913
2963
|
title: tx(props.deleteButtonText || "Delete"),
|
|
2914
|
-
icon:
|
|
2964
|
+
icon: rowDeleteIcon,
|
|
2915
2965
|
buttonStyleType: ButtonStyleType.DANGER_OUTLINE,
|
|
2916
2966
|
disabled: deleteGate.disabled,
|
|
2917
2967
|
tooltip: deleteGate.tooltip,
|
|
@@ -3008,28 +3058,43 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
|
|
3008
3058
|
|
|
3009
3059
|
const getDeleteBulkAction: GetDeleteBulkActionFunction =
|
|
3010
3060
|
(): BulkActionButtonSchema<TBaseModel> => {
|
|
3061
|
+
/*
|
|
3062
|
+
* A table that names its own verb ("Unlink") removes relationships, not
|
|
3063
|
+
* things, so its sentence drops "This action cannot be undone" - that
|
|
3064
|
+
* would say the opposite of the warning it usually appends.
|
|
3065
|
+
*/
|
|
3066
|
+
const deleteVerb: string | undefined =
|
|
3067
|
+
props.bulkActions?.deleteVerb?.trim() || undefined;
|
|
3068
|
+
|
|
3069
|
+
type GetItemLabelFunction = (items: Array<TBaseModel>) => string;
|
|
3070
|
+
|
|
3071
|
+
const getItemLabel: GetItemLabelFunction = (
|
|
3072
|
+
items: Array<TBaseModel>,
|
|
3073
|
+
): string => {
|
|
3074
|
+
return items.length === 1
|
|
3075
|
+
? props.singularName || model.singularName || "item"
|
|
3076
|
+
: props.pluralName || model.pluralName || "items";
|
|
3077
|
+
};
|
|
3078
|
+
|
|
3011
3079
|
return {
|
|
3012
|
-
title: "Delete",
|
|
3080
|
+
title: deleteVerb || "Delete",
|
|
3013
3081
|
buttonStyleType: ButtonStyleType.DANGER,
|
|
3014
|
-
icon: IconProp.Trash,
|
|
3082
|
+
icon: (deleteVerb && props.bulkActions?.deleteIcon) || IconProp.Trash,
|
|
3015
3083
|
confirmMessage: (items: Array<TBaseModel>) => {
|
|
3016
|
-
const itemLabel: string =
|
|
3017
|
-
items.length === 1
|
|
3018
|
-
? props.singularName || model.singularName || "item"
|
|
3019
|
-
: props.pluralName || model.pluralName || "items";
|
|
3084
|
+
const itemLabel: string = getItemLabel(items);
|
|
3020
3085
|
|
|
3021
3086
|
const warning: string = props.bulkActions?.deleteConfirmationWarning
|
|
3022
3087
|
? ` ${props.bulkActions.deleteConfirmationWarning}`
|
|
3023
3088
|
: "";
|
|
3024
3089
|
|
|
3090
|
+
if (deleteVerb) {
|
|
3091
|
+
return `Are you sure you want to ${deleteVerb.toLowerCase()} ${items.length} ${itemLabel.toLowerCase()}?${warning}`;
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3025
3094
|
return `Are you sure you want to delete ${items.length} ${itemLabel}? This action cannot be undone.${warning}`;
|
|
3026
3095
|
},
|
|
3027
3096
|
confirmTitle: (items: Array<TBaseModel>) => {
|
|
3028
|
-
|
|
3029
|
-
items.length === 1
|
|
3030
|
-
? props.singularName || model.singularName || "item"
|
|
3031
|
-
: props.pluralName || model.pluralName || "items";
|
|
3032
|
-
return `Delete ${items.length} ${itemLabel}`;
|
|
3097
|
+
return `${deleteVerb || "Delete"} ${items.length} ${getItemLabel(items)}`;
|
|
3033
3098
|
},
|
|
3034
3099
|
confirmButtonStyleType: ButtonStyleType.DANGER,
|
|
3035
3100
|
onClick: async ({
|
|
@@ -3207,7 +3272,19 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
|
|
3207
3272
|
* Table derives the row checkboxes from this array being non-empty,
|
|
3208
3273
|
* so injecting a locked Delete into an otherwise empty array would
|
|
3209
3274
|
* grow a selection column onto tables that have never had one.
|
|
3275
|
+
*
|
|
3276
|
+
* A page's own button counts as the delete action when it carries
|
|
3277
|
+
* the title the default one would have: "Delete", or the table's
|
|
3278
|
+
* deleteVerb.
|
|
3210
3279
|
*/
|
|
3280
|
+
const deleteActionTitles: Array<string> = ["Delete"];
|
|
3281
|
+
const bulkDeleteVerb: string | undefined =
|
|
3282
|
+
props.bulkActions?.deleteVerb?.trim() || undefined;
|
|
3283
|
+
|
|
3284
|
+
if (bulkDeleteVerb) {
|
|
3285
|
+
deleteActionTitles.push(bulkDeleteVerb);
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3211
3288
|
const alreadyHasDeleteAction: boolean = sourceButtons.some(
|
|
3212
3289
|
(
|
|
3213
3290
|
action:
|
|
@@ -3221,8 +3298,9 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
|
|
3221
3298
|
typeof action === "object" &&
|
|
3222
3299
|
action !== null &&
|
|
3223
3300
|
"title" in action &&
|
|
3224
|
-
(
|
|
3225
|
-
|
|
3301
|
+
deleteActionTitles.includes(
|
|
3302
|
+
(action as BulkActionButtonSchema<TBaseModel>).title,
|
|
3303
|
+
)
|
|
3226
3304
|
) {
|
|
3227
3305
|
return true;
|
|
3228
3306
|
}
|
|
@@ -63,6 +63,11 @@ export default interface Columns<
|
|
|
63
63
|
colSpan?: number | undefined;
|
|
64
64
|
disableSort?: boolean;
|
|
65
65
|
description?: string | undefined;
|
|
66
|
+
/*
|
|
67
|
+
* What the column's values mean, in an (i) beside the header (see the
|
|
68
|
+
* Table Column of the same name). Spread through to the table as is.
|
|
69
|
+
*/
|
|
70
|
+
headerTooltip?: string | undefined;
|
|
66
71
|
type: FieldType;
|
|
67
72
|
tooltipText?: ((item: TEntity) => string) | undefined;
|
|
68
73
|
actionButtons?: Array<ActionButton>;
|
|
@@ -10,6 +10,7 @@ import { VoidFunction } from "../../../Types/FunctionTypes";
|
|
|
10
10
|
import GenericObject from "../../../Types/GenericObject";
|
|
11
11
|
import IconProp from "../../../Types/Icon/IconProp";
|
|
12
12
|
import useTranslateValue from "../../Utils/Translation";
|
|
13
|
+
import InfoTooltip from "../Tooltip/InfoTooltip";
|
|
13
14
|
import React, { ReactElement, useEffect, useState } from "react";
|
|
14
15
|
|
|
15
16
|
export interface ComponentProps<T extends GenericObject> {
|
|
@@ -157,10 +158,21 @@ const TableHeader: TableHeaderFunction = <T extends GenericObject>(
|
|
|
157
158
|
</>
|
|
158
159
|
);
|
|
159
160
|
|
|
161
|
+
/*
|
|
162
|
+
* A header's name is computed from its content, and the (i)
|
|
163
|
+
* button's "About <title>" would join it - every cell in the
|
|
164
|
+
* column would be announced as "Last seen About Last seen". The
|
|
165
|
+
* column is named by its title alone; the (i) keeps its own name.
|
|
166
|
+
*/
|
|
167
|
+
const headerLabel: string | undefined = column.headerTooltip?.trim()
|
|
168
|
+
? translateString(column.title) ?? column.title
|
|
169
|
+
: undefined;
|
|
170
|
+
|
|
160
171
|
return (
|
|
161
172
|
<th
|
|
162
173
|
key={i}
|
|
163
174
|
scope="col"
|
|
175
|
+
aria-label={headerLabel}
|
|
164
176
|
aria-sort={ariaSort}
|
|
165
177
|
className="text-left text-sm font-semibold text-gray-900"
|
|
166
178
|
>
|
|
@@ -172,7 +184,27 @@ const TableHeader: TableHeaderFunction = <T extends GenericObject>(
|
|
|
172
184
|
* padding moves onto the button so the clickable area is the
|
|
173
185
|
* whole cell, exactly as before.
|
|
174
186
|
*/}
|
|
175
|
-
{
|
|
187
|
+
{/*
|
|
188
|
+
* A header tooltip is its own button, and a button may not
|
|
189
|
+
* sit inside the sort button - so for those columns the sort
|
|
190
|
+
* button shrinks to its content and the (i) follows it.
|
|
191
|
+
*/}
|
|
192
|
+
{canSort && column.headerTooltip?.trim() ? (
|
|
193
|
+
<div className="flex w-full items-center">
|
|
194
|
+
<button
|
|
195
|
+
type="button"
|
|
196
|
+
onClick={sortColumn}
|
|
197
|
+
className="flex py-3 pl-6 pr-1 cursor-pointer text-left font-semibold text-gray-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500"
|
|
198
|
+
>
|
|
199
|
+
{headerContent}
|
|
200
|
+
</button>
|
|
201
|
+
<InfoTooltip
|
|
202
|
+
label={translateString(column.title) ?? column.title}
|
|
203
|
+
text={column.headerTooltip}
|
|
204
|
+
className="mr-6 font-normal"
|
|
205
|
+
/>
|
|
206
|
+
</div>
|
|
207
|
+
) : canSort ? (
|
|
176
208
|
<button
|
|
177
209
|
type="button"
|
|
178
210
|
onClick={sortColumn}
|
|
@@ -181,7 +213,20 @@ const TableHeader: TableHeaderFunction = <T extends GenericObject>(
|
|
|
181
213
|
{headerContent}
|
|
182
214
|
</button>
|
|
183
215
|
) : (
|
|
184
|
-
<div
|
|
216
|
+
<div
|
|
217
|
+
className={
|
|
218
|
+
column.headerTooltip?.trim()
|
|
219
|
+
? `${contentClassName} items-center`
|
|
220
|
+
: contentClassName
|
|
221
|
+
}
|
|
222
|
+
>
|
|
223
|
+
{headerContent}
|
|
224
|
+
<InfoTooltip
|
|
225
|
+
label={translateString(column.title) ?? column.title}
|
|
226
|
+
text={column.headerTooltip}
|
|
227
|
+
className="ml-1 font-normal"
|
|
228
|
+
/>
|
|
229
|
+
</div>
|
|
185
230
|
)}
|
|
186
231
|
</th>
|
|
187
232
|
);
|
|
@@ -8,6 +8,13 @@ export default interface Column<T extends GenericObject> {
|
|
|
8
8
|
// Stable identity carried over from the ModelTable column, if it had one.
|
|
9
9
|
id?: string | undefined;
|
|
10
10
|
description?: string | undefined;
|
|
11
|
+
/*
|
|
12
|
+
* What the column's values mean, in an (i) tooltip beside the header - for
|
|
13
|
+
* a metric column whose title is jargon ("p95", "Restarts"). Separate from
|
|
14
|
+
* `description`, which ModelTable fills from the database column for other
|
|
15
|
+
* purposes and which is never shown in the header.
|
|
16
|
+
*/
|
|
17
|
+
headerTooltip?: string | undefined;
|
|
11
18
|
disableSort?: boolean | undefined;
|
|
12
19
|
tooltipText?: ((item: T) => string) | undefined;
|
|
13
20
|
type: FieldType;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement } from "react";
|
|
2
|
+
import IconProp from "../../../Types/Icon/IconProp";
|
|
3
|
+
import Icon from "../Icon/Icon";
|
|
4
|
+
import Tooltip from "./Tooltip";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* A small (i) beside a label that explains it on hover or keyboard focus -
|
|
8
|
+
* "what does p95 mean?" answered where the number is, rather than in the
|
|
9
|
+
* docs. Built for metric tiles and chart headers, but nothing here is
|
|
10
|
+
* metric-specific.
|
|
11
|
+
*
|
|
12
|
+
* The trigger is a real <button> so it is reachable with Tab and announced
|
|
13
|
+
* as a control; Tippy shows the text on focus as well as on hover and links
|
|
14
|
+
* it with aria-describedby. It is lazy: an overview page draws dozens of
|
|
15
|
+
* these, and none of them needs a Tippy instance until someone reaches for
|
|
16
|
+
* it.
|
|
17
|
+
*
|
|
18
|
+
* A click is swallowed, and so are Enter and Space: the (i) can sit inside a
|
|
19
|
+
* card that is itself a link or a button, and asking what a number means
|
|
20
|
+
* should not navigate away from it.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export interface ComponentProps {
|
|
24
|
+
// The explanation. Nothing renders when it is empty.
|
|
25
|
+
text?: string | undefined;
|
|
26
|
+
/*
|
|
27
|
+
* What is being explained, e.g. the metric's title. Names the button for
|
|
28
|
+
* screen readers ("About p95 duration") and is never shown.
|
|
29
|
+
*/
|
|
30
|
+
label: string;
|
|
31
|
+
className?: string | undefined;
|
|
32
|
+
iconClassName?: string | undefined;
|
|
33
|
+
dataTestId?: string | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const InfoTooltip: FunctionComponent<ComponentProps> = (
|
|
37
|
+
props: ComponentProps,
|
|
38
|
+
): ReactElement => {
|
|
39
|
+
const text: string = (props.text || "").trim();
|
|
40
|
+
|
|
41
|
+
if (!text) {
|
|
42
|
+
return <></>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Tooltip text={text} lazy={true}>
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
aria-label={`About ${props.label}`}
|
|
50
|
+
data-testid={props.dataTestId}
|
|
51
|
+
className={`inline-flex flex-shrink-0 items-center justify-center rounded-full text-gray-400 hover:text-gray-600 focus:outline-none focus-visible:text-gray-600 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 ${
|
|
52
|
+
props.className || ""
|
|
53
|
+
}`}
|
|
54
|
+
onClick={(event: React.MouseEvent<HTMLButtonElement>): void => {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
event.stopPropagation();
|
|
57
|
+
}}
|
|
58
|
+
/*
|
|
59
|
+
* A clickable card that handles Enter / Space itself (InfoCard, a
|
|
60
|
+
* tile acting as a button) would otherwise hear these keys bubble up
|
|
61
|
+
* from the (i) and activate.
|
|
62
|
+
*/
|
|
63
|
+
onKeyDown={(event: React.KeyboardEvent<HTMLButtonElement>): void => {
|
|
64
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
65
|
+
event.stopPropagation();
|
|
66
|
+
}
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
<Icon
|
|
70
|
+
icon={IconProp.InformationCircle}
|
|
71
|
+
className={props.iconClassName || "h-3.5 w-3.5"}
|
|
72
|
+
/>
|
|
73
|
+
</button>
|
|
74
|
+
</Tooltip>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default InfoTooltip;
|
|
@@ -79,6 +79,13 @@ export interface PermissionGateOptions {
|
|
|
79
79
|
* what the user sees on screen ("Monitor" vs "Monitor Template").
|
|
80
80
|
*/
|
|
81
81
|
singularName?: string | undefined;
|
|
82
|
+
/*
|
|
83
|
+
* The verb to use in the message when the operation is presented as
|
|
84
|
+
* something other than its own name - a table of links whose delete action
|
|
85
|
+
* is labelled "Unlink", say. Only the wording changes: the permissions
|
|
86
|
+
* checked and listed are still the ones for `action`.
|
|
87
|
+
*/
|
|
88
|
+
verb?: string | undefined;
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
/*
|
|
@@ -176,15 +183,17 @@ export default class PermissionGate {
|
|
|
176
183
|
const singularName: string =
|
|
177
184
|
options?.singularName || model.singularName || "item";
|
|
178
185
|
|
|
186
|
+
const verb: string = (options?.verb?.trim() || action).toLowerCase();
|
|
187
|
+
|
|
179
188
|
const titles: Array<string> = this.getPermissionTitles(
|
|
180
189
|
this.getModelPermissions(model, action),
|
|
181
190
|
);
|
|
182
191
|
|
|
183
192
|
if (titles.length === 0) {
|
|
184
|
-
return `You do not have permission to ${
|
|
193
|
+
return `You do not have permission to ${verb} this ${singularName}.`;
|
|
185
194
|
}
|
|
186
195
|
|
|
187
|
-
return `You do not have permission to ${
|
|
196
|
+
return `You do not have permission to ${verb} this ${singularName}. You need one of these permissions: ${titles.join(
|
|
188
197
|
", ",
|
|
189
198
|
)}.`;
|
|
190
199
|
}
|