@oneuptime/common 11.3.2 → 11.3.4

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.
Files changed (59) hide show
  1. package/Server/Infrastructure/ClickhouseConfig.ts +21 -0
  2. package/Server/Infrastructure/ClickhouseDatabase.ts +12 -0
  3. package/Server/Services/AnalyticsDatabaseService.ts +79 -4
  4. package/Server/Services/TraceAggregationService.ts +16 -3
  5. package/Tests/Server/Services/TraceAggregationService.test.ts +10 -0
  6. package/Tests/UI/Components/Pill.test.tsx +3 -1
  7. package/Types/Dashboard/DashboardComponentType.ts +1 -0
  8. package/Types/Dashboard/DashboardComponents/DashboardTraceTableComponent.ts +39 -0
  9. package/UI/Components/Date/RangeStartAndEndDateEdit.tsx +228 -44
  10. package/UI/Components/Date/RangeStartAndEndDateView.tsx +73 -21
  11. package/UI/Components/EditionLabel/EditionLabel.tsx +6 -1
  12. package/UI/Components/Footer/Footer.tsx +1 -1
  13. package/UI/Components/GanttChart/ChartContainer.tsx +8 -1
  14. package/UI/Components/Icon/Icon.tsx +13 -1
  15. package/UI/Components/Image/Image.tsx +6 -1
  16. package/UI/Components/Link/Link.tsx +22 -0
  17. package/UI/Components/MasterPage/MasterPage.tsx +30 -1
  18. package/UI/Components/Page/Page.tsx +29 -2
  19. package/UI/Components/Pagination/Pagination.tsx +110 -180
  20. package/Utils/Dashboard/Components/DashboardTraceTableComponent.ts +65 -0
  21. package/Utils/Dashboard/Components/Index.ts +7 -0
  22. package/build/dist/Server/Infrastructure/ClickhouseConfig.js +17 -0
  23. package/build/dist/Server/Infrastructure/ClickhouseConfig.js.map +1 -1
  24. package/build/dist/Server/Infrastructure/ClickhouseDatabase.js +10 -1
  25. package/build/dist/Server/Infrastructure/ClickhouseDatabase.js.map +1 -1
  26. package/build/dist/Server/Services/AnalyticsDatabaseService.js +59 -8
  27. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  28. package/build/dist/Server/Services/TraceAggregationService.js +12 -3
  29. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  30. package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
  31. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  32. package/build/dist/Types/Dashboard/DashboardComponents/DashboardTraceTableComponent.js +2 -0
  33. package/build/dist/Types/Dashboard/DashboardComponents/DashboardTraceTableComponent.js.map +1 -0
  34. package/build/dist/UI/Components/Date/RangeStartAndEndDateEdit.js +116 -26
  35. package/build/dist/UI/Components/Date/RangeStartAndEndDateEdit.js.map +1 -1
  36. package/build/dist/UI/Components/Date/RangeStartAndEndDateView.js +42 -16
  37. package/build/dist/UI/Components/Date/RangeStartAndEndDateView.js.map +1 -1
  38. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +1 -1
  39. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  40. package/build/dist/UI/Components/Footer/Footer.js +1 -1
  41. package/build/dist/UI/Components/GanttChart/ChartContainer.js +9 -1
  42. package/build/dist/UI/Components/GanttChart/ChartContainer.js.map +1 -1
  43. package/build/dist/UI/Components/Icon/Icon.js +2 -2
  44. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  45. package/build/dist/UI/Components/Image/Image.js +8 -1
  46. package/build/dist/UI/Components/Image/Image.js.map +1 -1
  47. package/build/dist/UI/Components/Link/Link.js +9 -0
  48. package/build/dist/UI/Components/Link/Link.js.map +1 -1
  49. package/build/dist/UI/Components/MasterPage/MasterPage.js +2 -1
  50. package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
  51. package/build/dist/UI/Components/Page/Page.js +24 -1
  52. package/build/dist/UI/Components/Page/Page.js.map +1 -1
  53. package/build/dist/UI/Components/Pagination/Pagination.js +44 -94
  54. package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
  55. package/build/dist/Utils/Dashboard/Components/DashboardTraceTableComponent.js +51 -0
  56. package/build/dist/Utils/Dashboard/Components/DashboardTraceTableComponent.js.map +1 -0
  57. package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
  58. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  59. package/package.json +1 -1
@@ -7,7 +7,7 @@ import { GetReactElementFunction } from "../../../UI/Types/FunctionTypes";
7
7
  import Icon from "../Icon/Icon";
8
8
  import Tooltip from "../Tooltip/Tooltip";
9
9
  import RangeStartAndEndDateEdit from "./RangeStartAndEndDateEdit";
10
- import Modal from "../Modal/Modal";
10
+ import Modal, { ModalWidth } from "../Modal/Modal";
11
11
 
12
12
  export interface ComponentProps {
13
13
  dashboardStartAndEndDate: RangeStartAndEndDateTime;
@@ -25,17 +25,65 @@ const DashboardStartAndEndDateView: FunctionComponent<ComponentProps> = (
25
25
  const isCustomRange: boolean =
26
26
  props.dashboardStartAndEndDate.range === TimeRange.CUSTOM;
27
27
 
28
- const getContent: GetReactElementFunction = (): ReactElement => {
29
- const title: string = isCustomRange
30
- ? `${OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(
31
- props.dashboardStartAndEndDate.startAndEndDate?.startValue ||
32
- OneUptimeDate.getCurrentDate(),
33
- )} - ${OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(
34
- props.dashboardStartAndEndDate.startAndEndDate?.endValue ||
35
- OneUptimeDate.getCurrentDate(),
36
- )}`
37
- : props.dashboardStartAndEndDate.range;
28
+ /*
29
+ * The Apply button should only commit a custom range when both ends are set
30
+ * and the start is strictly before the end.
31
+ */
32
+ const isSelectionValid: (
33
+ selection: RangeStartAndEndDateTime | null,
34
+ ) => boolean = (selection: RangeStartAndEndDateTime | null): boolean => {
35
+ if (!selection) {
36
+ return false;
37
+ }
38
+
39
+ if (selection.range !== TimeRange.CUSTOM) {
40
+ return true;
41
+ }
42
+
43
+ const startAndEndDate: typeof selection.startAndEndDate =
44
+ selection.startAndEndDate;
45
+
46
+ if (!startAndEndDate) {
47
+ return false;
48
+ }
49
+
50
+ return OneUptimeDate.isAfter(
51
+ startAndEndDate.endValue,
52
+ startAndEndDate.startValue,
53
+ );
54
+ };
55
+
56
+ const closeModal: () => void = (): void => {
57
+ setTempStartAndEndDate(null);
58
+ setShowTimeSelectModal(false);
59
+ };
38
60
 
61
+ const commitSelection: (selection: RangeStartAndEndDateTime) => void = (
62
+ selection: RangeStartAndEndDateTime,
63
+ ): void => {
64
+ props.onChange(selection);
65
+ closeModal();
66
+ };
67
+
68
+ const getButtonTitle: () => string = (): string => {
69
+ if (isCustomRange) {
70
+ return `${OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(
71
+ props.dashboardStartAndEndDate.startAndEndDate?.startValue ||
72
+ OneUptimeDate.getCurrentDate(),
73
+ false,
74
+ true,
75
+ )} - ${OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(
76
+ props.dashboardStartAndEndDate.startAndEndDate?.endValue ||
77
+ OneUptimeDate.getCurrentDate(),
78
+ false,
79
+ true,
80
+ )}`;
81
+ }
82
+
83
+ return props.dashboardStartAndEndDate.range;
84
+ };
85
+
86
+ const getContent: GetReactElementFunction = (): ReactElement => {
39
87
  return (
40
88
  <div>
41
89
  <Tooltip text="Click to change the date and time range of data on this dashboard.">
@@ -48,30 +96,34 @@ const DashboardStartAndEndDateView: FunctionComponent<ComponentProps> = (
48
96
  }}
49
97
  >
50
98
  <Icon icon={IconProp.Clock} className="w-3.5 h-3.5 text-gray-500" />
51
- <span className="text-xs text-gray-500">{title}</span>
99
+ <span className="text-xs text-gray-500">{getButtonTitle()}</span>
52
100
  </button>
53
101
  </Tooltip>
54
102
  {showTimeSelectModal && (
55
103
  <Modal
56
- title="Select Start and End Time"
57
- onClose={() => {
58
- setTempStartAndEndDate(null);
59
- setShowTimeSelectModal(false);
60
- }}
104
+ title="Select Time Range"
105
+ description="Choose a quick range or set an exact start and end date & time."
106
+ icon={IconProp.Clock}
107
+ modalWidth={ModalWidth.Medium}
108
+ submitButtonText="Apply"
109
+ disableSubmitButton={!isSelectionValid(tempStartAndEndDate)}
110
+ onClose={closeModal}
61
111
  onSubmit={() => {
62
112
  if (tempStartAndEndDate) {
63
- props.onChange(tempStartAndEndDate);
113
+ commitSelection(tempStartAndEndDate);
64
114
  }
65
- setShowTimeSelectModal(false);
66
- setTempStartAndEndDate(null);
67
115
  }}
68
116
  >
69
- <div className="mt-5">
117
+ <div className="mt-3">
70
118
  <RangeStartAndEndDateEdit
71
119
  value={tempStartAndEndDate || undefined}
72
120
  onChange={(startAndEndDate: RangeStartAndEndDateTime) => {
73
121
  setTempStartAndEndDate(startAndEndDate);
74
122
  }}
123
+ onApply={(startAndEndDate: RangeStartAndEndDateTime) => {
124
+ // Quick ranges apply immediately and close the modal.
125
+ commitSelection(startAndEndDate);
126
+ }}
75
127
  />
76
128
  </div>
77
129
  </Modal>
@@ -462,7 +462,12 @@ const EditionLabel: FunctionComponent<ComponentProps> = (
462
462
  type="button"
463
463
  onClick={openDialog}
464
464
  className={`${pillClassName} ${props.className ? props.className : ""}`}
465
- aria-label={`${editionName} details`}
465
+ /*
466
+ * Accessible name must include the visible text (WCAG 2.5.3 Label in
467
+ * Name) so voice-control users can activate it by the words they see
468
+ * ("{editionName}" and "{ctaLabel}", e.g. "Learn more").
469
+ */
470
+ aria-label={`${editionName}, ${ctaLabel}`}
466
471
  >
467
472
  {showAlertedPill && (
468
473
  <Icon
@@ -66,7 +66,7 @@ const Footer: FunctionComponent<ComponentProps> = (
66
66
  {/* Copyright: Show on mobile, hide on larger screens unless specified */}
67
67
  <div className="mt-5 md:order-1 md:mt-0 block md:hidden lg:block">
68
68
  {props.copyright && (
69
- <p className="text-center text-sm text-gray-400">
69
+ <p className="text-center text-sm text-gray-500">
70
70
  &copy; {props.copyright}
71
71
  </p>
72
72
  )}
@@ -33,7 +33,14 @@ const ChartContainer: FunctionComponent<ComponentProps> = (
33
33
  }, []);
34
34
 
35
35
  return (
36
- <div ref={divRef} className={"w-full"}>
36
+ /*
37
+ * `isolate` creates a new stacking context so the chart's internal
38
+ * z-indexes (e.g. a highlighted/matched bar uses z-30, see Bar/Index.tsx)
39
+ * stay trapped inside the chart. Without it those bars paint above
40
+ * page-level overlays like the span-details SideOver (z-10), making a
41
+ * matched span "show above" the sidebar.
42
+ */
43
+ <div ref={divRef} className={"w-full"} style={{ isolation: "isolate" }}>
37
44
  {props.children}
38
45
  </div>
39
46
  );
@@ -35,6 +35,14 @@ export interface ComponentProps {
35
35
  onClick?: (() => void) | undefined;
36
36
  type?: IconType | undefined;
37
37
  style?: React.CSSProperties | undefined;
38
+ /*
39
+ * When the icon conveys meaning on its own (e.g. a standalone status or
40
+ * severity indicator that is not described by adjacent text), pass an
41
+ * ariaLabel so the icon is exposed to assistive technology as an image
42
+ * with an accessible name (WCAG 1.1.1). When omitted, the icon is treated
43
+ * as decorative and hidden from assistive technology.
44
+ */
45
+ ariaLabel?: string | undefined;
38
46
  "data-testid"?: string;
39
47
  }
40
48
  const Icon: FunctionComponent<ComponentProps> = ({
@@ -46,6 +54,7 @@ const Icon: FunctionComponent<ComponentProps> = ({
46
54
  onClick,
47
55
  type,
48
56
  style,
57
+ ariaLabel,
49
58
  "data-testid": dataTestId,
50
59
  }: ComponentProps): ReactElement => {
51
60
  let sizeClassName: string = "";
@@ -113,7 +122,10 @@ const Icon: FunctionComponent<ComponentProps> = ({
113
122
  | undefined,
114
123
  ): ReactElement => {
115
124
  return (
116
- <div role="icon">
125
+ <div
126
+ role={ariaLabel ? "img" : undefined}
127
+ aria-label={ariaLabel || undefined}
128
+ >
117
129
  <svg
118
130
  onClick={() => {
119
131
  if (onClick) {
@@ -41,7 +41,12 @@ const Image: FunctionComponent<ComponentProps> = (
41
41
  props.onClick?.();
42
42
  }}
43
43
  data-testid={props["data-testid"]}
44
- alt={props.alt}
44
+ /*
45
+ * Always render an alt attribute so the <img> is never missing it
46
+ * (WCAG 1.1.1). Callers should pass a descriptive alt for meaningful
47
+ * images (e.g. avatars); an empty string marks the image decorative.
48
+ */
49
+ alt={props.alt ?? ""}
45
50
  src={url}
46
51
  height={props.height}
47
52
  className={props.className}
@@ -33,6 +33,28 @@ const Link: FunctionComponent<ComponentProps> = (
33
33
  children = props.children;
34
34
  }
35
35
 
36
+ /*
37
+ * Neither a destination nor an action: this is static text, not a link or a
38
+ * button. Render a <span> rather than an <a> with no href, which is
39
+ * ambiguous to assistive technology (is it a link, a button, or just text?
40
+ * WCAG 4.1.2 Name, Role, Value).
41
+ */
42
+ if (!props.to && !props.onClick) {
43
+ return (
44
+ <span
45
+ id={props.id}
46
+ className={`cursor-default ${props.className || ""}`}
47
+ onMouseOver={props.onMouseOver}
48
+ onMouseOut={props.onMouseOut}
49
+ onMouseLeave={props.onMouseLeave}
50
+ style={props.style}
51
+ title={props.title}
52
+ >
53
+ {children}
54
+ </span>
55
+ );
56
+ }
57
+
36
58
  const linkProps: JSONObject = {};
37
59
 
38
60
  /*
@@ -15,6 +15,12 @@ export interface ComponentProps {
15
15
  className?: string | undefined;
16
16
  hideHeader?: boolean | undefined;
17
17
  makeTopSectionUnstick?: boolean | undefined;
18
+ /*
19
+ * Set when the consumer renders its own <main id="main-content"> landmark
20
+ * and skip link inside children (e.g. the status page), so this component
21
+ * does not add a second, nested main landmark or a duplicate skip link.
22
+ */
23
+ disableMainContentWrapper?: boolean | undefined;
18
24
  }
19
25
 
20
26
  const MasterPage: FunctionComponent<ComponentProps> = (
@@ -42,6 +48,19 @@ const MasterPage: FunctionComponent<ComponentProps> = (
42
48
  <React.Fragment>
43
49
  {isOnline && (
44
50
  <div className={props.className}>
51
+ {/*
52
+ * Skip link so keyboard and screen-reader users can bypass the
53
+ * repeated header/navigation and jump straight to the page content
54
+ * (WCAG 2.4.1 Bypass Blocks). Visually hidden until focused.
55
+ */}
56
+ {!props.disableMainContentWrapper && (
57
+ <a
58
+ href="#main-content"
59
+ className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-50 focus:rounded-md focus:bg-indigo-600 focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-white focus:shadow-lg"
60
+ >
61
+ Skip to main content
62
+ </a>
63
+ )}
45
64
  <div
46
65
  className={props.makeTopSectionUnstick ? "" : "sticky top-0 z-10"}
47
66
  >
@@ -53,7 +72,17 @@ const MasterPage: FunctionComponent<ComponentProps> = (
53
72
  />
54
73
  </div>
55
74
 
56
- {props.children}
75
+ {props.disableMainContentWrapper ? (
76
+ props.children
77
+ ) : (
78
+ <main
79
+ id="main-content"
80
+ tabIndex={-1}
81
+ className="focus:outline-none"
82
+ >
83
+ {props.children}
84
+ </main>
85
+ )}
57
86
 
58
87
  {props.footer && props.footer}
59
88
  </div>
@@ -40,6 +40,33 @@ const Page: FunctionComponent<ComponentProps> = (
40
40
  }
41
41
  }, [props.breadcrumbLinks]);
42
42
 
43
+ /*
44
+ * Give each page a unique, descriptive document title (WCAG 2.4.2 Page
45
+ * Titled). Without this, every page keeps the generic title set once at app
46
+ * startup (e.g. "OneUptime | Dashboard"), which does not describe the page.
47
+ * Prefer the breadcrumb trail (most specific page last) and fall back to the
48
+ * page title.
49
+ */
50
+ useEffect(() => {
51
+ const breadcrumbTitle: string | undefined =
52
+ props.breadcrumbLinks && props.breadcrumbLinks.length > 0
53
+ ? props.breadcrumbLinks
54
+ .map((link: Link) => {
55
+ return translateString(link.title);
56
+ })
57
+ .filter((value: string | undefined): value is string => {
58
+ return Boolean(value);
59
+ })
60
+ .join(" - ")
61
+ : undefined;
62
+
63
+ const pageTitle: string | undefined = breadcrumbTitle || translatedTitle;
64
+
65
+ if (pageTitle) {
66
+ document.title = `OneUptime | ${pageTitle}`;
67
+ }
68
+ }, [translatedTitle, props.breadcrumbLinks]);
69
+
43
70
  if (props.error) {
44
71
  return <ErrorMessage message={props.error} />;
45
72
  }
@@ -104,7 +131,7 @@ const Page: FunctionComponent<ComponentProps> = (
104
131
  )}
105
132
 
106
133
  {props.sideMenu && (
107
- <main className="mx-auto max-w-full pb-10">
134
+ <div className="mx-auto max-w-full pb-10">
108
135
  <div className="flex flex-col md:flex-row md:gap-4 lg:gap-5">
109
136
  {props.sideMenu}
110
137
 
@@ -117,7 +144,7 @@ const Page: FunctionComponent<ComponentProps> = (
117
144
  </div>
118
145
  )}
119
146
  </div>
120
- </main>
147
+ </div>
121
148
  )}
122
149
 
123
150
  {!props.sideMenu && !props.isLoading && props.children}