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

feat(playground): custom granularities support #8743

Merged
merged 9 commits into from
Oct 9, 2024
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
2 changes: 1 addition & 1 deletion packages/cubejs-playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(null);

const hasOverflow = useHasOverflow(textRef);
const isAutoTitle = titleize(name) === title;

const button = (
<ListMemberButton
icon={isCustom ? <CalendarEditIcon /> : <CalendarIcon />}
data-member="timeDimension"
isSelected={isSelected}
onPress={onToggle}
>
<Text ref={textRef} ellipsis>
{name}
</Text>
</ListMemberButton>
);

if (hasOverflow || (!isAutoTitle && isCustom)) {
return (
<TooltipProvider
title={
<>
<Text preset="t4">{name}</Text>
<br />
<Text preset="t3">{title}</Text>
</>
}
delay={1000}
placement="right"
>
{button}
</TooltipProvider>
);
} else {
return button;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 = (
<ListMemberWrapper>
<ListMemberButton
Expand Down Expand Up @@ -122,11 +124,15 @@ export function ListMember(props: ListMemberProps) {
</ListMemberWrapper>
);

return hasOverflow ? (
return hasOverflow || !isAutoTitle ? (
<TooltipProvider
title={
<>
<b>{name}</b>
<Text preset="t4">
<b>{name}</b>
</Text>
<br />
<Text preset="t4">{title}</Text>
</>
}
delay={1000}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -468,12 +470,16 @@ export function SidePanelCubeItem({
return (
<Space flow="column" gap="0">
<CubeWrapper>
{hasOverflow ? (
{hasOverflow || !isAutoTitle ? (
<TooltipProvider
delay={1000}
title={
<>
<b>{name}</b>
<Text preset="t4">
<b>{name}</b>
</Text>
<br />
<Text preset="t4">{title}</Text>
</>
}
placement="right"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
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 { 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 { GranularityListMember } from './GranularityListMember';
import { ListMemberButton } from './ListMemberButton';
import { FilterByMemberButton } from './FilterByMemberButton';
import { MemberBadge } from './Badge';
import { FilteredLabel } from './FilteredLabel';

interface ListMemberProps {
Expand All @@ -32,7 +25,7 @@ interface ListMemberProps {
onToggleDataRange?: (name: string) => void;
}

const GRANULARITIES: TimeDimensionGranularity[] = [
const PREDEFINED_GRANULARITIES: TimeDimensionGranularity[] = [
'second',
'minute',
'hour',
Expand Down Expand Up @@ -66,13 +59,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<string, string>
)
);
}, [member.type === 'time' ? member.granularities : null]);
const memberGranularities = customGranularities.concat(PREDEFINED_GRANULARITIES);
const isGranularitySelectedMap: Record<string, boolean> = {};
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 = (
<ListMemberButton
icon={
Expand All @@ -99,22 +113,7 @@ export function TimeListMember(props: ListMemberProps) {
<Text ref={textRef} ellipsis>
{filterString ? <FilteredLabel text={name} filter={filterString} /> : name}
</Text>
{(isCompact || !open) && selectedGranularity ? (
<TooltipProvider
delay={1000}
title="Click the granularity label to remove it from the query"
>
<Action
onPress={() => {
onGranularityToggle(member.name, selectedGranularity);
}}
>
<MemberBadge isSpecial type="timeDimension">
{selectedGranularity}
</MemberBadge>
</Action>
</TooltipProvider>
) : null}

<Space gap=".5x">
<Space gap="1x">
{description ? <ItemInfoIcon title={title} description={description} /> : undefined}
Expand All @@ -130,9 +129,34 @@ export function TimeListMember(props: ListMemberProps) {
</ListMemberButton>
);

const granularityItems = (items: string[], isCustom?: boolean) => {
return items.map((granularity: string) => {
if (
((!open || isCompact) && !isGranularitySelectedMap[granularity]) ||
!customGranularitiesTitleMap
) {
return null;
}

return (
<GranularityListMember
key={`${name}.${granularity}`}
name={granularity}
title={customGranularitiesTitleMap[granularity]}
isCustom={isCustom}
isSelected={isGranularitySelectedMap[granularity]}
onToggle={() => {
onGranularityToggle(member.name, granularity);
setOpen(false);
}}
/>
);
});
};

return (
<>
{hasOverflow ? (
{hasOverflow || !isAutoTitle ? (
<TooltipProvider
title={
<>
Expand All @@ -147,7 +171,7 @@ export function TimeListMember(props: ListMemberProps) {
) : (
button
)}
{open || isCompact ? (
{open || isCompact || selectedGranularity ? (
<Flex flow="column" gap="1bw" padding="4.5x left">
{open && !isCompact ? (
<ListMemberButton
Expand All @@ -162,22 +186,8 @@ export function TimeListMember(props: ListMemberProps) {
<Text ellipsis>value</Text>
</ListMemberButton>
) : null}
{GRANULARITIES.map((granularity, i) => {
return open && !isCompact ? (
<ListMemberButton
key={`${name}.${granularity}`}
icon={<CalendarIcon />}
data-member="timeDimension"
isSelected={isGranularitySelectedList[i]}
onPress={() => {
onGranularityToggle(member.name, granularity);
setOpen(false);
}}
>
<Text ellipsis>{granularity}</Text>
</ListMemberButton>
) : null;
})}
{granularityItems(customGranularities, true)}
{granularityItems(PREDEFINED_GRANULARITIES)}
</Flex>
) : null}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TimeDimensionGranularity, string>) ?? 'second'] ??
FORMAT_MAP['second']
);
}

export function formatDateByPattern(timestamp: Date, format?: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './use-commit-press';
export * from './uncapitalize';
export * from './uniq-array';
export * from './graphql-converters';
export * from './titleize';
Loading
Loading