diff --git a/src/components/Campaigns/CloneCampaign.tsx b/src/components/Campaigns/CloneCampaign.tsx
index a8915a61f..088c31ab4 100644
--- a/src/components/Campaigns/CloneCampaign.tsx
+++ b/src/components/Campaigns/CloneCampaign.tsx
@@ -1,17 +1,18 @@
import {
Box,
Button,
- Chip,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
+ Tooltip,
} from "@mui/material";
import {
- CampaignFragment,
+ CampaignSummaryFragment,
useCreateCampaignMutation,
+ useLoadCampaignLazyQuery,
} from "graphql/campaign.generated";
import { useHistory } from "react-router-dom";
import { useContext, useState } from "react";
@@ -21,20 +22,21 @@ import { useAdvertiser } from "auth/hooks/queries/useAdvertiser";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import { useUser } from "auth/hooks/queries/useUser";
import { FilterContext } from "state/context";
+import { CampaignFormat, CampaignSource } from "graphql/types";
interface Props {
- campaignFragment?: CampaignFragment | null;
- useChip?: boolean;
+ campaign?: CampaignSummaryFragment;
disabled?: boolean;
}
-export function CloneCampaign({ campaignFragment, useChip, disabled }: Props) {
+export function CloneCampaign({ campaign, disabled }: Props) {
const { advertiser } = useAdvertiser();
const { fromDate } = useContext(FilterContext);
const { userId } = useUser();
const history = useHistory();
const [open, setOpen] = useState(false);
+ const [getCampaign, { loading: getLoading }] = useLoadCampaignLazyQuery();
const [copyCampaign, { loading }] = useCreateCampaignMutation({
refetchQueries: [
{
@@ -54,59 +56,72 @@ export function CloneCampaign({ campaignFragment, useChip, disabled }: Props) {
},
});
+ const doClone = async () => {
+ if (campaign) {
+ getCampaign({
+ variables: { id: campaign.id },
+ onCompleted(data) {
+ if (data.campaign) {
+ copyCampaign({
+ variables: {
+ input: createCampaignFromFragment(data.campaign, userId),
+ },
+ });
+ } else {
+ alert("Unable to clone campaign");
+ }
+ },
+ });
+ }
+ };
+
+ const canClone =
+ campaign &&
+ campaign.source === CampaignSource.SelfServe &&
+ [CampaignFormat.PushNotification, CampaignFormat.NewsDisplayAd].includes(
+ campaign.format,
+ );
return (
- {useChip ? (
- {
- setOpen(true);
- }}
- disabled={loading || !campaignFragment || disabled}
- icon={}
- />
- ) : (
-
- )}
+
+
+
+
+
);
diff --git a/src/components/Datagrid/CustomToolbar.tsx b/src/components/Datagrid/CustomToolbar.tsx
new file mode 100644
index 000000000..515200eb6
--- /dev/null
+++ b/src/components/Datagrid/CustomToolbar.tsx
@@ -0,0 +1,20 @@
+import {
+ GridToolbarColumnsButton,
+ GridToolbarContainer,
+ GridToolbarFilterButton,
+ GridToolbarQuickFilter,
+} from "@mui/x-data-grid";
+import { Box } from "@mui/material";
+import { PropsWithChildren } from "react";
+
+export function CustomToolbar({ children }: PropsWithChildren) {
+ return (
+
+ {children}
+
+
+
+
+
+ );
+}
diff --git a/src/components/EnhancedTable/renderers.tsx b/src/components/Datagrid/renderers.tsx
similarity index 93%
rename from src/components/EnhancedTable/renderers.tsx
rename to src/components/Datagrid/renderers.tsx
index 5594de06e..78efd319a 100644
--- a/src/components/EnhancedTable/renderers.tsx
+++ b/src/components/Datagrid/renderers.tsx
@@ -1,8 +1,7 @@
import { Box, Tooltip } from "@mui/material";
import _ from "lodash";
import { format, formatDistanceToNow, parseISO } from "date-fns";
-import { CellValue } from "./EnhancedTable";
-import { ReactChild, ReactNode, useContext } from "react";
+import { ReactElement, ReactNode, useContext } from "react";
import { formatInTimeZone } from "date-fns-tz";
import enUS from "date-fns/locale/en-US";
import {
@@ -12,18 +11,18 @@ import {
useUpdateCampaignMutation,
} from "graphql/campaign.generated";
import { useUpdateAdSetMutation } from "graphql/ad-set.generated";
-import { OnOff } from "../Switch/OnOff";
+import { OnOff } from "components/Switch/OnOff";
import { displayFromCampaignState } from "util/displayState";
import { CampaignExtras } from "user/adSet/AdSetList";
import { FilterContext } from "state/context";
import { refetchAdvertiserCampaignsQuery } from "graphql/advertiser.generated";
import { UpdateAdSetInput } from "graphql/types";
-export type CellValueRenderer = (value: CellValue) => ReactNode;
+export type CellValueRenderer = (value: any) => ReactNode;
const ADS_DEFAULT_TIMEZONE = "America/New_York";
const TOOLTIP_FORMAT = "E d LLL yyyy HH:mm:ss zzz";
-function formatDateForTooltip(dt: Date): ReactChild {
+function formatDateForTooltip(dt: Date): ReactElement {
return (
<>
diff --git a/src/components/EnhancedTable/EnhancedTable.tsx b/src/components/EnhancedTable/EnhancedTable.tsx
deleted file mode 100644
index dd6716d8d..000000000
--- a/src/components/EnhancedTable/EnhancedTable.tsx
+++ /dev/null
@@ -1,258 +0,0 @@
-import {
- Box,
- Grid,
- Skeleton,
- SortDirection,
- Stack,
- Table,
- TableBody,
- TableCell,
- TableCellProps,
- TableContainer,
- TableFooter,
- TableHead,
- TablePagination,
- TableRow,
-} from "@mui/material";
-import _ from "lodash";
-import { ChangeEvent, ReactNode, useState } from "react";
-import { EnhancedTableHeader } from "./EnhancedTableHeader";
-import { EnhancedTableRow } from "./EnhancedTableRow";
-import { FilterInput } from "./FilterInput";
-import { CellValueRenderer, StandardRenderers } from "./renderers";
-import { SxProps } from "@mui/system";
-
-export type CellValue = string | string[] | number | boolean | undefined | null;
-
-export interface ColumnDescriptor {
- // text title to be shown in header
- title: string;
-
- // how to obtain the raw cell value from the row
- // this is used both to pass to a CellValueRenderer, and used for
- // filtering and sorting
- value: (row: T) => CellValue;
-
- // how to convert the raw cell value into a react node
- // default: StandardRenderers.string
- renderer?: CellValueRenderer;
-
- // in some cases rendering a cell is not just a matter of taking the value and presenting it.
- // for example, rendering a link typically requires the cell value and also the id.
- // so an extendedRenderer, if defined, is used instead, which has access to the full row,
- // but is less genericisable.
- extendedRenderer?: undefined | ((row: T) => ReactNode);
-
- // is this column sortable?
- // default: true
- sortable?: boolean;
-
- // style to apply to this column's header and cells
- // use this to e.g. make fixed width using maxWidth or width
- sx?: SxProps;
-
- // default left
- align?: TableCellProps["align"];
-}
-
-interface EnhancedTableProps {
- rows: T[] | undefined;
- columns: ColumnDescriptor[];
- // defaults to 0
- initialSortColumn?: number;
- initialSortDirection?: SortDirection;
- initialRowsPerPage?: 5 | 10 | 25 | 100 | -1;
-
- additionalFilters?: ReactNode;
- filterable?: boolean;
-}
-
-// internally, we use a column accessor rather than the descriptor directly.
-// This has default values populated and some helper methods
-export interface ColumnAccessor
- extends Pick, "title" | "sx" | "align"> {
- sortable: boolean;
- render: (r: T) => ReactNode;
- getSortValue: (r: T) => string | number | undefined;
- matchesFilter: (r: T, filter: string) => boolean;
-}
-
-function mkColumnAccessor(
- descriptor: ColumnDescriptor,
-): ColumnAccessor {
- const {
- title,
- value,
- renderer = StandardRenderers.string,
- extendedRenderer,
- sortable = true,
- sx,
- align,
- } = descriptor;
- return {
- title,
- sortable,
- sx,
- align,
- render: (row) => {
- return extendedRenderer ? extendedRenderer(row) : renderer(value(row));
- },
- getSortValue: (row) => {
- const v = value(row);
- if (_.isString(v)) {
- return v.toLowerCase().trim();
- }
- if (_.isNumber(v)) {
- return v;
- }
- if (_.isBoolean(v)) {
- return v ? 1 : 0;
- }
- return undefined;
- },
- matchesFilter: (row, filter) => {
- const columnValue = value(row);
- return (
- _.isString(columnValue) && columnValue.toLowerCase().includes(filter)
- );
- },
- };
-}
-
-const LoadingSkeleton = ({ cols, rows }: { cols: number; rows: number }) => {
- return (
- <>
- {_.times(rows, (idx) => (
-
- {_.times(cols, (idx2) => (
-
-
-
- ))}
-
- ))}
- >
- );
-};
-
-export function EnhancedTable(props: EnhancedTableProps) {
- const [order, setOrder] = useState(
- props.initialSortDirection ?? "asc",
- );
- const [orderBy, setOrderBy] = useState(props.initialSortColumn ?? 0);
- const [filter, setFilter] = useState("");
- const [page, setPage] = useState(0);
- const [rowsPerPage, setRowsPerPage] = useState(
- props.initialRowsPerPage ?? 25,
- );
-
- const { rows } = props;
-
- const columns = props.columns.map(mkColumnAccessor);
-
- const processSortUpdate = (col: number) => {
- if (orderBy === col) {
- setOrder((current) => (current === "asc" ? "desc" : "asc"));
- } else {
- setOrderBy(col);
- setOrder("asc");
- setPage(0);
- }
- };
-
- const handleChangePage = (_event: unknown, newPage: number) => {
- setPage(newPage);
- };
-
- const handleChangeRowsPerPage = (event: ChangeEvent) => {
- setRowsPerPage(parseInt(event.target.value, 10));
- setPage(0);
- };
-
- const handleFilterUpdate = (newFilter: string) => {
- if (!_.isEmpty(newFilter)) {
- setPage(0);
- }
- setFilter(newFilter.toLowerCase());
- };
-
- const matchesFilter = (row: T) => {
- if (_.isEmpty(filter)) return true;
- return _.some(columns, (c) => c.matchesFilter(row, filter));
- };
-
- const filteredRows = _(rows)
- .filter((r) => matchesFilter(r))
- .orderBy((r) => columns[orderBy].getSortValue(r), order)
- .valueOf();
-
- const data =
- rowsPerPage === -1
- ? filteredRows
- : filteredRows.slice(
- page * rowsPerPage,
- page * rowsPerPage + rowsPerPage,
- );
-
- return (
-
-
-
- {props.additionalFilters}
-
- {props.filterable !== false && (
-
-
-
- )}
-
-
-
-
-
- {columns.map((c, idx) => (
- processSortUpdate(idx)}
- />
- ))}
-
-
-
- {_.isNil(rows) ? (
- 0 ? rowsPerPage : 20}
- />
- ) : (
- data.map((r, idx) => (
- // TODO: key here should be the idenfier of the row
-
- ))
- )}
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/components/EnhancedTable/EnhancedTableHeader.tsx b/src/components/EnhancedTable/EnhancedTableHeader.tsx
deleted file mode 100644
index d921721ba..000000000
--- a/src/components/EnhancedTable/EnhancedTableHeader.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { SortDirection, TableCell, TableSortLabel } from "@mui/material";
-import { ColumnAccessor } from "./EnhancedTable";
-
-interface EnhancedTableHeaderProps {
- column: ColumnAccessor;
- sortDirection: SortDirection;
- onSortClick: () => void;
-}
-
-export function EnhancedTableHeader(props: EnhancedTableHeaderProps) {
- return (
-
- {props.column.sortable ? (
-
- {props.column.title}
-
- ) : (
- props.column.title
- )}
-
- );
-}
diff --git a/src/components/EnhancedTable/EnhancedTableRow.tsx b/src/components/EnhancedTable/EnhancedTableRow.tsx
deleted file mode 100644
index 07b3c475d..000000000
--- a/src/components/EnhancedTable/EnhancedTableRow.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { TableCell, TableRow } from "@mui/material";
-import { ColumnAccessor } from "./EnhancedTable";
-
-interface EnhancedTableRowProps {
- columns: ColumnAccessor[];
- row: T;
-}
-
-export function EnhancedTableRow({
- columns,
- row,
-}: EnhancedTableRowProps) {
- return (
-
- {columns.map((c, idx) => (
-
- {c.render(row)}
-
- ))}
-
- );
-}
diff --git a/src/components/EnhancedTable/FilterInput.tsx b/src/components/EnhancedTable/FilterInput.tsx
deleted file mode 100644
index ca6c786e1..000000000
--- a/src/components/EnhancedTable/FilterInput.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { Box, TextField } from "@mui/material";
-import SearchIcon from "@mui/icons-material/Search";
-import { useMemo, useState } from "react";
-import _ from "lodash";
-
-interface Props {
- filter: string;
- setFilter: (newValue: string) => void;
-}
-
-export const FilterInput = (props: Props) => {
- // to retain responsiveness of the form, whilst still not constantly re-filtering the list,
- // we keep our own state here that updates immediately, and debounce the updates to
- // the caller
- const [filterField, setFilterField] = useState(props.filter);
-
- // HT: https://dmitripavlutin.com/react-throttle-debounce/
- const debouncedChangeHandler = useMemo(
- () => _.debounce(props.setFilter, 300),
- [props.setFilter],
- );
-
- const onChangeHandler = (value: string) => {
- setFilterField(value);
- debouncedChangeHandler(value);
- };
-
- return (
-
-
- onChangeHandler(e.target.value)}
- spellCheck={false}
- autoFocus
- fullWidth
- />
-
- );
-};
diff --git a/src/components/EnhancedTable/index.ts b/src/components/EnhancedTable/index.ts
deleted file mode 100644
index b0db561e5..000000000
--- a/src/components/EnhancedTable/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from "./EnhancedTable";
-export { StandardRenderers } from "./renderers";
diff --git a/src/user/User.tsx b/src/user/User.tsx
index d6adea0ed..38bf05c23 100644
--- a/src/user/User.tsx
+++ b/src/user/User.tsx
@@ -51,77 +51,76 @@ export function User() {
setFromDate,
}}
>
-
-
-
-
-
- {/* /adsmanager */}
-
- a.selfServiceManageCampaign
- }
- />
-
-
- a.selfServiceManageCampaign
- }
- />
-
-
- a.selfServiceManageCampaign
- }
- />
-
-
-
- {/* /campaigns/:campaignId/analytics - */}
-
-
-
-
-
-
-
-
-
-
-
-
- {/* default */}
-
-
-
+
+
+
+
+ {/* /adsmanager */}
+ a.selfServiceManageCampaign}
+ />
+
+ a.selfServiceManageCampaign}
+ />
+
+ a.selfServiceManageCampaign}
+ />
+
+
+
+ {/* /campaigns/:campaignId/analytics - */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* default */}
+
+
diff --git a/src/user/adSet/AdSetList.tsx b/src/user/adSet/AdSetList.tsx
index fcfabef2d..06c5cee6d 100644
--- a/src/user/adSet/AdSetList.tsx
+++ b/src/user/adSet/AdSetList.tsx
@@ -1,8 +1,10 @@
-import { ColumnDescriptor, StandardRenderers } from "components/EnhancedTable";
import { Chip } from "@mui/material";
import { Status } from "components/Campaigns/Status";
import _ from "lodash";
-import { adSetOnOffState } from "components/EnhancedTable/renderers";
+import {
+ adSetOnOffState,
+ StandardRenderers,
+} from "components/Datagrid/renderers";
import { CampaignAdsFragment } from "graphql/campaign.generated";
import { CampaignSource } from "graphql/types";
import { StatsMetric } from "user/analytics/analyticsOverview/types";
@@ -10,6 +12,7 @@ import { AdSetFragment } from "graphql/ad-set.generated";
import { AdDetailTable } from "user/views/user/AdDetailTable";
import { displayFromCampaignState } from "util/displayState";
import { uiLabelsForBillingType } from "util/billingType";
+import { GridColDef } from "@mui/x-data-grid";
interface Props {
loading: boolean;
@@ -71,52 +74,70 @@ export function AdSetList({ campaign, loading, engagements }: Props) {
advertiserId: campaign?.advertiser.id ?? "",
}));
- const columns: ColumnDescriptor[] = [
+ const columns: GridColDef[] = [
{
- title: "On/Off",
- value: (c) => c.state,
- extendedRenderer: (r) => adSetOnOffState(r),
- sx: { width: "10px" },
+ field: "switch",
+ type: "actions",
+ headerName: "On/Off",
+ valueGetter: ({ row }) => row.state,
+ renderCell: ({ row }) => adSetOnOffState(row),
sortable: false,
+ filterable: false,
+ width: 100,
},
{
- title: "Created",
- value: (c) => c.createdAt,
- renderer: StandardRenderers.date,
+ field: "createdAt",
+ headerName: "Created",
+ valueGetter: ({ row }) => row.createdAt,
+ renderCell: ({ row }) => StandardRenderers.date(row.createdAt),
+ width: 120,
},
{
- title: "Name",
- value: (c) => c.name || c.id.substring(0, 8),
+ field: "name",
+ headerName: "Name",
+ valueGetter: ({ row }) => row.name || row.id.substring(0, 8),
+ flex: 1,
},
{
- title: "Status",
- value: (c) => displayFromCampaignState(c),
- extendedRenderer: (r) => (
+ field: "state",
+ headerName: "Status",
+ valueGetter: ({ row }) => displayFromCampaignState(row),
+ renderCell: ({ row }) => (
),
+ width: 100,
},
{
- title: "Type",
- value: (c) => uiLabelsForBillingType(c.billingType).longLabel,
+ field: "billingType",
+ headerName: "Type",
+ valueGetter: ({ row }) =>
+ uiLabelsForBillingType(row.billingType).longLabel,
+ width: 150,
},
{
- title: "Platforms",
- value: (c) => c.oses?.map((o: { name: string }) => o.name).join(", "),
- extendedRenderer: (r) => ,
+ field: "oses",
+ headerName: "Platforms",
+ valueGetter: ({ row }) =>
+ row.oses?.map((o: { name: string }) => o.name).join(", "),
+ renderCell: ({ row }) => ,
+ flex: 1,
},
{
- title: "Audiences",
- value: (c) => c.segments?.map((o: { name: string }) => o.name).join(", "),
- extendedRenderer: (r) => (
+ field: "segments",
+ headerName: "Audiences",
+ valueGetter: ({ row }) =>
+ row.segments?.map((o: { name: string }) => o.name).join(", "),
+ renderCell: ({ row }) => (
100 ? 2 : 5}
+ items={row.segments}
+ max={(row.segments ?? []).join("").length > 100 ? 2 : 5}
/>
),
+ flex: 1,
},
];
diff --git a/src/user/ads/AdList.tsx b/src/user/ads/AdList.tsx
index 6ab70be35..06485a379 100644
--- a/src/user/ads/AdList.tsx
+++ b/src/user/ads/AdList.tsx
@@ -1,4 +1,3 @@
-import { ColumnDescriptor, StandardRenderers } from "components/EnhancedTable";
import _ from "lodash";
import { isAfterEndDate } from "util/isAfterEndDate";
import { AdFragment } from "graphql/ad-set.generated";
@@ -6,6 +5,9 @@ import { CampaignSource } from "graphql/types";
import { CampaignAdsFragment } from "graphql/campaign.generated";
import { StatsMetric } from "user/analytics/analyticsOverview/types";
import { AdDetailTable } from "user/views/user/AdDetailTable";
+import { GridColDef } from "@mui/x-data-grid";
+import { CreativeFragment } from "graphql/creative.generated";
+import { StandardRenderers } from "components/Datagrid/renderers";
interface Props {
campaign?: CampaignAdsFragment | null;
@@ -44,27 +46,36 @@ export function AdList({ campaign, loading, engagements }: Props) {
const ads: AdDetails[] = _.flatMap(adSets, "ads");
- const columns: ColumnDescriptor[] = [
+ const columns: GridColDef[] = [
{
- title: "Created",
- value: (c) => c.creative.createdAt,
- renderer: StandardRenderers.date,
+ field: "createdAt",
+ headerName: "Created",
+ valueGetter: ({ row }) => row.creative.createdAt,
+ renderCell: ({ row }) => StandardRenderers.date(row.creative.createdAt),
},
{
- title: "Ad Name",
- value: (c) => c.creative.name,
+ field: "name",
+ headerName: "Ad Name",
+ valueGetter: ({ row }) => row.creative.name,
+ flex: 1,
},
{
- title: "Ad Set Name",
- value: (c) => c.adSetName,
+ field: "adSetName",
+ headerName: "Ad Set Name",
+ valueGetter: ({ row }) => row.adSetName,
+ flex: 1,
},
{
- title: "Title",
- value: (c) => c.creative.payloadNotification?.title,
+ field: "title",
+ headerName: "Title",
+ valueGetter: ({ row }) => title(row.creative),
+ flex: 1,
},
{
- title: "Body",
- value: (c) => c.creative.payloadNotification?.body,
+ field: "body",
+ headerName: "Body",
+ valueGetter: ({ row }) => body(row.creative),
+ flex: 1,
},
];
@@ -74,9 +85,18 @@ export function AdList({ campaign, loading, engagements }: Props) {
columns={columns}
engagements={engagements}
loading={loading}
- propOverride={{
- initialSortColumn: 0,
- }}
/>
);
}
+
+const title = (c: CreativeFragment) =>
+ c.payloadNotification?.title ??
+ c.payloadInlineContent?.title ??
+ c.payloadSearch?.title ??
+ c.payloadSearchHomepage?.title;
+
+const body = (c: CreativeFragment) =>
+ c.payloadNotification?.body ??
+ c.payloadInlineContent?.ctaText ??
+ c.payloadSearch?.body ??
+ c.payloadSearchHomepage?.body;
diff --git a/src/user/analytics/analyticsOverview/components/BaseBarChart.tsx b/src/user/analytics/analyticsOverview/components/BaseBarChart.tsx
index 778d6fe78..044334ddb 100644
--- a/src/user/analytics/analyticsOverview/components/BaseBarChart.tsx
+++ b/src/user/analytics/analyticsOverview/components/BaseBarChart.tsx
@@ -1,8 +1,7 @@
import { Box, Tab, Tabs } from "@mui/material";
-import HighchartsReact from "highcharts-react-official";
-import * as Highcharts from "highcharts";
import { Options, SeriesOptionsType } from "highcharts";
import { Option } from "../types";
+import { HighchartsWrapper } from "user/analytics/analyticsOverview/components/HighchartsWrapper";
interface Props {
categories: string[];
@@ -81,7 +80,7 @@ export function BaseBarChart({
))}
-
+
);
}
diff --git a/src/user/analytics/analyticsOverview/components/BasePieChart.tsx b/src/user/analytics/analyticsOverview/components/BasePieChart.tsx
index d1ee809c4..8c79f26f5 100644
--- a/src/user/analytics/analyticsOverview/components/BasePieChart.tsx
+++ b/src/user/analytics/analyticsOverview/components/BasePieChart.tsx
@@ -1,8 +1,7 @@
import { Box, Tab, Tabs } from "@mui/material";
-import HighchartsReact from "highcharts-react-official";
-import * as Highcharts from "highcharts";
import { Options, SeriesOptionsType } from "highcharts";
import { Option } from "../types";
+import { HighchartsWrapper } from "user/analytics/analyticsOverview/components/HighchartsWrapper";
interface Props {
series: SeriesOptionsType[];
@@ -71,7 +70,7 @@ export function BasePieChart({ series, onSetType, extraOptions, type }: Props) {
))}
-
+
);
}
diff --git a/src/user/analytics/analyticsOverview/components/HighchartsWrapper.tsx b/src/user/analytics/analyticsOverview/components/HighchartsWrapper.tsx
new file mode 100644
index 000000000..96d0e917d
--- /dev/null
+++ b/src/user/analytics/analyticsOverview/components/HighchartsWrapper.tsx
@@ -0,0 +1,27 @@
+import { Box, BoxProps } from "@mui/material";
+import Highcharts from "highcharts";
+import { HighchartsReact } from "highcharts-react-official";
+
+type Props = BoxProps & {
+ options: Highcharts.Options;
+};
+
+export const HighchartsWrapper = ({ options, ...rest }: Props) => {
+ return (
+
+
+
+ );
+};
diff --git a/src/user/analytics/analyticsOverview/lib/overview.library.ts b/src/user/analytics/analyticsOverview/lib/overview.library.ts
index 3eef24c38..27a64b168 100644
--- a/src/user/analytics/analyticsOverview/lib/overview.library.ts
+++ b/src/user/analytics/analyticsOverview/lib/overview.library.ts
@@ -1,6 +1,6 @@
import _ from "lodash";
import moment from "moment";
-import { Options } from "highcharts";
+import Highcharts, { Options } from "highcharts";
import {
BaseMetric,
Metrics,
@@ -76,7 +76,7 @@ export const baseOverviewChart: Options = {
export const prepareChart = (
metrics: Metrics,
processedData: MetricDataSet,
-) => {
+): Highcharts.Options => {
const metricsEntries = Object.entries(metrics);
return {
...baseOverviewChart,
diff --git a/src/user/analytics/analyticsOverview/reports/campaign/EngagementsOverview.tsx b/src/user/analytics/analyticsOverview/reports/campaign/EngagementsOverview.tsx
index 987ba58d9..c778024a7 100644
--- a/src/user/analytics/analyticsOverview/reports/campaign/EngagementsOverview.tsx
+++ b/src/user/analytics/analyticsOverview/reports/campaign/EngagementsOverview.tsx
@@ -1,6 +1,4 @@
import { Alert, Box, Divider, Skeleton } from "@mui/material";
-import HighchartsReact from "highcharts-react-official";
-import * as Highcharts from "highcharts";
import { useState } from "react";
import {
CampaignWithEngagementsFragment,
@@ -18,6 +16,7 @@ import { CampaignFormat } from "graphql/types";
import { ErrorDetail } from "components/Error/ErrorDetail";
import { ApolloError } from "@apollo/client";
import { usePersistMetricFilter } from "user/analytics/analyticsOverview/hooks/usePersistMetricFilter";
+import { HighchartsWrapper } from "user/analytics/analyticsOverview/components/HighchartsWrapper";
interface Props {
loading: boolean;
@@ -100,9 +99,7 @@ export function EngagementsOverview({
paddingTop="14px"
paddingBottom="14px"
>
-
-
-
+
diff --git a/src/user/analytics/analyticsOverview/reports/creative/CreativeOverview.tsx b/src/user/analytics/analyticsOverview/reports/creative/CreativeOverview.tsx
deleted file mode 100644
index 971441008..000000000
--- a/src/user/analytics/analyticsOverview/reports/creative/CreativeOverview.tsx
+++ /dev/null
@@ -1,172 +0,0 @@
-import * as Highcharts from "highcharts";
-import { Options } from "highcharts";
-import HighchartsReact from "highcharts-react-official";
-import { useState } from "react";
-import { Box, Stack, Tab, Tabs, Typography } from "@mui/material";
-import { decideValueAttribute } from "../../lib/overview.library";
-import { CampaignFormat } from "graphql/types";
-import { EngagementFragment } from "graphql/analytics-overview.generated";
-import { CampaignFragment } from "graphql/campaign.generated";
-import { creativeEngagements } from "../../lib/creative.library";
-import { CreativeMetric, StatsMetric } from "../../types";
-import { CardContainer } from "components/Card/CardContainer";
-
-interface Props {
- engagements: EngagementFragment[];
- campaign: CampaignFragment;
-}
-
-export function CreativeOverview({ engagements, campaign }: Props) {
- const [type, setType] = useState("ctr");
- const isNtp = campaign.format === CampaignFormat.NtpSi;
-
- const metrics = creativeEngagements(engagements, campaign.format);
-
- const mapMetricToSeries = (cm: CreativeMetric[], type: keyof StatsMetric) => {
- return cm
- .map((m) => {
- const p = m.creativePayload;
- const y = m[type];
-
- return {
- name: p.title,
- y: y === Infinity ? 0 : y,
- custom: {
- name: p.body,
- },
- };
- })
- .filter((p) => p.y > 0);
- };
-
- const [chart, setChart] = useState(mapMetricToSeries(metrics, "ctr"));
-
- const attrs = decideValueAttribute(type);
- const options: Options = {
- chart: {
- type: "bar",
- height: chart.length > 10 ? chart.length * 30 : undefined,
- },
- title: {
- text: undefined,
- },
- xAxis: {
- categories: chart.map((c) => c.name),
- title: {
- text: null,
- },
- },
- accessibility: {
- point: {
- valueSuffix: attrs.suffix,
- valuePrefix: attrs.prefix,
- },
- },
- yAxis: {
- labels: {
- overflow: "justify",
- },
- },
- plotOptions: {
- bar: {
- colorByPoint: true,
- dataLabels: {
- enabled: true,
- inside: true,
- },
- },
- },
- tooltip: {
- valueDecimals: attrs.decimal,
- valuePrefix: attrs.prefix,
- valueSuffix: attrs.suffix,
- },
- series: [
- {
- name: "Ads",
- type: "bar",
- data: chart,
- dataLabels: [
- {
- align: "center",
- format: isNtp ? "" : "{point.options.custom.name}",
- color: "#FFFFFF",
- style: {
- fontSize: "12px",
- fontFamily: "Verdana, sans-serif",
- },
- },
- {
- align: "right",
- format: attrs.format,
- color: "#FFFFFF",
- style: {
- fontSize: "12px",
- fontFamily: "Verdana, sans-serif",
- },
- },
- ],
- },
- ],
- };
-
- const onChange = (metric: CreativeMetric[], type: keyof StatsMetric) => {
- setType(type);
- setChart(mapMetricToSeries(metric, type));
- };
-
- return (
-
- {isNtp && (
-
- {metrics.map((ntp, idx) => (
-
-
- Image {idx + 1}
-
- ))}
-
- )}
-
- {
- onChange(metrics, v);
- }}
- variant="scrollable"
- scrollButtons="auto"
- >
-
-
-
- {!isNtp && }
-
-
-
- {!isNtp && }
-
-
-
-
- );
-}
diff --git a/src/user/analytics/renderers/index.tsx b/src/user/analytics/renderers/index.tsx
index 660296ba0..f23a8f65b 100644
--- a/src/user/analytics/renderers/index.tsx
+++ b/src/user/analytics/renderers/index.tsx
@@ -1,5 +1,5 @@
import { Box, Skeleton, Typography } from "@mui/material";
-import { renderMonetaryAmount } from "components/EnhancedTable/renderers";
+import { renderMonetaryAmount } from "components/Datagrid/renderers";
import { CampaignSummaryFragment } from "graphql/campaign.generated";
import { CampaignFormat } from "graphql/types";
import { StatsMetric } from "user/analytics/analyticsOverview/types";
diff --git a/src/user/campaignList/CampaignList.tsx b/src/user/campaignList/CampaignList.tsx
index 668aa60a9..b4f3b1497 100644
--- a/src/user/campaignList/CampaignList.tsx
+++ b/src/user/campaignList/CampaignList.tsx
@@ -1,14 +1,10 @@
import { useState } from "react";
-import {
- ColumnDescriptor,
- EnhancedTable,
- StandardRenderers,
-} from "components/EnhancedTable";
-import { Checkbox, Link } from "@mui/material";
+import { Link } from "@mui/material";
import {
campaignOnOffState,
renderMonetaryAmount,
-} from "components/EnhancedTable/renderers";
+ StandardRenderers,
+} from "components/Datagrid/renderers";
import { Link as RouterLink } from "react-router-dom";
import { Status } from "components/Campaigns/Status";
import { isAfterEndDate } from "util/isAfterEndDate";
@@ -22,19 +18,17 @@ import {
import _ from "lodash";
import { uiTextForCampaignFormat } from "user/library";
import { CampaignSummaryFragment } from "graphql/campaign.generated";
+import { DataGrid, GridColDef } from "@mui/x-data-grid";
+import { CustomToolbar } from "components/Datagrid/CustomToolbar";
+import { CloneCampaign } from "components/Campaigns/CloneCampaign";
+import { EditButton } from "user/campaignList/EditButton";
interface Props {
advertiser?: AdvertiserCampaignsFragment | null;
- selectedCampaigns: string[];
- onCampaignSelect: (c: string, insert: boolean) => void;
}
-export function CampaignList({
- advertiser,
- selectedCampaigns,
- onCampaignSelect,
-}: Props) {
- let initialSort = 9;
+export function CampaignList({ advertiser }: Props) {
+ const [selectedCampaign, setSelectedCampaign] = useState();
const [engagementData, setEngagementData] =
useState