Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ui: add date range selector in stmts and txns pages #68831

Merged
merged 1 commit into from
Aug 28, 2021
Merged
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
39 changes: 39 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/fetchData.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2021 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import { propsToQueryString } from "./fetchData";

describe("fetchData functions", () => {
describe("propsToQueryString", () => {
it("creates query string from object", () => {
const obj = {
start: 100,
end: 200,
strParam: "hello",
bool: false,
};
const expected = "start=100&end=200&strParam=hello&bool=false";
const res = propsToQueryString(obj);
expect(res).toEqual(expected);
});

it("skips entries with nullish values", () => {
const obj = {
start: 100,
end: 200,
strParam: null as any,
hello: undefined as any,
};
const expected = "start=100&end=200";
const res = propsToQueryString(obj);
expect(res).toEqual(expected);
});
});
});
13 changes: 13 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/fetchData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { cockroach } from "@cockroachlabs/crdb-protobuf-client";
import { RequestError } from "../util";
import { getBasePath } from "./basePath";
import { stringify } from "querystring";

interface ProtoBuilder<
P extends ConstructorType,
Expand All @@ -29,6 +30,18 @@ export function toArrayBuffer(encodedRequest: Uint8Array): ArrayBuffer {
);
}

// propsToQueryString is a helper function that converts a set of object
// properties to a query string
// - keys with null or undefined values will be skipped
// - non-string values will be toString'd
maryliag marked this conversation as resolved.
Show resolved Hide resolved
export function propsToQueryString(props: { [k: string]: any }) {
const params = new URLSearchParams();
Object.entries(props).forEach(
([k, v]: [string, any]) => v != null && params.set(k, v.toString()),
);
return params.toString();
}

/**
* @param RespBuilder expects protobuf stub class to build decode response;
* @param path relative URL path for requested resource;
Expand Down
18 changes: 17 additions & 1 deletion pkg/ui/workspaces/cluster-ui/src/api/statementsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// licenses/APL.txt.

import { cockroach } from "@cockroachlabs/crdb-protobuf-client";
import { fetchData } from "src/api";
import { fetchData, propsToQueryString } from "src/api";

const STATEMENTS_PATH = "/_status/statements";

Expand All @@ -19,3 +19,19 @@ export const getStatements = (): Promise<cockroach.server.serverpb.StatementsRes
STATEMENTS_PATH,
);
};

export type StatementsRequest = cockroach.server.serverpb.StatementsRequest;

export const getCombinedStatements = (
req: StatementsRequest,
): Promise<cockroach.server.serverpb.StatementsResponse> => {
const queryStr = propsToQueryString({
start: req.start.toInt(),
end: req.end.toInt(),
combined: true,
});
return fetchData(
cockroach.server.serverpb.StatementsResponse,
`${STATEMENTS_PATH}?${queryStr}`,
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2021 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import moment, { Moment } from "moment";

export const start = moment.utc("2021.08.08");
export const end = moment.utc("2021.08.12");

export const allowedInterval: [Moment, Moment] = [start, end];
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
@import "../core/index.module";

.popup-container {
width: 506px;
height: 240px;

.label {
display: block;
margin-bottom: 1rem;
}

.popup-content {
:global(.ant-calendar-picker) {
width: 290px;
margin-right: 14px;
input {
padding-left: 34px;
}
}

:global(.ant-time-picker-icon),
:global(.ant-calendar-picker-icon) {
left: 12px;
}

:global(.ant-time-picker) {
width: 150px;
}
}

.popup-footer {
margin-top: 24px;
text-align: right;
button {
margin-left: 8px;
}
}

:global(.ant-popover-arrow) {
display: none;
}
}

.date-range-form {
width: 300px;
height: 40px;

:global(.ant-input-affix-wrapper) {
height: 40px;
&:hover {
:global(.ant-input:not(.ant-input-disabled)) {
border-color: $colors--primary-blue-3;
border-right-width: 2px !important;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2021 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import React from "react";
import { storiesOf } from "@storybook/react";
import { start, end, allowedInterval } from "./dateRange.fixtures";
import { DateRange } from "./index";

storiesOf("DateRange", module)
.add("default", () => (
<DateRange
allowedInterval={allowedInterval}
end={end}
onSubmit={() => {}}
start={start}
/>
))
.add("with invalid date range", () => (
<DateRange
allowedInterval={allowedInterval}
end={start}
onSubmit={() => {}}
start={end}
/>
));
190 changes: 190 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/dateRange/dateRange.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Copyright 2021 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import React, { useState } from "react";
import { Alert, DatePicker, Form, Input, Popover, TimePicker } from "antd";
import { Moment } from "moment";
import classNames from "classnames/bind";
import { Time as TimeIcon, ErrorCircleFilled } from "@cockroachlabs/icons";
import { Button } from "src/button";
import { Text, TextTypes } from "src/text";

import styles from "./dateRange.module.scss";

const cx = classNames.bind(styles);

function rangeToString(start: Moment, end: Moment): string {
const formatStr = "MMM D, H:mm";
const formatStrSameDay = "H:mm";

const isSameDay = start.isSame(end, "day");
maryliag marked this conversation as resolved.
Show resolved Hide resolved
return `${start.utc().format(formatStr)} - ${end
.utc()
.format(isSameDay ? formatStrSameDay : formatStr)} (UTC)`;
}

type DateRangeMenuProps = {
start: Moment;
end: Moment;
allowedInterval?: [Moment, Moment];
closeMenu: () => void;
onSubmit: (start: Moment, end: Moment) => void;
};

function DateRangeMenu({
start,
end,
allowedInterval,
closeMenu,
onSubmit,
}: DateRangeMenuProps): React.ReactElement {
const dateFormat = "MMMM D, YYYY";
const timeFormat = "H:mm [(UTC)]";
const [startMoment, setStartMoment] = useState<Moment>(start);
const [endMoment, setEndMoment] = useState<Moment>(end);

const onChangeStart = (m?: Moment) => {
m && setStartMoment(m);
};

const onChangeEnd = (m?: Moment) => {
m && setEndMoment(m);
};

const isDisabled = allowedInterval
? (date: Moment): boolean => {
return (
date.isBefore(allowedInterval[0]) || date.isAfter(allowedInterval[1])
);
}
: null;

const isValid = startMoment.isBefore(endMoment);

const onApply = (): void => {
onSubmit(startMoment, endMoment);
closeMenu();
};

return (
<div className={cx("popup-content")}>
<Text className={cx("label")} textType={TextTypes.CaptionStrong}>
From
</Text>
<DatePicker
disabledDate={isDisabled}
allowClear={false}
format={dateFormat}
onChange={onChangeStart}
suffixIcon={<TimeIcon />}
value={startMoment}
/>
<TimePicker
allowClear={false}
format={timeFormat}
onChange={onChangeStart}
suffixIcon={<span />}
value={startMoment}
/>
<Text className={cx("label")} textType={TextTypes.CaptionStrong}>
To
</Text>
<DatePicker
allowClear={false}
disabledDate={isDisabled}
format={dateFormat}
onChange={onChangeEnd}
suffixIcon={<TimeIcon />}
value={endMoment}
/>
<TimePicker
allowClear={false}
format={timeFormat}
onChange={onChangeEnd}
suffixIcon={<span />}
value={endMoment}
/>
{!isValid && (
<Alert
icon={<ErrorCircleFilled fill="#FF3B4E" />}
message="Date interval not valid"
type="error"
showIcon
/>
)}
<div className={cx("popup-footer")}>
<Button onClick={closeMenu} type="secondary" textAlign="center">
Cancel
</Button>
<Button
disabled={!isValid}
onClick={onApply}
type="primary"
textAlign="center"
>
Apply
</Button>
</div>
</div>
);
}

type DateRangeProps = {
start: Moment;
end: Moment;
allowedInterval?: [Moment, Moment];
onSubmit: (start: Moment, end: Moment) => void;
};

export function DateRange({
allowedInterval,
start,
end,
onSubmit,
}: DateRangeProps): React.ReactElement {
const [menuVisible, setMenuVisible] = useState<boolean>(false);
const displayStr = rangeToString(start, end);

const onVisibleChange = (visible: boolean): void => {
setMenuVisible(visible);
};

const closeMenu = (): void => {
setMenuVisible(false);
};

const menu = (
<DateRangeMenu
allowedInterval={allowedInterval}
start={start}
end={end}
closeMenu={closeMenu}
onSubmit={onSubmit}
/>
);

return (
<Form className={cx("date-range-form")}>
<Form.Item>
<Popover
destroyTooltipOnHide
content={menu}
overlayClassName={cx("popup-container")}
placement="bottomLeft"
visible={menuVisible}
onVisibleChange={onVisibleChange}
trigger="click"
>
<Input value={displayStr} prefix={<TimeIcon />} />
</Popover>
</Form.Item>
</Form>
);
}
Loading