diff --git a/packages/cubejs-playground/package.json b/packages/cubejs-playground/package.json index 6853f0bd09aee..05e24f20a7f2e 100644 --- a/packages/cubejs-playground/package.json +++ b/packages/cubejs-playground/package.json @@ -65,7 +65,7 @@ "devDependencies": { "@ant-design/compatible": "^1.0.1", "@ant-design/icons": "^5.3.5", - "@cube-dev/ui-kit": "0.37.3", + "@cube-dev/ui-kit": "0.37.4", "@cubejs-client/core": "^0.36.4", "@cubejs-client/react": "^0.36.4", "@types/flexsearch": "^0.7.3", diff --git a/packages/cubejs-playground/src/QueryBuilderV2/components/ChartRenderer.tsx b/packages/cubejs-playground/src/QueryBuilderV2/components/ChartRenderer.tsx index 4414802aad0f7..e2a7eee5906b3 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/components/ChartRenderer.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/components/ChartRenderer.tsx @@ -1,7 +1,12 @@ -import { ChartType, TimeDimensionGranularity } from '@cubejs-client/core'; +import { + ChartType, + TimeDimensionGranularity, + granularityFor, + minGranularityForIntervals, + isPredefinedGranularity, +} from '@cubejs-client/core'; import { UseCubeQueryResult } from '@cubejs-client/react'; import { Skeleton, Tag, tasty } from '@cube-dev/ui-kit'; -import formatDate from 'date-fns/format'; import { ComponentType, memo, useCallback, useMemo } from 'react'; import { Col, Row, Statistic, Table } from 'antd'; import { @@ -28,28 +33,10 @@ import { getChartColorByIndex, getChartSolidColorByIndex, } from '../utils/chart-colors'; +import { formatDateByGranularity, formatDateByPattern } from '../utils/index'; import { LocalError } from './LocalError'; -const FORMAT_MAP = { - second: 'HH:mm:ss, yyyy-LL-dd', - minute: 'HH:mm, yyyy-LL-dd', - hour: 'HH:00, yyyy-LL-dd', - day: 'yyyy-LL-dd', - week: "'W'w yyyy-LL-dd", - month: 'LLL yyyy', - quarter: 'QQQ yyyy', - year: 'yyyy', -}; - -export function formatDateByGranularity(timestamp: Date, granularity?: TimeDimensionGranularity) { - return formatDate(timestamp, FORMAT_MAP[granularity ?? 'second']); -} - -export function formatDateByPattern(timestamp: Date, format?: string) { - return formatDate(timestamp, format ?? FORMAT_MAP['second']); -} - function CustomDot(props: any) { const { cx, cy, fill } = props; @@ -113,7 +100,18 @@ function CartesianChart({ return (key as string).split('.').length === 3; } ) as string; - const granularity = granularityField?.split('.')[2]; + let granularity = granularityField?.split('.')[2]; + + if (!isPredefinedGranularity(granularity)) { + const granularityInfo = + resultSet?.loadResponse.results[0]?.annotation.timeDimensions[granularityField]?.granularity; + if (granularityInfo) { + granularity = minGranularityForIntervals( + granularityInfo.interval, + granularityInfo.offset || granularityFor(granularityInfo.origin) + ); + } + } const formatDate = useMemo(() => { if (dateFormat) { diff --git a/packages/cubejs-playground/src/QueryBuilderV2/components/GranularityListMember.tsx b/packages/cubejs-playground/src/QueryBuilderV2/components/GranularityListMember.tsx new file mode 100644 index 0000000000000..b9f356ca8d99f --- /dev/null +++ b/packages/cubejs-playground/src/QueryBuilderV2/components/GranularityListMember.tsx @@ -0,0 +1,56 @@ +import { CalendarEditIcon, CalendarIcon, Text, TooltipProvider } from '@cube-dev/ui-kit'; +import { useRef } from 'react'; + +import { useHasOverflow } from '../hooks/index'; +import { titleize } from '../utils/index'; + +import { ListMemberButton } from './ListMemberButton'; + +export interface GranularityListMemberProps { + name: string; + title?: string; + isCustom?: boolean; + isSelected: boolean; + onToggle: () => void; +} + +export function GranularityListMember(props: GranularityListMemberProps) { + const { name, title, isCustom, isSelected, onToggle } = props; + const textRef = useRef(null); + + const hasOverflow = useHasOverflow(textRef); + const isAutoTitle = titleize(name) === title; + + const button = ( + : } + data-member="timeDimension" + isSelected={isSelected} + onPress={onToggle} + > + + {name} + + + ); + + if (hasOverflow || (!isAutoTitle && isCustom)) { + return ( + + {name} +
+ {title} + + } + delay={1000} + placement="right" + > + {button} +
+ ); + } else { + return button; + } +} diff --git a/packages/cubejs-playground/src/QueryBuilderV2/components/ListMember.tsx b/packages/cubejs-playground/src/QueryBuilderV2/components/ListMember.tsx index 3c2db7af09d8a..bce1d54ef356a 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/components/ListMember.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/components/ListMember.tsx @@ -11,7 +11,7 @@ import { import { TCubeMeasure, TCubeDimension, TCubeSegment, Cube, MemberType } from '@cubejs-client/core'; import { PlusOutlined } from '@ant-design/icons'; -import { getTypeIcon } from '../utils'; +import { getTypeIcon, titleize } from '../utils'; import { PrimaryKeyIcon } from '../icons/PrimaryKeyIcon'; import { NonPublicIcon } from '../icons/NonPublicIcon'; import { ItemInfoIcon } from '../icons/ItemInfoIcon'; @@ -60,6 +60,8 @@ export function ListMember(props: ListMemberProps) { const description = member.description; const hasOverflow = useHasOverflow(textRef); + const isAutoTitle = titleize(member.name) === title; + const button = ( ); - return hasOverflow ? ( + return hasOverflow || !isAutoTitle ? ( - {name} + + {name} + +
+ {title} } delay={1000} diff --git a/packages/cubejs-playground/src/QueryBuilderV2/components/SidePanelCubeItem.tsx b/packages/cubejs-playground/src/QueryBuilderV2/components/SidePanelCubeItem.tsx index e0988049a9699..32be9da44b9a1 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/components/SidePanelCubeItem.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/components/SidePanelCubeItem.tsx @@ -16,6 +16,7 @@ import { ArrowIcon } from '../icons/ArrowIcon'; import { NonPublicIcon } from '../icons/NonPublicIcon'; import { ItemInfoIcon } from '../icons/ItemInfoIcon'; import { useHasOverflow, useFilteredMembers } from '../hooks'; +import { titleize } from '../utils/index'; import { ListMember } from './ListMember'; import { TimeListMember } from './TimeListMember'; @@ -366,6 +367,7 @@ export function SidePanelCubeItem({ }, [segments.join(','), query?.segments?.join(','), showMembers, mode, meta]); const hasOverflow = useHasOverflow(textRef); + const isAutoTitle = titleize(name) === title; const noVisibleMembers = !dimensions.length && !measures.length && !segments.length; @@ -468,12 +470,16 @@ export function SidePanelCubeItem({ return ( - {hasOverflow ? ( + {hasOverflow || !isAutoTitle ? ( - {name} + + {name} + +
+ {title} } placement="right" diff --git a/packages/cubejs-playground/src/QueryBuilderV2/components/TimeListMember.tsx b/packages/cubejs-playground/src/QueryBuilderV2/components/TimeListMember.tsx index d0794894f33b0..0dc08a5537fb8 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/components/TimeListMember.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/components/TimeListMember.tsx @@ -1,23 +1,17 @@ -import { useRef, useState } from 'react'; -import { - Action, - Flex, - Space, - Text, - TimeIcon, - CalendarIcon, - TooltipProvider, -} from '@cube-dev/ui-kit'; +import { useMemo, useRef, useState } from 'react'; +import { Flex, Space, Text, TimeIcon, TooltipProvider } from '@cube-dev/ui-kit'; import { Cube, TCubeDimension, TimeDimensionGranularity } from '@cubejs-client/core'; +import { GranularityListMember } from '@/modules/playground/components/QueryBuilder/components/GranularityListMember'; + import { ArrowIcon } from '../icons/ArrowIcon'; import { NonPublicIcon } from '../icons/NonPublicIcon'; import { ItemInfoIcon } from '../icons/ItemInfoIcon'; import { useHasOverflow } from '../hooks/has-overflow'; +import { titleize } from '../utils/index'; import { ListMemberButton } from './ListMemberButton'; import { FilterByMemberButton } from './FilterByMemberButton'; -import { MemberBadge } from './Badge'; import { FilteredLabel } from './FilteredLabel'; interface ListMemberProps { @@ -32,7 +26,7 @@ interface ListMemberProps { onToggleDataRange?: (name: string) => void; } -const GRANULARITIES: TimeDimensionGranularity[] = [ +const PREDEFINED_GRANULARITIES: TimeDimensionGranularity[] = [ 'second', 'minute', 'hour', @@ -66,13 +60,34 @@ export function TimeListMember(props: ListMemberProps) { // @ts-ignore const description = member.description; const isTimestampSelected = isSelected(); - const isGranularitySelectedList = GRANULARITIES.map((granularity) => isSelected(granularity)); - const selectedGranularity = GRANULARITIES.find((granularity) => isSelected(granularity)); - // const isGranularitySelected = !!isGranularitySelectedList.find((gran) => gran); + + const customGranularities = + member.type === 'time' && member.granularities ? member.granularities.map((g) => g.name) : []; + const customGranularitiesTitleMap = useMemo(() => { + return ( + member.type === 'time' && + member.granularities?.reduce( + (map, granularity) => { + map[granularity.name] = granularity.title; + + return map; + }, + {} as Record + ) + ); + }, [member.type === 'time' ? member.granularities : null]); + const memberGranularities = customGranularities.concat(PREDEFINED_GRANULARITIES); + const isGranularitySelectedMap: Record = {}; + memberGranularities.forEach((granularity) => { + isGranularitySelectedMap[granularity] = isSelected(granularity); + }); + const selectedGranularity = memberGranularities.find((granularity) => isSelected(granularity)); open = isCompact ? false : open; const hasOverflow = useHasOverflow(textRef); + const isAutoTitle = titleize(member.name) === title; + const button = ( {filterString ? : name} - {(isCompact || !open) && selectedGranularity ? ( - - { - onGranularityToggle(member.name, selectedGranularity); - }} - > - - {selectedGranularity} - - - - ) : null} + {description ? : undefined} @@ -130,9 +130,34 @@ export function TimeListMember(props: ListMemberProps) { ); + const granularityItems = (items: string[], isCustom?: boolean) => { + return items.map((granularity: string) => { + if ( + ((!open || isCompact) && !isGranularitySelectedMap[granularity]) || + !customGranularitiesTitleMap + ) { + return null; + } + + return ( + { + onGranularityToggle(member.name, granularity); + setOpen(false); + }} + /> + ); + }); + }; + return ( <> - {hasOverflow ? ( + {hasOverflow || !isAutoTitle ? ( @@ -147,7 +172,7 @@ export function TimeListMember(props: ListMemberProps) { ) : ( button )} - {open || isCompact ? ( + {open || isCompact || selectedGranularity ? ( {open && !isCompact ? ( value ) : null} - {GRANULARITIES.map((granularity, i) => { - return open && !isCompact ? ( - } - data-member="timeDimension" - isSelected={isGranularitySelectedList[i]} - onPress={() => { - onGranularityToggle(member.name, granularity); - setOpen(false); - }} - > - {granularity} - - ) : null; - })} + {granularityItems(customGranularities, true)} + {granularityItems(PREDEFINED_GRANULARITIES)} ) : null} diff --git a/packages/cubejs-playground/src/QueryBuilderV2/utils/format-date-by-granularity.tsx b/packages/cubejs-playground/src/QueryBuilderV2/utils/format-date-by-granularity.tsx index c7edabfa5f628..7320f6f454e98 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/utils/format-date-by-granularity.tsx +++ b/packages/cubejs-playground/src/QueryBuilderV2/utils/format-date-by-granularity.tsx @@ -13,7 +13,11 @@ const FORMAT_MAP = { }; export function formatDateByGranularity(timestamp: Date, granularity?: TimeDimensionGranularity) { - return formatDate(timestamp, FORMAT_MAP[granularity ?? 'second']); + return formatDate( + timestamp, + FORMAT_MAP[(granularity as Exclude) ?? 'second'] ?? + FORMAT_MAP['second'] + ); } export function formatDateByPattern(timestamp: Date, format?: string) { diff --git a/packages/cubejs-playground/src/QueryBuilderV2/utils/index.ts b/packages/cubejs-playground/src/QueryBuilderV2/utils/index.ts index d44f70600db87..f36b9e5e25c5b 100644 --- a/packages/cubejs-playground/src/QueryBuilderV2/utils/index.ts +++ b/packages/cubejs-playground/src/QueryBuilderV2/utils/index.ts @@ -11,3 +11,4 @@ export * from './use-commit-press'; export * from './uncapitalize'; export * from './uniq-array'; export * from './graphql-converters'; +export * from './titleize'; diff --git a/packages/cubejs-playground/src/QueryBuilderV2/utils/titleize.ts b/packages/cubejs-playground/src/QueryBuilderV2/utils/titleize.ts new file mode 100644 index 0000000000000..cdfb68e78b2fd --- /dev/null +++ b/packages/cubejs-playground/src/QueryBuilderV2/utils/titleize.ts @@ -0,0 +1,57 @@ +const underbar = new RegExp('_', 'g'); +const dot = new RegExp('\\.', 'g'); +const nonTitlecasedWords = [ + 'and', + 'or', + 'nor', + 'a', + 'an', + 'the', + 'so', + 'but', + 'to', + 'of', + 'at', + 'by', + 'from', + 'into', + 'on', + 'onto', + 'off', + 'out', + 'in', + 'over', + 'with', + 'for', +]; + +function capitalize(str: string) { + str = str.toLowerCase(); + + return str.substring(0, 1).toUpperCase() + str.substring(1); +} + +export function titleize(str: string) { + str = str.toLowerCase().replace(underbar, ' ').replace(dot, ' '); + const strArr = str.split(' '); + const j = strArr.length; + let d: string[], l: number; + + for (let i = 0; i < j; i++) { + d = strArr[i].split('-'); + l = d.length; + + for (let k = 0; k < l; k++) { + if (nonTitlecasedWords.indexOf(d[k].toLowerCase()) < 0) { + d[k] = capitalize(d[k]); + } + } + + strArr[i] = d.join('-'); + } + + str = strArr.join(' '); + str = str.substring(0, 1).toUpperCase() + str.substring(1); + + return str; +} diff --git a/yarn.lock b/yarn.lock index 2fd122919b09e..1e138d2493137 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4242,10 +4242,10 @@ resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz#b6c75a56a1947cc916ea058772d666a2c8932f31" integrity sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA== -"@cube-dev/ui-kit@0.37.3": - version "0.37.3" - resolved "https://registry.yarnpkg.com/@cube-dev/ui-kit/-/ui-kit-0.37.3.tgz#cb0bbba199fe63e692ee1b163f7473e6dd0eb9e9" - integrity sha512-7VJXTm+Z9MYdCErzssb2LkkyF3xzAtrfiD1JkJVX+M/Pc6is3JHLp0MQWBOe859ue4bj6AobHaTZcGB4mpxesg== +"@cube-dev/ui-kit@0.37.4": + version "0.37.4" + resolved "https://registry.yarnpkg.com/@cube-dev/ui-kit/-/ui-kit-0.37.4.tgz#5e79a6875cd20ce24db401aecb594c9278f71313" + integrity sha512-F7P+XAL5ZO1pKauRkeaVxQc0iBx05urKHfZbSvq5Pubavif8HWMBce22rGQ/N7N9+GnTFt0Wt1zq+FEdJsvBUA== dependencies: "@ant-design/icons" "^5.3.4" "@internationalized/date" "^3.5.2"