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

Data explorer #6159

Closed
wants to merge 71 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
21cbb4f
Add reports page
ad-elias Jul 1, 2024
a5d97fb
Reports data model draft
ad-elias Jul 4, 2024
0b51b48
Disable chart entity to make workspace:sync-metadata work
ad-elias Jul 5, 2024
80ae167
Add chart entity
ad-elias Jul 5, 2024
ee43744
Fix report standardId
ad-elias Jul 5, 2024
388a062
Add relation between chart and analyticsQuery
ad-elias Jul 5, 2024
0c6e7a4
Add create report button and load reports
ad-elias Jul 6, 2024
398598c
Draft reports UI
ad-elias Jul 6, 2024
d9dee57
Draft chart editor UI
ad-elias Jul 8, 2024
1e42afe
Remove debug print
ad-elias Jul 8, 2024
165f508
Create RunAnalyticsQuery mutation
ad-elias Jul 8, 2024
2566c14
Remove reports
ad-elias Jul 12, 2024
f228dad
Rename reports to charts
ad-elias Jul 12, 2024
3348509
Merge AnalyticsQueryWorkspaceEntity to ChartWorkspaceEntity
ad-elias Jul 13, 2024
ce29c62
Add chart tab to ShowPage
ad-elias Jul 14, 2024
22f1991
Run a simple hardcoded chart SQL query from frontend
ad-elias Jul 14, 2024
22a3f80
Join query generation WiP
ad-elias Jul 17, 2024
215dc40
Make chart measure enum
ad-elias Jul 18, 2024
09b4608
Fix single metric query
ad-elias Jul 19, 2024
70602a6
Remove result fields
ad-elias Jul 19, 2024
5154b1c
Add Nivo charts
ad-elias Jul 19, 2024
64904c6
Display chart data
ad-elias Jul 19, 2024
d0a0749
Rename files to chart
ad-elias Jul 20, 2024
82b3a20
Fix naming, fix chart height
ad-elias Jul 21, 2024
1f18757
Merge branch 'main' into feat/data-explorer
ad-elias Jul 21, 2024
b3dcbb2
Fix empty chart
ad-elias Jul 25, 2024
035f9e6
Add FIELD_PATH field metadata type
ad-elias Jul 25, 2024
711c84a
Field path picker WiP
ad-elias Jul 28, 2024
9a02410
Add field list to field path selector
ad-elias Jul 28, 2024
cb617c9
Field path picker styling
ad-elias Jul 29, 2024
9bed28e
Filter and style field path options
ad-elias Jul 29, 2024
b1b2aee
Fix FieldPathPicker structure
ad-elias Aug 5, 2024
27ad5c6
Persist field path value
ad-elias Aug 5, 2024
4239bed
Display field path
ad-elias Aug 6, 2024
6bb7bfa
Field path display styling
ad-elias Aug 7, 2024
7fc799b
Fix groupBy field metadata type
ad-elias Aug 8, 2024
f0f4d70
Use new field path in backend, temp disable group by
ad-elias Aug 8, 2024
78d073f
Fix feature flag key
ad-elias Aug 8, 2024
3400414
Merge branch 'main' into feat/data-explorer
ad-elias Aug 9, 2024
ced9a6d
Remove unused code
ad-elias Aug 9, 2024
fc8ab76
Field path picker improvements
ad-elias Aug 9, 2024
ef059b1
Field path picker max depth
ad-elias Aug 9, 2024
90704b1
Fix view seeding, enable Charts by default in dev env
FelixMalfait Aug 10, 2024
d2cf9d2
Field path empty value
ad-elias Aug 10, 2024
1f5c8eb
Field path join generation WiP
ad-elias Aug 10, 2024
957dd54
Update todo
ad-elias Aug 10, 2024
66e3f9b
Fix warnings
ad-elias Aug 10, 2024
1c637af
Add table aliases
ad-elias Aug 11, 2024
c2cbf95
Merge branch 'main' into feat/data-explorer
ad-elias Aug 11, 2024
5462df2
Refactor
ad-elias Aug 11, 2024
2dd88c0
Group by WiP
ad-elias Aug 12, 2024
d12c034
Merge branch 'main' into feat/data-explorer
ad-elias Aug 12, 2024
2dc3d89
Rename chart.fieldPath to chart.target, fix empty group by
ad-elias Aug 12, 2024
97368cd
Update todo
ad-elias Aug 12, 2024
199b4da
Microfix import
FelixMalfait Aug 12, 2024
94fa77a
Fix getTableAliasAndColumn
ad-elias Aug 12, 2024
bc3963b
Draft composite type handling
ad-elias Aug 13, 2024
d1442c3
Merge branch 'main' into feat/data-explorer
ad-elias Aug 13, 2024
86deef6
Fix getCommonTableExpressionDefinitions return type
ad-elias Aug 14, 2024
efa4a24
Common table expressions for composite type handling WiP
ad-elias Aug 14, 2024
8aaaffc
Merge branch 'main' into feat/data-explorer
ad-elias Aug 14, 2024
dfb501a
Refactor to support common table expressions
ad-elias Aug 16, 2024
ffe3661
Merge branch 'main' into feat/data-explorer
ad-elias Aug 16, 2024
3621a22
Merge remote-tracking branch 'upstream/main' into feat/data-explorer
ad-elias Aug 17, 2024
19e10ad
Add soft delete to chart workspace entity
ad-elias Aug 17, 2024
e41dc25
Draft chart query data structure
ad-elias Aug 17, 2024
f8e591f
Group by common table expressions support WiP
ad-elias Aug 18, 2024
f1fa4a2
Group by with common table expressions and the source table works
ad-elias Aug 18, 2024
aa651b1
Fix group by common table expression
ad-elias Aug 19, 2024
9bce68b
Improve chart query format
ad-elias Aug 21, 2024
27bee2a
Persist data explorer query
ad-elias Aug 21, 2024
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
6 changes: 6 additions & 0 deletions packages/twenty-front/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ import { CreateWorkspace } from '~/pages/onboarding/CreateWorkspace';
import { InviteTeam } from '~/pages/onboarding/InviteTeam';
import { PaymentSuccess } from '~/pages/onboarding/PaymentSuccess';
import { SyncEmails } from '~/pages/onboarding/SyncEmails';
import { ChartEditor } from '~/pages/reports/ChartEditor';
import { Charts } from '~/pages/reports/Charts';
import { Reports } from '~/pages/reports/Reports';
import { SettingsAccounts } from '~/pages/settings/accounts/SettingsAccounts';
import { SettingsAccountsCalendars } from '~/pages/settings/accounts/SettingsAccountsCalendars';
import { SettingsAccountsCalendarsSettings } from '~/pages/settings/accounts/SettingsAccountsCalendarsSettings';
Expand Down Expand Up @@ -152,6 +155,9 @@ const createRouter = (isBillingEnabled?: boolean) =>
/>
<Route path={indexAppPath.getIndexAppPath()} element={<></>} />
<Route path={AppPath.TasksPage} element={<Tasks />} />
<Route path={AppPath.ReportsPage} element={<Reports />} />
<Route path={AppPath.ChartsPage} element={<Charts />} />
<Route path={AppPath.ChartEditorPage} element={<ChartEditor />} />
<Route path={AppPath.Impersonate} element={<ImpersonateEffect />} />
<Route path={AppPath.RecordIndexPage} element={<RecordIndexPage />} />
<Route path={AppPath.RecordShowPage} element={<RecordShowPage />} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useState } from 'react';
import styled from '@emotion/styled';

import { AnalyticsQueryFilters } from '@/activities/reports/components/AnalyticsQueryFilters';
import { AnalyticsQuery } from '@/activities/reports/types/AnalyticsQuery';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { Select } from '@/ui/input/components/Select';

const StyledContainer = styled.div`
display: flex;
flex-direction: column;
`;

interface AnalyticsQueryEditorProps {
analyticsQuery?: AnalyticsQuery;
}

export const AnalyticsQueryEditor = (props: AnalyticsQueryEditorProps) => {
const { objectMetadataItems } = useObjectMetadataItems();
const sourceObjectSelectOptions = objectMetadataItems.map(
(objectMetadataItem) => ({
value: objectMetadataItem.nameSingular,
label: objectMetadataItem.labelPlural,
}),
);

const [sourceObjectNameSingular, setSourceObjectNameSingular] =
useState<string>();

const fieldPathOptions: any[] = []; // TODO
const measureOptions: any[] = []; // TODO
const groupByOptions: any[] = []; // TODO

return (
<StyledContainer>
<Select
label="Source object"
fullWidth
dropdownId="source-object-select"
options={sourceObjectSelectOptions}
value={
sourceObjectNameSingular ??
props.analyticsQuery?.sourceObjectNameSingular ??
sourceObjectSelectOptions?.[0].value
}
onChange={async (value) => {
setSourceObjectNameSingular(value);
// TODO: mutation
}}
/>
<AnalyticsQueryFilters analyticsQuery={props.analyticsQuery} />

<Select
label="Field"
fullWidth
dropdownId="field-path-select"
options={fieldPathOptions}
/>

<Select
label="Measure"
fullWidth
dropdownId="measure-select"
options={measureOptions}
/>

<Select
label="Group by"
fullWidth
dropdownId="measure-select"
options={measureOptions}
ad-elias marked this conversation as resolved.
Show resolved Hide resolved
/>
</StyledContainer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import styled from '@emotion/styled';

import { AnalyticsQueryFilter as AnalyticsQueryFilterType } from '@/activities/reports/types/AnalyticsQueryFilter';
import { Select } from '@/ui/input/components/Select';
import { TextInput } from '@/ui/input/components/TextInput';

interface AnalyticsQueryFilterProps {
analyticsQueryFilter?: AnalyticsQueryFilterType;
}

const StyledContainer = styled.div`
display: flex;
`;

export const AnalyticsQueryFilter = (props: AnalyticsQueryFilterProps) => {
const fieldOptions: { value: string; label: string }[] = [];
const operatorOptions: { value: string; label: string }[] = [];

return (
<StyledContainer>
<Select
fullWidth
dropdownId="analytics-query-field-select"
options={fieldOptions}
//value={}
onChange={async () => {
// TODO: Save
ad-elias marked this conversation as resolved.
Show resolved Hide resolved
ad-elias marked this conversation as resolved.
Show resolved Hide resolved
}}
/>
<Select
fullWidth
dropdownId="analytics-query-operator-select"
options={operatorOptions}
//value={}
onChange={async () => {
// TODO: Save
ad-elias marked this conversation as resolved.
Show resolved Hide resolved
ad-elias marked this conversation as resolved.
Show resolved Hide resolved
}}
/>
<TextInput />
</StyledContainer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { AnalyticsQueryFilter } from '@/activities/reports/components/AnalyticsQueryFilter';
import { AnalyticsQuery } from '@/activities/reports/types/AnalyticsQuery';

interface AnalyticsQueryFiltersProps {
analyticsQuery?: AnalyticsQuery;
}

export const AnalyticsQueryFilters = (props: AnalyticsQueryFiltersProps) =>
props.analyticsQuery?.analyticsQueryFilters.map((analytcsQueryFilter) => (
ad-elias marked this conversation as resolved.
Show resolved Hide resolved
<AnalyticsQueryFilter analyticsQueryFilter={analytcsQueryFilter} />
));
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Chart as ChartType } from '@/activities/reports/types/Chart';

interface ChartProps {
chart: ChartType;
}

export const Chart = (props: ChartProps) => (
<div>
<h2>{props.chart.title}</h2>
<p>{props.chart.title}</p>
ad-elias marked this conversation as resolved.
Show resolved Hide resolved
{/* TODO: Nivo charts */}
</div>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Chart } from '@/activities/reports/types/Chart';
import { Select } from '@/ui/input/components/Select';

interface ChartConfigProps {
chart?: Chart;
}

export const ChartConfig = (props: ChartConfigProps) => {
const chartTypes: any[] = [];

return (
<Select
label="Chart type"
fullWidth
dropdownId="chart-type-select"
options={chartTypes}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

import { Report } from '@/activities/reports/types/Report';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { PageAddButton } from '@/ui/layout/page/PageAddButton';

export const PageAddReportButton = () => {
const [creating, setCreating] = useState(false);
const navigate = useNavigate();

const { createOneRecord: createOneReport } = useCreateOneRecord<Report>({
objectNameSingular: CoreObjectNameSingular.Report,
});

return (
<PageAddButton
onClick={async () => {
if (creating) return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧠 logic: Consider adding error handling for the report creation process to manage potential failures.

setCreating(true);
const report = await createOneReport({ title: 'New Report' });
await navigate(`/reports/${report.id}/charts`);
setCreating(false);
}}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import styled from '@emotion/styled';

import { ReportRow } from '@/activities/reports/components/ReportRow';
import { Report } from '@/activities/reports/types/Report';

interface ReportGroupProps {
title?: string;
reports: Report[];
}

const StyledContainer = styled.div`
align-items: flex-start;
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: center;
padding: 8px 24px;
`;

const StyledTitleBar = styled.div`
display: flex;
justify-content: space-between;
margin-bottom: ${({ theme }) => theme.spacing(4)};
margin-top: ${({ theme }) => theme.spacing(4)};
place-items: center;
width: 100%;
`;

// H1Title instead?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🪶 style: Consider removing this commented-out line if it's not needed.

const StyledTitle = styled.h3`
color: ${({ theme }) => theme.font.color.primary};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
`;

const StyledCount = styled.span`
color: ${({ theme }) => theme.font.color.light};
margin-left: ${({ theme }) => theme.spacing(2)};
`;

const StyledReportRows = styled.div`
background-color: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.md};
width: 100%;
`;

export const ReportGroup = (props: ReportGroupProps) => (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🪶 style: Destructure props for better readability and to avoid repetitive props. usage.

<>
{props.reports && props.reports.length > 0 && (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧠 logic: No need to check props.reports before checking props.reports.length > 0 since props.reports is typed as Report[] and will always be defined.

<StyledContainer>
<StyledTitleBar>
{props.title && (
<StyledTitle>
{props.title} <StyledCount>{props.reports.length}</StyledCount>
</StyledTitle>
)}
</StyledTitleBar>
<StyledReportRows>
{props.reports.map((report) => (
<ReportRow key={report.id} report={report} />
))}
</StyledReportRows>
</StyledContainer>
)}
</>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';

import { ReportGroup } from '@/activities/reports/components/ReportGroup';
import {
REPORT_GROUP_TIME_SPANS,
ReportGroupTimeSpan,
} from '@/activities/reports/constants/ReportGroupTimeSpans';
import { Report } from '@/activities/reports/types/Report';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { capitalize } from '~/utils/string/capitalize';

export const ReportGroups = () => {
const { records: reports } = useFindManyRecords<Report>({
objectNameSingular: CoreObjectNameSingular.Report,
});

const reportGroups: { title: string; reports: any[] }[] =
REPORT_GROUP_TIME_SPANS.map((reportGroupTimeSpan, i) => {
const largerReportGroupTimeSpan: ReportGroupTimeSpan | undefined =
REPORT_GROUP_TIME_SPANS[i + 1];

const groupReports = reports.filter((report) => {
const createdAt = new Date(report.createdAt);
const now = new Date();
const sinceMs = +now - +createdAt;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🪶 style: Consider memoizing the groupReports calculation to optimize performance.


return (
sinceMs >= reportGroupTimeSpan.minSinceMs &&
sinceMs < largerReportGroupTimeSpan.minSinceMs
);
});

return {
title: reportGroupTimeSpan.title,
reports: groupReports,
};
}).filter((reportGroup) => reportGroup.reports.length);

return (
<div>
{reportGroups.map((reportGroup) => (
<ReportGroup
title={capitalize(reportGroup.title)}
reports={reportGroup.reports}
/>
))}
</div>
);
};
Loading
Loading