Typescript refactor.

This commit is contained in:
Mike Cao 2023-12-03 03:07:03 -08:00
parent b578162cb6
commit 7c42f0da82
173 changed files with 968 additions and 549 deletions

View file

@ -3,13 +3,21 @@ import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm';
import { useApi, useMessages } from 'components/hooks';
import { setValue } from 'store/cache';
export function ReportDeleteButton({ reportId, reportName, onDelete }) {
export function ReportDeleteButton({
reportId,
reportName,
onDelete,
}: {
reportId: string;
reportName: string;
onDelete?: () => void;
}) {
const { formatMessage, labels } = useMessages();
const { del, useMutation } = useApi();
const { mutate } = useMutation(reportId => del(`/reports/${reportId}`));
const handleConfirm = close => {
mutate(reportId, {
const handleConfirm = (close: () => void) => {
mutate(reportId as any, {
onSuccess: () => {
setValue('reports', Date.now());
onDelete?.();

View file

@ -1,18 +1,10 @@
'use client';
import { useApi } from 'components/hooks';
import { useReports } from 'components/hooks';
import ReportsTable from './ReportsTable';
import useFilterQuery from 'components/hooks/useFilterQuery';
import DataTable from 'components/common/DataTable';
import useCache from 'store/cache';
export default function ReportsDataTable({ websiteId }: { websiteId?: string }) {
const { get } = useApi();
const modified = useCache(state => (state as any)?.reports);
const queryResult = useFilterQuery({
queryKey: ['reports', { websiteId, modified }],
queryFn: (params: any) =>
get(websiteId ? `/websites/${websiteId}/reports` : `/reports`, params),
});
const queryResult = useReports(websiteId);
return (
<DataTable queryResult={queryResult}>

View file

@ -5,7 +5,7 @@ import useUser from 'components/hooks/useUser';
import { REPORT_TYPES } from 'lib/constants';
import ReportDeleteButton from './ReportDeleteButton';
export function ReportsTable({ data = [], showDomain }) {
export function ReportsTable({ data = [], showDomain }: { data: any[]; showDomain?: boolean }) {
const { formatMessage, labels } = useMessages();
const { user } = useUser();
const breakpoint = useBreakpoint();

View file

@ -6,12 +6,19 @@ import WebsiteSelect from 'components/input/WebsiteSelect';
import { useMessages } from 'components/hooks';
import { ReportContext } from './Report';
export interface BaseParametersProps {
showWebsiteSelect?: boolean;
allowWebsiteSelect?: boolean;
showDateSelect?: boolean;
allowDateSelect?: boolean;
}
export function BaseParameters({
showWebsiteSelect = true,
allowWebsiteSelect = true,
showDateSelect = true,
allowDateSelect = true,
}) {
}: BaseParametersProps) {
const { report, updateReport } = useContext(ReportContext);
const { formatMessage, labels } = useMessages();
@ -19,11 +26,11 @@ export function BaseParameters({
const { websiteId, dateRange } = parameters || {};
const { value, startDate, endDate } = dateRange || {};
const handleWebsiteSelect = websiteId => {
const handleWebsiteSelect = (websiteId: string) => {
updateReport({ websiteId, parameters: { websiteId } });
};
const handleDateChange = value => {
const handleDateChange = (value: string) => {
updateReport({ parameters: { dateRange: { ...parseDateRange(value) } } });
};

View file

@ -7,10 +7,20 @@ import FieldAggregateForm from './FieldAggregateForm';
import FieldFilterForm from './FieldFilterForm';
import styles from './FieldAddForm.module.css';
export function FieldAddForm({ fields = [], group, onAdd, onClose }) {
const [selected, setSelected] = useState();
export function FieldAddForm({
fields = [],
group,
onAdd,
onClose,
}: {
fields?: any[];
group: string;
onAdd: (group: string, value: string) => void;
onClose: () => void;
}) {
const [selected, setSelected] = useState<{ name: string; type: string; value: string }>();
const handleSelect = value => {
const handleSelect = (value: any) => {
const { type } = value;
if (group === REPORT_PARAMETERS.groups || type === 'array' || type === 'boolean') {
@ -22,7 +32,7 @@ export function FieldAddForm({ fields = [], group, onAdd, onClose }) {
setSelected(value);
};
const handleSave = value => {
const handleSave = (value: any) => {
onAdd(group, value);
onClose();
};

View file

@ -1,7 +1,15 @@
import { Form, FormRow, Menu, Item } from 'react-basics';
import { useMessages } from 'components/hooks';
export default function FieldAggregateForm({ name, type, onSelect }) {
export default function FieldAggregateForm({
name,
type,
onSelect,
}: {
name: string;
type: string;
onSelect: (key: any) => void;
}) {
const { formatMessage, labels } = useMessages();
const options = {
@ -27,7 +35,7 @@ export default function FieldAggregateForm({ name, type, onSelect }) {
const items = options[type];
const handleSelect = value => {
const handleSelect = (value: any) => {
onSelect({ name, type, value });
};

View file

@ -3,6 +3,15 @@ import { Form, FormRow, Item, Flexbox, Dropdown, Button } from 'react-basics';
import { useMessages, useFilters, useFormat, useLocale } from 'components/hooks';
import styles from './FieldFilterForm.module.css';
export interface FieldFilterFormProps {
name: string;
label?: string;
type: string;
values?: any[];
onSelect?: (key: any) => void;
allowFilterSelect?: boolean;
}
export default function FieldFilterForm({
name,
label,
@ -10,7 +19,7 @@ export default function FieldFilterForm({
values,
onSelect,
allowFilterSelect = true,
}) {
}: FieldFilterFormProps) {
const { formatMessage, labels } = useMessages();
const [filter, setFilter] = useState('eq');
const [value, setValue] = useState();
@ -21,7 +30,7 @@ export default function FieldFilterForm({
const formattedValues = useMemo(() => {
const formatted = {};
const format = val => {
const format = (val: string) => {
formatted[val] = formatValue(val, name);
return formatted[val];
};
@ -56,7 +65,7 @@ export default function FieldFilterForm({
items={filters}
value={filter}
renderValue={renderFilterValue}
onChange={setFilter}
onChange={(key: any) => setFilter(key)}
>
{({ value, label }) => {
return <Item key={value}>{label}</Item>;
@ -69,12 +78,12 @@ export default function FieldFilterForm({
items={values}
value={value}
renderValue={renderValue}
onChange={setValue}
onChange={(key: any) => setValue(key)}
style={{
minWidth: '250px',
}}
>
{value => {
{(value: string) => {
return <Item key={value}>{formattedValues[value]}</Item>;
}}
</Dropdown>

View file

@ -1,15 +1,26 @@
import { Menu, Item, Form, FormRow } from 'react-basics';
import { useMessages } from 'components/hooks';
import styles from './FieldSelectForm.module.css';
import { Key } from 'react';
export default function FieldSelectForm({ items, onSelect, showType = true }) {
export interface FieldSelectFormProps {
fields?: any[];
onSelect?: (key: any) => void;
showType?: boolean;
}
export default function FieldSelectForm({
fields = [],
onSelect,
showType = true,
}: FieldSelectFormProps) {
const { formatMessage, labels } = useMessages();
return (
<Form>
<FormRow label={formatMessage(labels.fields)}>
<Menu className={styles.menu} onSelect={key => onSelect(items[key])}>
{items.map(({ name, label, type }, index) => {
<Menu className={styles.menu} onSelect={key => onSelect(fields[key as any])}>
{fields.map(({ name, label, type }: any, index: Key) => {
return (
<Item key={index} className={styles.item}>
<div>{label || name}</div>

View file

@ -5,7 +5,7 @@ import FieldSelectForm from './FieldSelectForm';
import FieldFilterForm from './FieldFilterForm';
import { useApi } from 'components/hooks';
function useValues(websiteId, type) {
function useValues(websiteId: string, type: string) {
const now = Date.now();
const { get, useQuery } = useApi();
const { data, error, isLoading } = useQuery({
@ -22,12 +22,24 @@ function useValues(websiteId, type) {
return { data, error, isLoading };
}
export default function FilterSelectForm({ websiteId, items, onSelect, allowFilterSelect }) {
const [field, setField] = useState();
export interface FilterSelectFormProps {
websiteId: string;
items: any[];
onSelect?: (key: any) => void;
allowFilterSelect?: boolean;
}
export default function FilterSelectForm({
websiteId,
items,
onSelect,
allowFilterSelect,
}: FilterSelectFormProps) {
const [field, setField] = useState<{ name: string; label: string; type: string }>();
const { data, isLoading } = useValues(websiteId, field?.name);
if (!field) {
return <FieldSelectForm items={items} onSelect={setField} showType={false} />;
return <FieldSelectForm fields={items} onSelect={setField} showType={false} />;
}
if (isLoading) {

View file

@ -1,10 +1,17 @@
import { ReactNode } from 'react';
import { Icon, TooltipPopup } from 'react-basics';
import Icons from 'components/icons';
import Empty from 'components/common/Empty';
import { useMessages } from 'components/hooks';
import styles from './ParameterList.module.css';
export function ParameterList({ items = [], children, onRemove }) {
export interface ParameterListProps {
items: any[];
children?: ReactNode | ((item: any) => ReactNode);
onRemove: (index: number, e: any) => void;
}
export function ParameterList({ items = [], children, onRemove }: ParameterListProps) {
const { formatMessage, labels } = useMessages();
return (

View file

@ -1,7 +1,16 @@
import { CSSProperties, ReactNode } from 'react';
import classNames from 'classnames';
import styles from './PopupForm.module.css';
export function PopupForm({ className, style, children }) {
export function PopupForm({
className,
style,
children,
}: {
className?: string;
style?: CSSProperties;
children: ReactNode;
}) {
return (
<div
className={classNames(styles.form, className)}

View file

@ -1,11 +1,19 @@
'use client';
import { createContext } from 'react';
import { createContext, ReactNode } from 'react';
import { useReport } from 'components/hooks';
import styles from './Report.module.css';
import classNames from 'classnames';
export const ReportContext = createContext(null);
export function Report({ reportId, defaultParameters, children, ...props }) {
export interface ReportProps {
reportId: string;
defaultParameters: { [key: string]: any };
children: ReactNode;
className?: string;
}
export function Report({ reportId, defaultParameters, children, className }: ReportProps) {
const report = useReport(reportId, defaultParameters);
if (!report) {
@ -14,9 +22,7 @@ export function Report({ reportId, defaultParameters, children, ...props }) {
return (
<ReportContext.Provider value={{ ...report }}>
<div {...props} className={styles.container}>
{children}
</div>
<div className={classNames(styles.container, className)}>{children}</div>
</ReportContext.Provider>
);
}

View file

@ -12,7 +12,7 @@ const reports = {
retention: RetentionReport,
};
export default function ReportDetails({ reportId }) {
export default function ReportDetails({ reportId }: { reportId: string }) {
const { get, useQuery } = useApi();
const { data: report } = useQuery({
queryKey: ['reports', reportId],

View file

@ -12,8 +12,10 @@ export function ReportHeader({ icon }) {
const { showToast } = useToasts();
const { post, useMutation } = useApi();
const router = useRouter();
const { mutate: create, isLoading: isCreating } = useMutation(data => post(`/reports`, data));
const { mutate: update, isLoading: isUpdating } = useMutation(data =>
const { mutate: create, isLoading: isCreating } = useMutation((data: any) =>
post(`/reports`, data),
);
const { mutate: update, isLoading: isUpdating } = useMutation((data: any) =>
post(`/reports/${data.id}`, data),
);
@ -26,7 +28,7 @@ export function ReportHeader({ icon }) {
create(report, {
onSuccess: async ({ id }) => {
showToast({ message: formatMessage(messages.saved), variant: 'success' });
router.push(`/reports/${id}`, null, { shallow: true });
router.push(`/reports/${id}`);
},
});
} else {
@ -38,11 +40,11 @@ export function ReportHeader({ icon }) {
}
};
const handleNameChange = name => {
const handleNameChange = (name: string) => {
updateReport({ name: name || defaultName });
};
const handleDescriptionChange = description => {
const handleDescriptionChange = (description: string) => {
updateReport({ description });
};

View file

@ -1,4 +1,4 @@
import { useContext, useRef } from 'react';
import { useContext } from 'react';
import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics';
import Empty from 'components/common/Empty';
import Icons from 'components/icons';
@ -29,7 +29,6 @@ function useFields(websiteId, startDate, endDate) {
export function EventDataParameters() {
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
const { formatMessage, labels, messages } = useMessages();
const ref = useRef(null);
const { parameters } = report || {};
const { websiteId, dateRange, fields, filters, groups } = parameters || {};
const { startDate, endDate } = dateRange || {};
@ -53,28 +52,28 @@ export function EventDataParameters() {
runReport(values);
};
const handleAdd = (group, value) => {
const handleAdd = (group: string, value: any) => {
const data = parameterData[group];
if (!data.find(({ name }) => name === value.name)) {
if (!data.find(({ name }) => name === value?.name)) {
updateReport({ parameters: { [group]: data.concat(value) } });
}
};
const handleRemove = (group, index) => {
const handleRemove = (group: string, index: number) => {
const data = [...parameterData[group]];
data.splice(index, 1);
updateReport({ parameters: { [group]: data } });
};
const AddButton = ({ group }) => {
const AddButton = ({ group, onAdd }) => {
return (
<PopupTrigger>
<Icon>
<Icons.Plus />
</Icon>
<Popup position="bottom" alignment="start">
{close => {
{(close: () => void) => {
return (
<FieldAddForm
fields={data.map(({ eventKey, eventDataType }) => ({
@ -82,7 +81,7 @@ export function EventDataParameters() {
type: DATA_TYPES[eventDataType],
}))}
group={group}
onAdd={handleAdd}
onAdd={onAdd}
onClose={close}
/>
);
@ -93,7 +92,7 @@ export function EventDataParameters() {
};
return (
<Form ref={ref} values={parameters} error={error} onSubmit={handleSubmit}>
<Form values={parameters} error={error} onSubmit={handleSubmit}>
<BaseParameters />
{!hasData && <Empty message={formatMessage(messages.noEventData)} />}
{parametersSelected &&

View file

@ -11,7 +11,7 @@ const defaultParameters = {
parameters: { fields: [], filters: [] },
};
export default function EventDataReport({ reportId }) {
export default function EventDataReport({ reportId }: { reportId: string }) {
return (
<Report reportId={reportId} defaultParameters={defaultParameters}>
<ReportHeader icon={<Nodes />} />

View file

@ -1,13 +1,18 @@
import { useCallback, useContext, useMemo } from 'react';
import { JSX, useCallback, useContext, useMemo } from 'react';
import { Loading, StatusLight } from 'react-basics';
import useMessages from 'components/hooks/useMessages';
import useTheme from 'components/hooks/useTheme';
import BarChart from 'components/metrics/BarChart';
import { formatLongNumber } from 'lib/format';
import styles from './FunnelChart.module.css';
import { ReportContext } from '../[id]/Report';
import styles from './FunnelChart.module.css';
export function FunnelChart({ className, loading }) {
export interface FunnelChartProps {
className?: string;
isLoading?: boolean;
}
export function FunnelChart({ className, isLoading }: FunnelChartProps) {
const { report } = useContext(ReportContext);
const { formatMessage, labels } = useMessages();
const { colors } = useTheme();
@ -15,33 +20,39 @@ export function FunnelChart({ className, loading }) {
const { parameters, data } = report || {};
const renderXLabel = useCallback(
(label, index) => {
(label: string, index: number) => {
return parameters.urls[index];
},
[parameters],
);
const renderTooltipPopup = useCallback((setTooltipPopup, model) => {
const { opacity, labelColors, dataPoints } = model.tooltip;
const renderTooltipPopup = useCallback(
(
setTooltipPopup: (arg0: JSX.Element) => void,
model: { tooltip: { opacity: any; labelColors: any; dataPoints: any } },
) => {
const { opacity, labelColors, dataPoints } = model.tooltip;
if (!dataPoints?.length || !opacity) {
setTooltipPopup(null);
return;
}
if (!dataPoints?.length || !opacity) {
setTooltipPopup(null);
return;
}
setTooltipPopup(
<>
<div>
{formatLongNumber(dataPoints[0].raw.y)} {formatMessage(labels.visitors)}
</div>
<div>
<StatusLight color={labelColors?.[0]?.backgroundColor}>
{formatLongNumber(dataPoints[0].raw.z)}% {formatMessage(labels.dropoff)}
</StatusLight>
</div>
</>,
);
}, []);
setTooltipPopup(
<>
<div>
{formatLongNumber(dataPoints[0].raw.y)} {formatMessage(labels.visitors)}
</div>
<div>
<StatusLight color={labelColors?.[0]?.backgroundColor}>
{formatLongNumber(dataPoints[0].raw.z)}% {formatMessage(labels.dropoff)}
</StatusLight>
</div>
</>,
);
},
[],
);
const datasets = useMemo(() => {
return [
@ -54,7 +65,7 @@ export function FunnelChart({ className, loading }) {
];
}, [data, colors, formatMessage, labels]);
if (loading) {
if (isLoading) {
return <Loading icon="dots" className={styles.loading} />;
}
@ -63,7 +74,7 @@ export function FunnelChart({ className, loading }) {
className={className}
datasets={datasets}
unit="day"
loading={loading}
isLoading={isLoading}
renderXLabel={renderXLabel}
renderTooltipPopup={renderTooltipPopup}
XAxisType="category"

View file

@ -1,4 +1,4 @@
import { useContext, useRef } from 'react';
import { useContext } from 'react';
import { useMessages } from 'components/hooks';
import {
Icon,
@ -21,13 +21,12 @@ import PopupForm from '../[id]/PopupForm';
export function FunnelParameters() {
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
const { formatMessage, labels } = useMessages();
const ref = useRef(null);
const { parameters } = report || {};
const { websiteId, dateRange, urls } = parameters || {};
const queryDisabled = !websiteId || !dateRange || urls?.length < 2;
const handleSubmit = (data, e) => {
const handleSubmit = (data: any, e: any) => {
e.stopPropagation();
e.preventDefault();
if (!queryDisabled) {
@ -35,11 +34,11 @@ export function FunnelParameters() {
}
};
const handleAddUrl = url => {
const handleAddUrl = (url: string) => {
updateReport({ parameters: { urls: parameters.urls.concat(url) } });
};
const handleRemoveUrl = (index, e) => {
const handleRemoveUrl = (index: number, e: any) => {
e.stopPropagation();
const urls = [...parameters.urls];
urls.splice(index, 1);
@ -62,7 +61,7 @@ export function FunnelParameters() {
};
return (
<Form ref={ref} values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
<BaseParameters />
<FormRow label={formatMessage(labels.window)}>
<FormInput
@ -73,7 +72,10 @@ export function FunnelParameters() {
</FormInput>
</FormRow>
<FormRow label={formatMessage(labels.urls)} action={<AddUrlButton />}>
<ParameterList items={urls} onRemove={handleRemoveUrl} />
<ParameterList
items={urls}
onRemove={(index: number, e: any) => handleRemoveUrl(index, e)}
/>
</FormRow>
<FormButtons>
<SubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}>

View file

@ -3,7 +3,12 @@ import { useMessages } from 'components/hooks';
import { Button, Form, FormRow, TextField, Flexbox } from 'react-basics';
import styles from './UrlAddForm.module.css';
export function UrlAddForm({ defaultValue = '', onAdd }) {
export interface UrlAddFormProps {
defaultValue?: string;
onAdd?: (url: string) => void;
}
export function UrlAddForm({ defaultValue = '', onAdd }: UrlAddFormProps) {
const [url, setUrl] = useState(defaultValue);
const { formatMessage, labels } = useMessages();

View file

@ -1,4 +1,4 @@
import { useContext, useRef } from 'react';
import { useContext } from 'react';
import { useFormat, useMessages, useFilters } from 'components/hooks';
import {
Form,
@ -24,7 +24,6 @@ export function InsightsParameters() {
const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat();
const { filterLabels } = useFilters();
const ref = useRef(null);
const { parameters } = report || {};
const { websiteId, dateRange, fields, filters } = parameters || {};
const { startDate, endDate } = dateRange || {};
@ -72,7 +71,7 @@ export function InsightsParameters() {
updateReport({ parameters: { [id]: data } });
};
const AddButton = ({ id }) => {
const AddButton = ({ id, onAdd }) => {
return (
<PopupTrigger>
<TooltipPopup label={formatMessage(labels.add)} position="top">
@ -84,8 +83,8 @@ export function InsightsParameters() {
<PopupForm>
{id === 'fields' && (
<FieldSelectForm
items={fieldOptions}
onSelect={handleAdd.bind(null, id)}
fields={fieldOptions}
onSelect={onAdd.bind(null, id)}
showType={false}
/>
)}
@ -93,7 +92,7 @@ export function InsightsParameters() {
<FilterSelectForm
websiteId={websiteId}
items={fieldOptions}
onSelect={handleAdd.bind(null, id)}
onSelect={onAdd.bind(null, id)}
/>
)}
</PopupForm>
@ -103,7 +102,7 @@ export function InsightsParameters() {
};
return (
<Form ref={ref} values={parameters} onSubmit={handleSubmit}>
<Form values={parameters} onSubmit={handleSubmit}>
<BaseParameters />
{parametersSelected &&
parameterGroups.map(({ id, label }) => {

View file

@ -13,7 +13,7 @@ const defaultParameters = {
parameters: { fields: [], filters: [] },
};
export default function InsightsReport({ reportId }) {
export default function InsightsReport({ reportId }: { reportId: string }) {
return (
<Report reportId={reportId} defaultParameters={defaultParameters}>
<ReportHeader icon={<Lightbulb />} />

View file

@ -5,7 +5,7 @@ import { ReportContext } from '../[id]/Report';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
export function InsightsTable() {
const [fields, setFields] = useState();
const [fields, setFields] = useState([]);
const { report } = useContext(ReportContext);
const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat();

View file

@ -1,4 +1,4 @@
import { useContext, useRef } from 'react';
import { useContext } from 'react';
import { useMessages } from 'components/hooks';
import { Form, FormButtons, FormRow, SubmitButton } from 'react-basics';
import { ReportContext } from '../[id]/Report';
@ -9,14 +9,13 @@ import { parseDateRange } from 'lib/date';
export function RetentionParameters() {
const { report, runReport, isRunning, updateReport } = useContext(ReportContext);
const { formatMessage, labels } = useMessages();
const ref = useRef(null);
const { parameters } = report || {};
const { websiteId, dateRange } = parameters || {};
const { startDate } = dateRange || {};
const queryDisabled = !websiteId || !dateRange;
const handleSubmit = (data, e) => {
const handleSubmit = (data: any, e: any) => {
e.stopPropagation();
e.preventDefault();
@ -30,7 +29,7 @@ export function RetentionParameters() {
};
return (
<Form ref={ref} values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
<BaseParameters showDateSelect={false} />
<FormRow label={formatMessage(labels.date)}>
<MonthSelect date={startDate} onChange={handleDateChange} />

View file

@ -19,7 +19,7 @@ const defaultParameters = {
},
};
export default function RetentionReport({ reportId }) {
export default function RetentionReport({ reportId }: { reportId: string }) {
return (
<Report reportId={reportId} defaultParameters={defaultParameters}>
<ReportHeader icon={<Magnet />} />

View file

@ -18,7 +18,7 @@ export function RetentionTable({ days = DAYS }) {
return <EmptyPlaceholder />;
}
const rows = data.reduce((arr, row) => {
const rows = data.reduce((arr: any[], row: { date: any; visitors: any; day: any }) => {
const { date, visitors, day } = row;
if (day === 0) {
return arr.concat({