From 9e91375632c7b61564bd59e3ce2bbd3a4c39edde Mon Sep 17 00:00:00 2001 From: Raj Kamal Singh <1133322+rkssisodiya@users.noreply.github.com> Date: Sun, 8 Oct 2023 14:49:16 +0530 Subject: [PATCH] Logs pipeline editor - filter preview (#3683) * feat: get started with Logs Filter Preview * chore: rename PipelineFilterPreview -> PipelineFilterSummary * chore: initial styles for pipeline filter preview * feat: wire up logs fetching for pipeline filter preview * feat: show empty preview if filter is empty * feat: get logs preview table display started * feat: use simple div + style based display for logs preview * feat: log preview item expand action * feat: move preview below filter and make filter last i/p in pipeline form * feat: add duration selector for logs filter preview * feat: add matched logs count to pipeline filter preview * chore: reorganize preview logs list into its own file * chore: cleanup * chore: revert type export from useGetQueryRange.ts * chore: get all tests passing * chore: address review comments: import cloneDeep directly * chore: address review comments: avoid inline handler func, return JSX.Element | null * chore: address review comments: move preview interval selector helper into its own folder * chore: address feedback: fix cloneDeep import * chore: address feedback: avoid inline handler and remove eslint supression --- .../index.tsx} | 35 +++++++-- .../FormFields/FilterInput/styles.scss | 3 + .../Preview/LogsFilterPreview/index.tsx | 43 +++++++++++ .../Preview/LogsFilterPreview/styles.scss | 18 +++++ .../Preview/components/LogsList/index.tsx | 52 +++++++++++++ .../Preview/components/LogsList/styles.scss | 46 +++++++++++ .../components/LogsCountInInterval/index.tsx | 55 ++++++++++++++ .../LogsCountInInterval/styles.scss | 3 + .../PreviewIntervalSelector/index.tsx | 45 +++++++++++ .../PreviewIntervalSelector/styles.scss | 4 + .../Preview/components/SampleLogs/index.tsx | 76 +++++++++++++++++++ .../Preview/components/SampleLogs/styles.scss | 7 ++ .../index.tsx | 8 +- .../styles.scss | 0 .../TableComponents/index.tsx | 4 +- .../PipelinePage/PipelineListsView/config.ts | 16 ++-- .../TopNav/DateTimeSelection/config.ts | 6 +- 17 files changed, 398 insertions(+), 23 deletions(-) rename frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/{FilterInput.tsx => FilterInput/index.tsx} (64%) create mode 100644 frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/FilterInput/styles.scss create mode 100644 frontend/src/container/PipelinePage/PipelineListsView/Preview/LogsFilterPreview/index.tsx create mode 100644 frontend/src/container/PipelinePage/PipelineListsView/Preview/LogsFilterPreview/styles.scss create mode 100644 frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx create mode 100644 frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/styles.scss create mode 100644 frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/components/LogsCountInInterval/index.tsx create mode 100644 frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/components/LogsCountInInterval/styles.scss create mode 100644 frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/index.tsx create mode 100644 frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/styles.scss create mode 100644 frontend/src/container/PipelinePage/PipelineListsView/Preview/components/SampleLogs/index.tsx create mode 100644 frontend/src/container/PipelinePage/PipelineListsView/Preview/components/SampleLogs/styles.scss rename frontend/src/container/PipelinePage/PipelineListsView/TableComponents/{PipelineFilterPreview => PipelineFilterSummary}/index.tsx (72%) rename frontend/src/container/PipelinePage/PipelineListsView/TableComponents/{PipelineFilterPreview => PipelineFilterSummary}/styles.scss (100%) diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/FilterInput.tsx b/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/FilterInput/index.tsx similarity index 64% rename from frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/FilterInput.tsx rename to frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/FilterInput/index.tsx index ef6b93f11c..94466aa197 100644 --- a/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/FilterInput.tsx +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/FilterInput/index.tsx @@ -1,3 +1,5 @@ +import './styles.scss'; + import { Form } from 'antd'; import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder'; import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch'; @@ -5,9 +7,10 @@ import isEqual from 'lodash-es/isEqual'; import { useTranslation } from 'react-i18next'; import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; -import { ProcessorFormField } from '../../AddNewProcessor/config'; -import { formValidationRules } from '../../config'; -import { FormLabelStyle } from '../styles'; +import { ProcessorFormField } from '../../../AddNewProcessor/config'; +import { formValidationRules } from '../../../config'; +import LogsFilterPreview from '../../../Preview/LogsFilterPreview'; +import { FormLabelStyle } from '../../styles'; function TagFilterInput({ value, @@ -41,9 +44,27 @@ interface TagFilterInputProps { placeholder: string; } +function TagFilterInputWithLogsResultPreview({ + value, + onChange, + placeholder, +}: TagFilterInputProps): JSX.Element { + return ( + <> + +
+ +
+ + ); +} + function FilterInput({ fieldData }: FilterInputProps): JSX.Element { const { t } = useTranslation('pipeline'); - return ( - {/* Antd form will supply value and onChange to here. + {/* Antd form will supply value and onChange here. // @ts-ignore */} - + ); } diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/FilterInput/styles.scss b/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/FilterInput/styles.scss new file mode 100644 index 0000000000..8508da2799 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/FilterInput/styles.scss @@ -0,0 +1,3 @@ +.pipeline-filter-input-preview-container { + margin-top: 1rem; +} diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/LogsFilterPreview/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/Preview/LogsFilterPreview/index.tsx new file mode 100644 index 0000000000..c4bd7be438 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/LogsFilterPreview/index.tsx @@ -0,0 +1,43 @@ +import './styles.scss'; + +import { RelativeDurationOptions } from 'container/TopNav/DateTimeSelection/config'; +import { useState } from 'react'; +import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; + +import PreviewIntervalSelector from '../components/PreviewIntervalSelector'; +import SampleLogs from '../components/SampleLogs'; + +function LogsFilterPreview({ filter }: LogsFilterPreviewProps): JSX.Element { + const last1HourInterval = RelativeDurationOptions[3].value; + const [previewTimeInterval, setPreviewTimeInterval] = useState( + last1HourInterval, + ); + + const isEmptyFilter = (filter?.items?.length || 0) < 1; + + return ( +
+
+
Filtered Logs Preview
+ +
+
+ {isEmptyFilter ? ( +
Please select a filter
+ ) : ( + + )} +
+
+ ); +} + +interface LogsFilterPreviewProps { + filter: TagFilter; +} + +export default LogsFilterPreview; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/LogsFilterPreview/styles.scss b/frontend/src/container/PipelinePage/PipelineListsView/Preview/LogsFilterPreview/styles.scss new file mode 100644 index 0000000000..725a12e59a --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/LogsFilterPreview/styles.scss @@ -0,0 +1,18 @@ +.logs-filter-preview-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 8px; +} + +.logs-filter-preview-content { + position: relative; + display: flex; + justify-content: center; + align-items: center; + + width: 100%; + height: 8rem; + overflow: hidden; + border: 1px solid rgba(253, 253, 253, 0.12); +} diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx new file mode 100644 index 0000000000..122d5e2b3a --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/index.tsx @@ -0,0 +1,52 @@ +import './styles.scss'; + +import { ExpandAltOutlined } from '@ant-design/icons'; +import LogDetail from 'components/LogDetail'; +import dayjs from 'dayjs'; +import { useActiveLog } from 'hooks/logs/useActiveLog'; +import { ILog } from 'types/api/logs/log'; + +function LogsList({ logs }: LogsListProps): JSX.Element { + const { + activeLog, + onSetActiveLog, + onClearActiveLog, + onAddToQuery, + } = useActiveLog(); + + const makeLogDetailsHandler = (log: ILog) => (): void => onSetActiveLog(log); + + return ( +
+ {logs.map((log) => ( +
+
+ {dayjs(String(log.timestamp)).format('MMM DD HH:mm:ss.SSS')} +
+
{log.body}
+
+ +
+
+ ))} + +
+ ); +} + +interface LogsListProps { + logs: ILog[]; +} + +export default LogsList; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/styles.scss b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/styles.scss new file mode 100644 index 0000000000..80cec9ef28 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/LogsList/styles.scss @@ -0,0 +1,46 @@ +.logs-preview-list-container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: stretch; + + box-sizing: border-box; + padding: 0.25rem 0.5rem; +} + +.logs-preview-list-item { + width: 100%; + position: relative; + + display: flex; + justify-content: space-between; + align-items: center; + + flex-grow: 1; +} + +.logs-preview-list-item:not(:first-child) { + border-top: 1px solid rgba(253, 253, 253, 0.12); +} + +.logs-preview-list-item-timestamp { + margin-right: 0.75rem; + white-space: nowrap; +} + +.logs-preview-list-item-body { + flex-grow: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.logs-preview-list-item-expand{ + margin-left: 0.75rem; + color: #1677ff; + padding: 0.25rem 0.375rem; + cursor: pointer; + font-size: 12px; +} diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/components/LogsCountInInterval/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/components/LogsCountInInterval/index.tsx new file mode 100644 index 0000000000..63ee3ff3c0 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/components/LogsCountInInterval/index.tsx @@ -0,0 +1,55 @@ +import './styles.scss'; + +import { + initialFilters, + initialQueriesMap, + PANEL_TYPES, +} from 'constants/queryBuilder'; +import { Time } from 'container/TopNav/DateTimeSelection/config'; +import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; +import cloneDeep from 'lodash-es/cloneDeep'; +import { useMemo } from 'react'; +import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; +import { LogsAggregatorOperator } from 'types/common/queryBuilder'; + +function LogsCountInInterval({ + filter, + timeInterval, +}: LogsCountInIntervalProps): JSX.Element | null { + const query = useMemo(() => { + const q = cloneDeep(initialQueriesMap.logs); + q.builder.queryData[0] = { + ...q.builder.queryData[0], + filters: filter || initialFilters, + aggregateOperator: LogsAggregatorOperator.COUNT, + }; + return q; + }, [filter]); + + const result = useGetQueryRange({ + graphType: PANEL_TYPES.TABLE, + query, + selectedTime: 'GLOBAL_TIME', + globalSelectedInterval: timeInterval, + }); + + if (!result.isFetched) { + return null; + } + + const count = + result?.data?.payload?.data?.newResult?.data?.result?.[0]?.series?.[0] + ?.values?.[0]?.value; + return ( +
+ {count} matches in +
+ ); +} + +interface LogsCountInIntervalProps { + filter: TagFilter; + timeInterval: Time; +} + +export default LogsCountInInterval; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/components/LogsCountInInterval/styles.scss b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/components/LogsCountInInterval/styles.scss new file mode 100644 index 0000000000..2074e176a6 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/components/LogsCountInInterval/styles.scss @@ -0,0 +1,3 @@ +.logs-filter-preview-matched-logs-count { + margin-right: 0.5rem; +} diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/index.tsx new file mode 100644 index 0000000000..a40f1d0376 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/index.tsx @@ -0,0 +1,45 @@ +import './styles.scss'; + +import { Select } from 'antd'; +import { + RelativeDurationOptions, + Time, +} from 'container/TopNav/DateTimeSelection/config'; +import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; + +import LogsCountInInterval from './components/LogsCountInInterval'; + +function PreviewIntervalSelector({ + previewFilter, + value, + onChange, +}: PreviewIntervalSelectorProps): JSX.Element { + const onSelectInterval = (value: unknown): void => onChange(value as Time); + + const isEmptyFilter = (previewFilter?.items?.length || 0) < 1; + + return ( +
+ {!isEmptyFilter && ( + + )} +
+ +
+
+ ); +} + +interface PreviewIntervalSelectorProps { + value: Time; + onChange: (interval: Time) => void; + previewFilter: TagFilter; +} + +export default PreviewIntervalSelector; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/styles.scss b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/styles.scss new file mode 100644 index 0000000000..d2bc3347ea --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/PreviewIntervalSelector/styles.scss @@ -0,0 +1,4 @@ +.logs-filter-preview-time-interval-summary { + display: flex; + align-items: center; +} diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/SampleLogs/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/SampleLogs/index.tsx new file mode 100644 index 0000000000..82d7fba4fc --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/SampleLogs/index.tsx @@ -0,0 +1,76 @@ +import './styles.scss'; + +import { + initialFilters, + initialQueriesMap, + PANEL_TYPES, +} from 'constants/queryBuilder'; +import { Time } from 'container/TopNav/DateTimeSelection/config'; +import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; +import cloneDeep from 'lodash-es/cloneDeep'; +import { useMemo } from 'react'; +import { ILog } from 'types/api/logs/log'; +import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; +import { LogsAggregatorOperator } from 'types/common/queryBuilder'; + +import LogsList from '../LogsList'; + +function SampleLogs({ filter, timeInterval }: SampleLogsProps): JSX.Element { + const sampleLogsQuery = useMemo(() => { + const q = cloneDeep(initialQueriesMap.logs); + q.builder.queryData[0] = { + ...q.builder.queryData[0], + filters: filter || initialFilters, + aggregateOperator: LogsAggregatorOperator.NOOP, + orderBy: [{ columnName: 'timestamp', order: 'desc' }], + limit: 5, + }; + return q; + }, [filter]); + + const sampleLogsResponse = useGetQueryRange({ + graphType: PANEL_TYPES.LIST, + query: sampleLogsQuery, + selectedTime: 'GLOBAL_TIME', + globalSelectedInterval: timeInterval, + }); + + if (sampleLogsResponse?.isError) { + return ( +
+ could not fetch logs for filter +
+ ); + } + + if (sampleLogsResponse?.isFetching) { + return
Loading...
; + } + + if ((filter?.items?.length || 0) < 1) { + return ( +
Please select a filter
+ ); + } + + const logsList = + sampleLogsResponse?.data?.payload?.data?.newResult?.data?.result[0]?.list || + []; + + if (logsList.length < 1) { + return
No logs found
; + } + + const logs: ILog[] = logsList.map((item) => ({ + ...item.data, + timestamp: item.timestamp, + })); + return ; +} + +interface SampleLogsProps { + filter: TagFilter; + timeInterval: Time; +} + +export default SampleLogs; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/SampleLogs/styles.scss b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/SampleLogs/styles.scss new file mode 100644 index 0000000000..815a8ce45f --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/components/SampleLogs/styles.scss @@ -0,0 +1,7 @@ +.sample-logs-notice-container { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterPreview/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterSummary/index.tsx similarity index 72% rename from frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterPreview/index.tsx rename to frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterSummary/index.tsx index b33ed6c087..6217f168b0 100644 --- a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterPreview/index.tsx +++ b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterSummary/index.tsx @@ -3,9 +3,9 @@ import './styles.scss'; import { queryFilterTags } from 'hooks/queryBuilder/useTag'; import { PipelineData } from 'types/api/pipeline/def'; -function PipelineFilterPreview({ +function PipelineFilterSummary({ filter, -}: PipelineFilterPreviewProps): JSX.Element { +}: PipelineFilterSummaryProps): JSX.Element { return (
{queryFilterTags(filter).map((tag) => ( @@ -17,8 +17,8 @@ function PipelineFilterPreview({ ); } -interface PipelineFilterPreviewProps { +interface PipelineFilterSummaryProps { filter: PipelineData['filter']; } -export default PipelineFilterPreview; +export default PipelineFilterSummary; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterPreview/styles.scss b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterSummary/styles.scss similarity index 100% rename from frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterPreview/styles.scss rename to frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineFilterSummary/styles.scss diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/index.tsx index 25ec206566..8fc4b5b6eb 100644 --- a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/index.tsx +++ b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/index.tsx @@ -4,7 +4,7 @@ import { PipelineData, ProcessorData } from 'types/api/pipeline/def'; import { PipelineIndexIcon } from '../AddNewProcessor/styles'; import { ColumnDataStyle, ListDataStyle, ProcessorIndexIcon } from '../styles'; -import PipelineFilterPreview from './PipelineFilterPreview'; +import PipelineFilterSummary from './PipelineFilterSummary'; const componentMap: ComponentMap = { orderId: ({ record }) => {record}, @@ -15,7 +15,7 @@ const componentMap: ComponentMap = { ), id: ({ record }) => {record}, name: ({ record }) => {record}, - filter: ({ record }) => , + filter: ({ record }) => , }; function TableComponents({ diff --git a/frontend/src/container/PipelinePage/PipelineListsView/config.ts b/frontend/src/container/PipelinePage/PipelineListsView/config.ts index baecbb8d14..6fc56c48b5 100644 --- a/frontend/src/container/PipelinePage/PipelineListsView/config.ts +++ b/frontend/src/container/PipelinePage/PipelineListsView/config.ts @@ -14,25 +14,25 @@ import NameInput from './AddNewPipeline/FormFields/NameInput'; export const pipelineFields = [ { id: 1, - fieldName: 'Filter', - placeholder: 'pipeline_filter_placeholder', - name: 'filter', - component: FilterInput, - }, - { - id: 2, fieldName: 'Name', placeholder: 'pipeline_name_placeholder', name: 'name', component: NameInput, }, { - id: 4, + id: 2, fieldName: 'Description', placeholder: 'pipeline_description_placeholder', name: 'description', component: DescriptionTextArea, }, + { + id: 3, + fieldName: 'Filter', + placeholder: 'pipeline_filter_placeholder', + name: 'filter', + component: FilterInput, + }, ]; export const tagInputStyle: React.CSSProperties = { diff --git a/frontend/src/container/TopNav/DateTimeSelection/config.ts b/frontend/src/container/TopNav/DateTimeSelection/config.ts index 2aaf3e1c19..38c6c06611 100644 --- a/frontend/src/container/TopNav/DateTimeSelection/config.ts +++ b/frontend/src/container/TopNav/DateTimeSelection/config.ts @@ -41,7 +41,7 @@ export interface Option { label: string; } -export const ServiceMapOptions: Option[] = [ +export const RelativeDurationOptions: Option[] = [ { value: '5min', label: 'Last 5 min' }, { value: '15min', label: 'Last 15 min' }, { value: '30min', label: 'Last 30 min' }, @@ -53,7 +53,7 @@ export const ServiceMapOptions: Option[] = [ export const getDefaultOption = (route: string): Time => { if (route === ROUTES.SERVICE_MAP) { - return ServiceMapOptions[2].value; + return RelativeDurationOptions[2].value; } if (route === ROUTES.APPLICATION) { return Options[2].value; @@ -63,7 +63,7 @@ export const getDefaultOption = (route: string): Time => { export const getOptions = (routes: string): Option[] => { if (routes === ROUTES.SERVICE_MAP) { - return ServiceMapOptions; + return RelativeDurationOptions; } return Options; };