diff --git a/managed/ui/src/components/alerts/AlertConfiguration/AlertConfiguration.js b/managed/ui/src/components/alerts/AlertConfiguration/AlertConfiguration.js index 25d953085632..1e41c08fee43 100644 --- a/managed/ui/src/components/alerts/AlertConfiguration/AlertConfiguration.js +++ b/managed/ui/src/components/alerts/AlertConfiguration/AlertConfiguration.js @@ -16,6 +16,7 @@ import { AlertsList } from './AlertsList'; import CreateAlert from './CreateAlert'; import { getPromiseState } from '../../../utils/PromiseUtils'; import { AlertDestinationChannels } from './AlertDestinationChannels'; +import { MaintenanceWindow } from '../MaintenanceWindow'; export const AlertConfiguration = (props) => { const [listView, setListView] = useState(false); @@ -156,6 +157,17 @@ export const AlertConfiguration = (props) => { {...props} /> + + Maintenance Windows + + } + unmountOnExit + > + + ); diff --git a/managed/ui/src/components/alerts/MaintenanceWindow/CautionIcon.tsx b/managed/ui/src/components/alerts/MaintenanceWindow/CautionIcon.tsx new file mode 100644 index 000000000000..d04bbe1f5316 --- /dev/null +++ b/managed/ui/src/components/alerts/MaintenanceWindow/CautionIcon.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +const CautionIcon = () => { + return ( + + + + ); +}; + +export default CautionIcon; diff --git a/managed/ui/src/components/alerts/MaintenanceWindow/CreateMaintenanceWindow.scss b/managed/ui/src/components/alerts/MaintenanceWindow/CreateMaintenanceWindow.scss new file mode 100644 index 000000000000..06ddb5b69201 --- /dev/null +++ b/managed/ui/src/components/alerts/MaintenanceWindow/CreateMaintenanceWindow.scss @@ -0,0 +1,70 @@ +.create-maintenance-window { + div.rw-datetime-picker { + margin-left: 0 !important; + } + + div.rw-calendar-header { + display: flex; + align-items: center; + + button.rw-calendar-btn-right, + button.rw-calendar-btn-left:hover { + background: #fff !important; + color: #000 !important; + } + } + + .time-label { + margin-bottom: 5px; + } + + .field-error { + color: #a94442; + } + + .row { + margin-bottom: 16px; + } + + label.btn-group-radio, + .form-item-custom-label, + .form-item-label { + font-weight: 100 !important; + } + + .alert-notice { + padding: 8px; + border: 1px solid #e5e5e6; + border-radius: 8px; + display: flex; + align-items: center; + margin-top: 5px; + color: #333333; + + .icon { + margin-right: 8px; + margin-top: 5px; + } + } + + .action-btns-margin { + margin-bottom: 71px; + } + + .maintenance-action-button-container { + position: absolute !important; + left: 0; + right: 0; + bottom: 0; + min-height: 30px; + padding: 14px 12px; + border-top: 1px solid #ececea; + background-color: #f6f6f5; + margin-bottom: 0 !important; + + .btn { + margin: 0 6px; + min-width: 120px !important; + } + } +} diff --git a/managed/ui/src/components/alerts/MaintenanceWindow/CreateMaintenanceWindow.tsx b/managed/ui/src/components/alerts/MaintenanceWindow/CreateMaintenanceWindow.tsx new file mode 100644 index 000000000000..d2aaaf12904f --- /dev/null +++ b/managed/ui/src/components/alerts/MaintenanceWindow/CreateMaintenanceWindow.tsx @@ -0,0 +1,293 @@ +import { Field, Form, Formik } from 'formik'; +import React, { ChangeEvent, FC } from 'react'; +import { Col, Row } from 'react-bootstrap'; +import { + YBButton, + YBControlledTextInput, + YBMultiSelectWithLabel, + YBTextArea +} from '../../common/forms/fields'; +import moment from 'moment'; +import '../../metrics/CustomDatePicker/CustomDatePicker.scss'; +import './CreateMaintenanceWindow.scss'; +import CautionIcon from './CautionIcon'; +import * as Yup from 'yup'; +import { useMutation } from 'react-query'; +import { + convertUTCStringToDate, + createMaintenanceWindow, + formatDateToUTC, + MaintenanceWindowSchema, + updateMaintenanceWindow +} from '.'; +import { toast } from 'react-toastify'; + +const reactWidgets = require('react-widgets'); +const momentLocalizer = require('react-widgets-moment'); +require('react-widgets/dist/css/react-widgets.css'); + +const { DateTimePicker } = reactWidgets; +momentLocalizer(moment); + +interface CreateMaintenanceWindowProps { + universeList: Array<{ universeUUID: string; name: string }>; + showListView: () => void; + selectedWindow: MaintenanceWindowSchema | null; +} + +enum TARGET_OPTIONS { + ALL = 'all', + SELECTED = 'selected' +} + +const targetOptions = [ + { label: 'All Universes', value: TARGET_OPTIONS.ALL }, + { label: 'Selected Universes', value: TARGET_OPTIONS.SELECTED } +]; + +const initialValues = { + target: TARGET_OPTIONS.ALL, + selectedUniverse: [] +}; + +const DATE_FORMAT = 'YYYY-DD-MMMM'; + +const validationSchema = Yup.object().shape({ + name: Yup.string().required('Enter name'), + description: Yup.string().required('Enter description'), + startTime: Yup.string().required('Enter start time'), + endTime: Yup.string().required('Enter end time'), + target: Yup.string().required('select a target'), + selectedUniverse: Yup.array().when('target', { + is: TARGET_OPTIONS.SELECTED, + then: Yup.array().min(1, 'atleast one universe has to be selected') + }) +}); + +/** + * Create Maintenance Window Component + * @param universeList List of universes + * @param showListView switch back to list view + * @param selectedWindow current selected window + * @returns + */ +export const CreateMaintenanceWindow: FC = ({ + universeList, + showListView, + selectedWindow +}) => { + const createWindow = useMutation( + (values: MaintenanceWindowSchema) => { + return createMaintenanceWindow({ + ...values, + alertConfigurationFilter: { + targetType: 'UNIVERSE', + target: { + all: values['target'] === TARGET_OPTIONS.ALL, + uuids: values['selectedUniverse'].map((universe: any) => universe.value) + } + } + }); + }, + { + onSuccess: () => { + toast.success('Maintenance Window created sucessfully!'); + showListView(); + } + } + ); + + const updateWindow = useMutation( + (values: MaintenanceWindowSchema) => { + return updateMaintenanceWindow({ + ...values, + alertConfigurationFilter: { + targetType: 'UNIVERSE', + target: { + all: values['target'] === TARGET_OPTIONS.ALL, + uuids: values['selectedUniverse'].map((universe: any) => universe.value) + } + } + }); + }, + { + onSuccess: () => { + toast.success('Maintenance Window updated sucessfully!'); + showListView(); + } + } + ); + + const universes = universeList.map((universe) => { + return { label: universe.name, value: universe.universeUUID }; + }); + + const findUniverseNamesByUUIDs = (uuids: string[]) => { + return universeList + .filter((universe) => uuids.includes(universe.universeUUID)) + .map((universe) => { + return { label: universe.name, value: universe.universeUUID }; + }); + }; + + /** + * prepares initial values for formik from maintenanceWindowsSchema + * @returns map of values or null + */ + const getInitialValues = () => { + if (!selectedWindow) return initialValues; + const selectedUniverse = findUniverseNamesByUUIDs( + selectedWindow?.alertConfigurationFilter.target.uuids + ); + return { + ...selectedWindow, + target: selectedWindow?.alertConfigurationFilter.target.all + ? TARGET_OPTIONS.ALL + : TARGET_OPTIONS.SELECTED, + selectedUniverse + }; + }; + + return ( + + selectedWindow === null + ? createWindow.mutateAsync(values as MaintenanceWindowSchema) + : updateWindow.mutateAsync(values as MaintenanceWindowSchema) + } + validationSchema={validationSchema} + validateOnBlur={false} + > + {({ handleSubmit, setFieldValue, values, errors }) => ( + + + + ) => + setFieldValue('name' as never, event.target.value, false) + } + placeHolder="Enter window name" + val={values['name']} + /> + {errors['name']} + + + + + setFieldValue('description' as never, value, false) + }} + /> + {errors['description']} + + + + + Start Time + + setFieldValue('startTime' as never, formatDateToUTC(time), false) + } + defaultValue={ + values['startTime'] ? convertUTCStringToDate(values['startTime']) : null + } + /> + {errors['startTime']} + + + End Time + + setFieldValue('endTime' as never, formatDateToUTC(time), false) + } + defaultValue={values['endTime'] ? convertUTCStringToDate(values['endTime']) : null} + /> + {errors['endTime']} + + + + + Target + {targetOptions.map((target) => ( + + ) => + setFieldValue('target' as never, e.target.value, false) + } + type="radio" + value={target.value} + /> + {target.label} + + ))} + { + setFieldValue('selectedUniverse' as never, values ?? [], false); + } + }} + validate={false} + className={values['target'] !== 'selected' ? 'hide-field' : ''} + /> + {errors['selectedUniverse']} + + + + + Affected Alerts + + + + + All alerts will be snoozed during the defined time range. + + + + + + { + showListView(); + }} + /> + + + + + )} + + ); +}; diff --git a/managed/ui/src/components/alerts/MaintenanceWindow/IMainenanceWindow.ts b/managed/ui/src/components/alerts/MaintenanceWindow/IMainenanceWindow.ts new file mode 100644 index 000000000000..dc99567e107d --- /dev/null +++ b/managed/ui/src/components/alerts/MaintenanceWindow/IMainenanceWindow.ts @@ -0,0 +1,23 @@ +interface AlertConfigurationSchema { + targetType: 'UNIVERSE'; //only universe is supported right now + target: { + all: boolean; + uuids: string[]; + }; +} + +export enum MaintenanceWindowState { + FINISHED = 'FINISHED', + ACTIVE = 'ACTIVE', + PENDING = 'PENDING' +} +export interface MaintenanceWindowSchema { + uuid: string; + name: string; + description: string; + createTime: Date; + endTime: string; + startTime: string; + state: MaintenanceWindowState; + alertConfigurationFilter: AlertConfigurationSchema; +} diff --git a/managed/ui/src/components/alerts/MaintenanceWindow/MaintenanceWindow.tsx b/managed/ui/src/components/alerts/MaintenanceWindow/MaintenanceWindow.tsx new file mode 100644 index 000000000000..0339093f2d8f --- /dev/null +++ b/managed/ui/src/components/alerts/MaintenanceWindow/MaintenanceWindow.tsx @@ -0,0 +1,52 @@ +import React, { FC, useState } from 'react'; +import { useQuery } from 'react-query'; +import { MaintenanceWindowSchema } from '.'; +import { fetchUniversesList } from '../../../actions/xClusterReplication'; +import { YBLoading } from '../../common/indicators'; +import { CreateMaintenanceWindow } from './CreateMaintenanceWindow'; + +import { MaintenanceWindowsList } from './MaintenanceWindowsList'; + +enum VIEW_STATES { + CREATE, + LIST +} + +export const MaintenanceWindow: FC = () => { + const [currentView, setCurrentView] = useState(VIEW_STATES.LIST); + + const [selectedWindow, setSelectedWindow] = useState(null); + + const { data: universeList, isLoading: isUniverseListLoading } = useQuery(['universeList'], () => + fetchUniversesList().then((res) => res.data) + ); + + if (isUniverseListLoading) { + return ; + } + + if (currentView === VIEW_STATES.CREATE) { + return ( + { + setCurrentView(VIEW_STATES.LIST); + }} + selectedWindow={selectedWindow} + /> + ); + } + + return ( + { + setCurrentView(VIEW_STATES.CREATE); + }} + setSelectedWindow={(selectedWindow) => { + setSelectedWindow(selectedWindow); + setCurrentView(VIEW_STATES.CREATE); + }} + /> + ); +}; diff --git a/managed/ui/src/components/alerts/MaintenanceWindow/MaintenanceWindowAPI.ts b/managed/ui/src/components/alerts/MaintenanceWindow/MaintenanceWindowAPI.ts new file mode 100644 index 000000000000..34c3987f572f --- /dev/null +++ b/managed/ui/src/components/alerts/MaintenanceWindow/MaintenanceWindowAPI.ts @@ -0,0 +1,103 @@ +import axios from 'axios'; +import moment from 'moment'; +import { ROOT_URL } from '../../../config'; +import { MaintenanceWindowSchema } from './IMainenanceWindow'; + +const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'; + +/** + * get customer id + * @returns customerID + */ +const getCustomerId = (): string => { + const customerId = localStorage.getItem('customerId'); + return customerId || ''; +}; + +/** + * formate date to YYYY-MM-DD hh:mm:ss + * @param date date object to format + * @returns string + */ +export const formatDateToUTC = (date: Date) => { + return moment(date).utc().format(DATE_FORMAT); +}; + +/** + * Convert string(utc) to local time + * @param dateString utc time stamp to convert + * @returns Date + */ +export const convertUTCStringToDate = (dateString: string) => { + return moment.utc(dateString).local().toDate(); +}; + +/** + * Convert UTC string to moment + * @param dateString dateString to convert + * @returns moment + */ +export const convertUTCStringToMoment = (dateString: string) => { + return moment(convertUTCStringToDate(dateString)); +}; + +/** + * format UTC string to local timezone + * @param dateString date String to convert + * @returns return date in 'YYYY-MM-DD HH:mm:ss' format + */ +export const formatUTCStringToLocal = (dateString: string) => { + return moment(convertUTCStringToDate(dateString)).format(DATE_FORMAT); +}; +/** + * fetches list of maintenance windows + * @returns Maintenance Windows List + */ +export const getMaintenanceWindowList = (): Promise => { + const requestURL = `${ROOT_URL}/customers/${getCustomerId()}/maintenance_windows/list`; + return axios.post(requestURL, {}).then((res) => res.data); +}; + +/** + * Create maintenance Windows + * @param name name for the maintenance window + * @param startTime starting time of the maintenance windows + * @param endTime end time of the maintenance windows + * @returns api response + */ +export const createMaintenanceWindow = ( + payload: Pick< + MaintenanceWindowSchema, + 'name' | 'startTime' | 'endTime' | 'description' | 'alertConfigurationFilter' + > +): Promise => { + const customerUUID = getCustomerId(); + const requestURL = `${ROOT_URL}/customers/${customerUUID}/maintenance_windows`; + return axios + .post(requestURL, { + ...payload, + customerUUID + }) + .then((res) => res.data); +}; + +/** + * Delete the maintenance window + * @param windowUUID uuid of the maintenance windows + * @returns api response + */ +export const deleteMaintenanceWindow = (windowUUID: string) => { + const requestURL = `${ROOT_URL}/customers/${getCustomerId()}/maintenance_windows/${windowUUID}`; + return axios.delete(requestURL).then((res) => res.data); +}; + +/** + * Updated the maintenance window + * @param window maintenance window to edit + * @returns api response + */ +export const updateMaintenanceWindow = (window: MaintenanceWindowSchema) => { + const customerUUID = getCustomerId(); + const requestURL = `${ROOT_URL}/customers/${customerUUID}/maintenance_windows/${window.uuid}`; + return axios.put(requestURL, { ...window, customerUUID }).then((res) => res.data); +}; diff --git a/managed/ui/src/components/alerts/MaintenanceWindow/MaintenanceWindowsList.scss b/managed/ui/src/components/alerts/MaintenanceWindow/MaintenanceWindowsList.scss new file mode 100644 index 000000000000..763d45837d1f --- /dev/null +++ b/managed/ui/src/components/alerts/MaintenanceWindow/MaintenanceWindowsList.scss @@ -0,0 +1,72 @@ +.maintenance-windows { + .header { + display: flex; + align-items: center; + margin-bottom: 32px; + } + + .checkbox-label { + font-weight: normal; + } + + .state-tag { + text-transform: capitalize; + padding: 6px; + border-radius: 6px; + + &.ACTIVE { + color: #097345; + background: #dff0e3; + } + + &.FINISHED { + color: #4e5f6d; + background: #e9eef2; + } + + &.PENDING { + background: #e3e5ee; + color: #44518b; + } + } + + .extend-actions { + width: 90px !important; + } + + .actions-btn { + width: 50px !important; + border: 0; + margin-left: 10px !important; + font-weight: 500; + } + + .yb-actions-cell { + overflow: inherit !important; + + div { + overflow: inherit !important; + } + } + + .more-universe-list { + color: #ef5824; + text-decoration: underline; + cursor: pointer; + } + + #maintenance-window-table .react-bs-table-container td { + vertical-align: middle !important; + } +} + +div#more-universe-list { + background: #fff; + color: #333; + max-height: 350px; + overflow-y: auto; + + .universe-name:not(:last-child) { + margin-bottom: 24px; + } +} diff --git a/managed/ui/src/components/alerts/MaintenanceWindow/MaintenanceWindowsList.tsx b/managed/ui/src/components/alerts/MaintenanceWindow/MaintenanceWindowsList.tsx new file mode 100644 index 000000000000..e24e5135581c --- /dev/null +++ b/managed/ui/src/components/alerts/MaintenanceWindow/MaintenanceWindowsList.tsx @@ -0,0 +1,335 @@ +import React, { ChangeEvent, FC, useState } from 'react'; +import { Col, DropdownButton, MenuItem, OverlayTrigger, Popover, Row } from 'react-bootstrap'; +import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table'; +import { useMutation, useQuery, useQueryClient } from 'react-query'; +import { + convertUTCStringToMoment, + deleteMaintenanceWindow, + formatDateToUTC, + formatUTCStringToLocal, + getMaintenanceWindowList, + MaintenanceWindowSchema, + MaintenanceWindowState, + updateMaintenanceWindow +} from '.'; +import { YBButton, YBCheckBox } from '../../common/forms/fields'; +import { YBLoading } from '../../common/indicators'; +import { YBConfirmModal } from '../../modals'; + +import './MaintenanceWindowsList.scss'; + +/** + * Extend time frame options in minutes + */ +const extendTimeframes = { + '30 Minutes': 30, + '1 hour': 60, + '2 hours': 120, + '6 hours': 360 +}; + +interface MaintenanceWindowsListProps { + universeList: { universeUUID: string; name: string }[]; + showCreateView: () => void; + setSelectedWindow: (window: MaintenanceWindowSchema | null) => void; +} + +/** + * Calculates the difference between two dates + * @param startTime start time + * @param endtime end time + * @returns diff between the dates + */ +const calculateDuration = (startTime: string, endtime: string): string => { + const start = convertUTCStringToMoment(startTime); + const end = convertUTCStringToMoment(endtime); + + const totalDays = end.diff(start, 'days'); + const totalHours = end.diff(start, 'hours'); + const totalMinutes = end.diff(start, 'minutes'); + let duration = totalDays !== 0 ? `${totalDays} d ` : ''; + duration += totalHours % 24 !== 0 ? `${totalHours % 24} h ` : ''; + duration += totalMinutes % 60 !== 0 ? `${totalMinutes % 60} m` : ''; + return duration; +}; + +const getStatusTag = (status: MaintenanceWindowSchema['state']) => { + return {status.toLowerCase()}; +}; + +/** + * return the action dom for the maintenance window table + */ +const GetMaintenanceWindowActions = ({ + currentWindow, + setSelectedWindow +}: { + currentWindow: MaintenanceWindowSchema; + setSelectedWindow: MaintenanceWindowsListProps['setSelectedWindow']; +}) => { + const queryClient = useQueryClient(); + + const extendTime = useMutation( + ({ window, minutesToExtend }: { window: MaintenanceWindowSchema; minutesToExtend: number }) => { + const currentEndTime = convertUTCStringToMoment(window.endTime).add( + minutesToExtend, + 'minute' + ); + return updateMaintenanceWindow({ + ...window, + endTime: formatDateToUTC(currentEndTime.toDate()) + }); + }, + { + onSuccess: () => queryClient.invalidateQueries('maintenenceWindows') + } + ); + + const markAsCompleted = useMutation( + (window: MaintenanceWindowSchema) => + updateMaintenanceWindow({ ...window, endTime: formatDateToUTC(new Date()) }), + { + onSuccess: () => queryClient.invalidateQueries('maintenenceWindows') + } + ); + + const deleteWindow = useMutation((uuid: string) => deleteMaintenanceWindow(uuid), { + onSuccess: () => queryClient.invalidateQueries('maintenenceWindows') + }); + + const [visibleModal, setVisibleModal] = useState(null); + return ( + <> + {/* Extend Options */} + {currentWindow.state !== MaintenanceWindowState.FINISHED && ( + + {Object.keys(extendTimeframes).map((timeframe) => ( + { + extendTime.mutateAsync({ + window: currentWindow, + minutesToExtend: extendTimeframes[timeframe] + }); + }} + > + {timeframe} + + ))} + + )} + {/* Actions */} + + {currentWindow.state === MaintenanceWindowState.ACTIVE && ( + markAsCompleted.mutateAsync(currentWindow)}> + Mark as Completed + + )} + {currentWindow.state !== MaintenanceWindowState.FINISHED && ( + { + setSelectedWindow(currentWindow); + }} + > + Edit Window + + )} + + { + setVisibleModal(currentWindow?.uuid); + }} + > + Delete Window + + + deleteWindow.mutateAsync(currentWindow.uuid)} + currentModal={currentWindow?.uuid} + visibleModal={visibleModal} + hideConfirmModal={() => { + setVisibleModal(null); + }} + > + Are you sure you want to delete "{currentWindow?.name}" maintenance window? + + > + ); +}; + +const getTargetUniverse = (universeNames: string[]) => { + if (universeNames.length === 1) return universeNames[0]; + const popover = ( + + {universeNames.slice(1).map((name) => ( + + {name} + + ))} + + ); + return ( + <> + {universeNames[0]}, + + +{universeNames.length - 1} + + > + ); +}; + +export const MaintenanceWindowsList: FC = ({ + universeList, + showCreateView, + setSelectedWindow +}) => { + const { data, isFetching: isMaintenanceWindowsListFetching } = useQuery( + ['maintenenceWindows'], + () => getMaintenanceWindowList() + ); + + const [showCompletedWindows, setShowCompletedWindows] = useState(false); + + const maintenenceWindows = !showCompletedWindows + ? data?.filter((window) => window.state !== MaintenanceWindowState.FINISHED) + : data; + + const findUniverseNamesByUUIDs = (uuids: string[]) => { + return universeList + .filter((universe) => uuids.includes(universe.universeUUID)) + .map((universe) => universe.name); + }; + + return ( + + + + ) => + setShowCompletedWindows(e.target.checked) + }} + label={Show Completed Maintenance} + /> + + + { + setSelectedWindow(null); + showCreateView(); + }} + /> + + + + + {isMaintenanceWindowsListFetching && } + + {maintenenceWindows && ( + + + + name + + formatUTCStringToLocal(cell)} + > + Start Time + + formatUTCStringToLocal(cell)} + > + End Time + + { + if (row.alertConfigurationFilter?.target?.all) { + return 'ALL'; + } + const universes = findUniverseNamesByUUIDs( + row.alertConfigurationFilter.target.uuids + ); + // if the universe are deleted return empty string + return universes.length === 0 ? '' : getTargetUniverse(universes); + }} + > + Target Universe + + calculateDuration(row.startTime, row.endTime)} + > + Duration + + getStatusTag(cell)} + dataSort + > + Status + + ( + + )} + columnClassName="yb-actions-cell no-border" + > + Actions + + + )} + + + + ); +}; diff --git a/managed/ui/src/components/alerts/MaintenanceWindow/index.tsx b/managed/ui/src/components/alerts/MaintenanceWindow/index.tsx new file mode 100644 index 000000000000..5a4a375075d4 --- /dev/null +++ b/managed/ui/src/components/alerts/MaintenanceWindow/index.tsx @@ -0,0 +1,3 @@ +export * from './IMainenanceWindow'; +export * from './MaintenanceWindowAPI'; +export * from './MaintenanceWindow'; diff --git a/managed/ui/src/components/metrics/CustomDatePicker/CustomDatePicker.scss b/managed/ui/src/components/metrics/CustomDatePicker/CustomDatePicker.scss index bde6e618fe1c..9e746597cf70 100644 --- a/managed/ui/src/components/metrics/CustomDatePicker/CustomDatePicker.scss +++ b/managed/ui/src/components/metrics/CustomDatePicker/CustomDatePicker.scss @@ -11,179 +11,179 @@ display: none; } - div.rw-datetime-picker { - display: inline-block; - vertical-align: middle; - cursor: pointer; - margin-left: 8px; - margin-right: 10px; + .btn { + margin-bottom: 0px; + } +} - &:first-child { - margin-left: 0; - } +div.rw-datetime-picker { + display: inline-block; + vertical-align: middle; + cursor: pointer; + margin-left: 8px; + margin-right: 10px; - & > .rw-widget-container { - border-radius: 7px; - border-color: $YB_GRAY; - height: 42px; - right: auto; - min-width: 240px; - } + &:first-child { + margin-left: 0; + } - &.rw-state-focus > .rw-widget-container { - border-color: rgba($YB_ORANGE, 0.5); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba($YB_ORANGE, 0.2); - } + & > .rw-widget-container { + border-radius: 7px; + border-color: $YB_GRAY; + height: 42px; + right: auto; + min-width: 240px; + } - &.rw-has-both { - padding-right: 5em; - } + &.rw-state-focus > .rw-widget-container { + border-color: rgba($YB_ORANGE, 0.5); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba($YB_ORANGE, 0.2); + } - .rw-input { - border-bottom-left-radius: 7px; - border-top-left-radius: 7px; - } + &.rw-has-both { + padding-right: 5em; + } - .rw-select { - border-bottom-right-radius: 7px; - border-top-right-radius: 7px; - padding-left: 7px; - padding-right: 7px; - width: 5em; - vertical-align: middle; + .rw-input { + border-bottom-left-radius: 7px; + border-top-left-radius: 7px; + } - .rw-btn { - width: 1.9em; - color: $YB_VIOLET_TEXT; - } + .rw-select { + border-bottom-right-radius: 7px; + border-top-right-radius: 7px; + padding-left: 7px; + padding-right: 7px; + width: 5em; + vertical-align: middle; + + .rw-btn { + width: 1.9em; + color: $YB_VIOLET_TEXT; } + } - .rw-popup { - padding: 0; - border: 0; - background-color: #fff; - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.174); - border-radius: 7px; - margin-top: 5px; - - ul.rw-list, - .rw-selectlist { - & > li.rw-list-option { - border-color: transparent; - border-radius: 0; - padding: 3px 10px; - - &:hover { - background-color: $YB_GRAY; - } + .rw-popup { + padding: 0; + border: 0; + background-color: #fff; + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.174); + border-radius: 7px; + margin-top: 5px; + + ul.rw-list, + .rw-selectlist { + & > li.rw-list-option { + border-color: transparent; + border-radius: 0; + padding: 3px 10px; + + &:hover { + background-color: $YB_GRAY; } } } + } - .rw-calendar { - .rw-calendar-header { - padding: 10px 0px 10px 0px; + .rw-calendar { + .rw-calendar-header { + padding: 10px 0px 10px 0px; - button:focus { - box-shadow: none; - } + button:focus { + box-shadow: none; + } - .rw-btn { - color: $YB_VIOLET_TEXT; - } + .rw-btn { + color: $YB_VIOLET_TEXT; + } - .rw-btn[disabled], - .rw-state-disabled .rw-btn, - .rw-state-readonly .rw-btn { - color: #333; - } + .rw-btn[disabled], + .rw-state-disabled .rw-btn, + .rw-state-readonly .rw-btn { + color: #333; + } - .rw-calendar-btn-left { - border-radius: 50% 0 0 50%; - } + .rw-calendar-btn-left { + border-radius: 50% 0 0 50%; + } - .rw-calendar-btn-right { - border-radius: 0 50% 50% 0; - } + .rw-calendar-btn-right { + border-radius: 0 50% 50% 0; + } - .rw-calendar-btn-view { - float: left; - text-align: left; - font-weight: 500; - font-size: 1.15em; - margin: 0 0 0 1.2em; - width: 60%; - - &, - &:hover, - &:focus { - background-color: transparent; - } + .rw-calendar-btn-view { + float: left; + text-align: left; + font-weight: 500; + font-size: 1.15em; + margin: 0 0 0 1.2em; + width: 60%; + + &, + &:hover, + &:focus { + background-color: transparent; } + } - .rw-calendar-btn-left, - .rw-calendar-btn-right { - &:not(:disabled):hover { - background-color: $YB_VIOLET_TEXT; - color: #fff; - } + .rw-calendar-btn-left, + .rw-calendar-btn-right { + &:not(:disabled):hover { + background-color: $YB_VIOLET_TEXT; + color: #fff; + } - &:disabled { - color: #dce0e0; - } + &:disabled { + color: #dce0e0; } } + } - .rw-calendar-transition-group { - margin: 0 1em 1em 1em; + .rw-calendar-transition-group { + margin: 0 1em 1em 1em; - thead > tr { - & > th { - font-size: 0.875em; - padding-bottom: 10px; - border: none; - color: #8b9898; - } + thead > tr { + & > th { + font-size: 0.875em; + padding-bottom: 10px; + border: none; + color: #8b9898; } + } - tbody { - & > tr { - & > td { - font-size: 1.3rem; - border: none; + tbody { + & > tr { + & > td { + font-size: 1.3rem; + border: none; - &.rw-cell-off-range, - &.rw-state-disabled { - color: #dce0e0; - } + &.rw-cell-off-range, + &.rw-state-disabled { + color: #dce0e0; + } - &.rw-now { - color: $YB_ORANGE; - } + &.rw-now { + color: $YB_ORANGE; + } - &.rw-state-selected { - background-color: $YB_SIDEBAR_ACTIVE; - border: none; - color: #fff; + &.rw-state-selected { + background-color: $YB_SIDEBAR_ACTIVE; + border: none; + color: #fff; - &:hover { - background-color: $YB_VIOLET_TEXT; - } + &:hover { + background-color: $YB_VIOLET_TEXT; } } + } - &:last-child { - &.rw-empty-cell { - display: none; - } + &:last-child { + &.rw-empty-cell { + display: none; } } } } } } - - .btn { - margin-bottom: 0px; - } -} +} \ No newline at end of file