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

PMM-10764 - Time range selector for PITR backup #526

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { css, cx } from '@emotion/css';
import { TimePickerProps } from 'rc-time-picker';
import React, { FC, FormEvent, ReactNode, useCallback, useEffect, useState } from 'react';
import Calendar from 'react-calendar';
import Calendar, { CalendarProps } from 'react-calendar';
import { useMedia } from 'react-use';

import { dateTimeFormat, DateTime, dateTime, GrafanaTheme2, isDateTime } from '@grafana/data';
Expand All @@ -21,11 +22,14 @@ export interface Props {
label?: ReactNode;
/** Set the latest selectable date */
maxDate?: Date;
// @PERCONA
calendarProps?: CalendarProps;
timepickerProps?: TimePickerProps;
}

const stopPropagation = (event: React.MouseEvent<HTMLDivElement>) => event.stopPropagation();

export const DateTimePicker: FC<Props> = ({ date, maxDate, label, onChange }) => {
export const DateTimePicker: FC<Props> = ({ date, maxDate, label, onChange, calendarProps, timepickerProps }) => {
const [isOpen, setOpen] = useState(false);

const theme = useTheme2();
Expand Down Expand Up @@ -61,13 +65,22 @@ export const DateTimePicker: FC<Props> = ({ date, maxDate, label, onChange }) =>
isFullscreen={true}
onClose={() => setOpen(false)}
maxDate={maxDate}
calendarProps={calendarProps}
timepickerProps={timepickerProps}
/>
</ClickOutsideWrapper>
) : (
<Portal>
<ClickOutsideWrapper onClick={() => setOpen(false)}>
<div className={styles.modal} onClick={stopPropagation}>
<DateTimeCalendar date={date} onChange={onApply} isFullscreen={false} onClose={() => setOpen(false)} />
<DateTimeCalendar
date={date}
onChange={onApply}
isFullscreen={false}
onClose={() => setOpen(false)}
calendarProps={calendarProps}
timepickerProps={timepickerProps}
/>
</div>
<div className={containerStyles.backdrop} onClick={stopPropagation} />
</ClickOutsideWrapper>
Expand All @@ -84,6 +97,9 @@ interface DateTimeCalendarProps {
onClose: () => void;
isFullscreen: boolean;
maxDate?: Date;
// @PERCONA
calendarProps?: CalendarProps;
timepickerProps?: TimePickerProps;
}

interface InputProps {
Expand Down Expand Up @@ -161,7 +177,15 @@ const DateTimeInput: FC<InputProps> = ({ date, label, onChange, isFullscreen, on
);
};

const DateTimeCalendar: FC<DateTimeCalendarProps> = ({ date, onClose, onChange, isFullscreen, maxDate }) => {
const DateTimeCalendar: FC<DateTimeCalendarProps> = ({
date,
onClose,
onChange,
isFullscreen,
maxDate,
calendarProps,
timepickerProps,
}) => {
const calendarStyles = useStyles2(getBodyStyles);
const styles = useStyles2(getStyles);
const [internalDate, setInternalDate] = useState<Date>(() => {
Expand Down Expand Up @@ -190,6 +214,12 @@ const DateTimeCalendar: FC<DateTimeCalendarProps> = ({ date, onClose, onChange,
setInternalDate(date.toDate());
}, []);

useEffect(() => {
if (date?.isValid()) {
setInternalDate(date.toDate());
}
}, [date]);

return (
<div className={cx(styles.container, { [styles.fullScreen]: isFullscreen })} onClick={stopPropagation}>
<Calendar
Expand All @@ -205,9 +235,15 @@ const DateTimeCalendar: FC<DateTimeCalendarProps> = ({ date, onClose, onChange,
className={calendarStyles.body}
tileClassName={calendarStyles.title}
maxDate={maxDate}
{...calendarProps}
/>
<div className={styles.time}>
<TimeOfDayPicker showSeconds={true} onChange={onChangeTime} value={dateTime(internalDate)} />
<TimeOfDayPicker
showSeconds={true}
onChange={onChangeTime}
value={dateTime(internalDate)}
timepickerProps={timepickerProps}
/>
</div>
<HorizontalGroup>
<Button type="button" onClick={() => onChange(dateTime(internalDate))}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { css, cx } from '@emotion/css';
import RcTimePicker from 'rc-time-picker';
import RcTimePicker, { TimePickerProps } from 'rc-time-picker';
import React, { FC } from 'react';

import { dateTime, DateTime, dateTimeAsMoment, GrafanaTheme } from '@grafana/data';
Expand All @@ -18,6 +18,7 @@ export interface Props {
minuteStep?: number;
size?: FormInputSize;
disabled?: boolean;
timepickerProps?: TimePickerProps;
}

export const TimeOfDayPicker: FC<Props> = ({
Expand All @@ -28,6 +29,7 @@ export const TimeOfDayPicker: FC<Props> = ({
value,
size = 'auto',
disabled,
timepickerProps,
}) => {
const styles = useStyles(getStyles);

Expand All @@ -44,6 +46,7 @@ export const TimeOfDayPicker: FC<Props> = ({
minuteStep={minuteStep}
inputIcon={<Caret wrapperStyle={styles.caretWrapper} />}
disabled={disabled}
{...timepickerProps}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ export const getBodyStyles = (theme: GrafanaTheme2) => {
display: flex;
}

.react-calendar__month-view__days__day:disabled,
.react-calendar__year-view__months__month:disabled,
.react-calendar__decade-view__years__year:disabled {
color: #3f4249;
}

.react-calendar__navigation__label,
.react-calendar__navigation__arrow,
.react-calendar__navigation {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export const LIST_ARTIFACTS_CANCEL_TOKEN = 'listArtifacts';
export const BACKUP_CANCEL_TOKEN = 'backup';
export const RESTORE_CANCEL_TOKEN = 'restore';
export const LOGS_LIMIT = 50;
export const DAY_FORMAT = 'YYYY[-]MM[-]DD';
export const HOUR_FORMAT = 'HH[:]mm[:]ss';
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { CancelToken } from 'axios';

import { SelectableValue } from '@grafana/data';
import { DBServiceList, ServiceListPayload } from 'app/percona/inventory/Inventory.types';
import { api } from 'app/percona/shared/helpers/api';

import { BackupLogResponse, BackupLogs, DataModel } from '../../Backup.types';

import { Backup, BackupResponse } from './BackupInventory.types';
import { Backup, BackupResponse, Timeranges, TimerangesResponse } from './BackupInventory.types';
import { formatDate } from './BackupInventory.utils';

const BASE_URL = '/v1/management/backup';

Expand Down Expand Up @@ -40,12 +42,22 @@ export const BackupInventoryService = {
})
);
},
async restore(serviceId: string, artifactId: string, token?: CancelToken) {
async listPitrTimeranges(artifactId: string): Promise<Array<SelectableValue<Timeranges>>> {
const { timeranges = [] } = await api.post<TimerangesResponse, Object>(`${BASE_URL}/Artifacts/ListPITRTimeranges`, {
artifact_id: artifactId,
});
return timeranges.map((value) => ({
label: `${formatDate(value.start_timestamp)} / ${formatDate(value.end_timestamp)}`,
value: { startTimestamp: value.start_timestamp, endTimestamp: value.end_timestamp },
}));
},
async restore(serviceId: string, artifactId: string, pitrTimestamp?: string, token?: CancelToken) {
return api.post(
`${BASE_URL}/Backups/Restore`,
{
service_id: serviceId,
artifact_id: artifactId,
pitr_timestamp: pitrTimestamp,
},
false,
token
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,9 @@ export const BackupInventory: FC = () => {
setLogsModalVisible(false);
};

const handleRestore = async (serviceId: string, artifactId: string) => {
const handleRestore = async (serviceId: string, artifactId: string, pitrTimestamp?: string) => {
try {
await BackupInventoryService.restore(serviceId, artifactId, generateToken(RESTORE_CANCEL_TOKEN));
await BackupInventoryService.restore(serviceId, artifactId, pitrTimestamp, generateToken(RESTORE_CANCEL_TOKEN));
setRestoreErrors([]);
setRestoreModalVisible(false);
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,17 @@ export interface RawBackup {
export interface BackupResponse {
artifacts: RawBackup[];
}

export interface RawTimeranges {
start_timestamp: string;
end_timestamp: string;
}

export interface Timeranges {
startTimestamp: string;
endTimestamp: string;
}

export interface TimerangesResponse {
timeranges: RawTimeranges[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import moment from 'moment/moment';

import { DAY_FORMAT, HOUR_FORMAT } from './BackupInventory.constants';

export const formatDate = (value: string) => {
return moment(value).format(DAY_FORMAT + ' ' + HOUR_FORMAT);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const Messages = {
title: 'Restore from backup',
serviceSelection: 'Service selection',
timeRange: 'Time range',
vendor: 'Vendor',
serviceName: 'Service name',
dataModel: 'Data model',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,19 @@
import { css } from '@emotion/css';

import { GrafanaTheme } from '@grafana/data';

export const getStyles = ({ palette, typography, spacing }: GrafanaTheme) => ({
formHalvesContainer: css`
display: flex;
margin-bottom: ${spacing.formInputMargin};
import { GrafanaTheme2 } from '@grafana/data';

export const getStyles = ({ v1: { palette, typography, spacing } }: GrafanaTheme2) => ({
modalWrapper: css`
display: grid;
grid-template-columns: 1fr 1fr;
justify-content: center;
align-items: center;
gap: 0px ${spacing.sm};
& > div {
flex: 0 1 50%;

&:first-child {
padding-right: ${spacing.md};
}

&:last-child {
padding-left: ${spacing.md};
}
height: 100%;
}
`,
radioGroup: css`
& > div:nth-last-of-type(2) {
flex-wrap: nowrap;
}

& input[type='radio'] + label {
height: auto;
white-space: nowrap;
Expand Down
Loading