Skip to content

Commit

Permalink
Merge pull request #265 from AmbireTech/block-pacements
Browse files Browse the repository at this point in the history
Block pacements
  • Loading branch information
ivopaunov authored Aug 14, 2024
2 parents 1079491 + 8ba3c85 commit 620517f
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 129 deletions.
5 changes: 3 additions & 2 deletions src/components/CampaignAnalytics/CampaignAnalytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ const CampaignAnalytics = ({ isAdminPanel = false }: { isAdminPanel?: boolean })
)
}

if (!id) {
if (!id || (!loading && !campaign)) {
return <div>Invalid campaign ID</div>
}

Expand Down Expand Up @@ -192,13 +192,14 @@ const CampaignAnalytics = ({ isAdminPanel = false }: { isAdminPanel?: boolean })
currencyName={currencyName}
/>
)}
{!loading && activeTab === 'hostname' && (
{!loading && campaign && activeTab === 'hostname' && (
<Placements
placements={campaignMappedAnalytics}
currencyName={currencyName}
// NOTE: currently we have only have one placement per campaign
// TODO; this can be get from analytics but that means 2x request to validator
placement={campaign?.targetingInput.inputs.placements.in[0] || 'site'}
campaign={campaign}
/>
)}
{!loading && activeTab === 'country' && (
Expand Down
85 changes: 70 additions & 15 deletions src/components/CampaignAnalytics/Placements.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { Placement } from 'adex-common'
import CustomTable from 'components/common/CustomTable'
import { Campaign, CampaignStatus, Placement } from 'adex-common'
import CustomTable, { TableElement, TableRowAction } from 'components/common/CustomTable'
import { getHumneSrcName } from 'helpers'
import { useMemo } from 'react'
import { BaseAnalyticsData } from 'types'
import InvisibilityIcon from 'resources/icons/Invisibility'
import VisibilityIcon from 'resources/icons/Visibility'
import { useCampaignsData } from 'hooks/useCampaignsData'

const Placements = ({
placements,
currencyName,
placement
placement,
campaign
}: {
placements: BaseAnalyticsData[] | undefined
currencyName: string
placement: Placement
campaign: Campaign
}) => {
const { toggleBlockedSource } = useCampaignsData()

if (!placements?.length) {
return <div>No placement found</div>
}
Expand All @@ -29,20 +36,68 @@ const Placements = ({
[placement]
)

const elements = useMemo(
type PlacementsTableElement = Omit<TableElement, 'actionData'> & {
actionData: {
placementName: string
isBlocked: boolean
segment: string
}
id: string
placementName: string
impressions: string
clicks: string
ctr: string
avgCpm: string
paid: string
}

const elements: PlacementsTableElement[] = useMemo(
() =>
placements?.map((item) => ({
id: item.segment,
segment: getHumneSrcName(item.segment, placement),
impressions: item.impressions.toLocaleString(),
clicks: item.clicks.toLocaleString(),
ctr: `${item.ctr} %`,
avgCpm: `${item.avgCpm} ${currencyName}`,
paid: `${item.paid.toFixed(4)} ${currencyName}`
})) || [],
[placements, placement, currencyName]
placements?.map((item) => {
const isBlocked = campaign.targetingInput.inputs.publishers.nin.includes(item.segment)
const placementName = getHumneSrcName(item.segment, placement)
const data: PlacementsTableElement = {
rowColor: isBlocked ? 'red' : 'inherit',
actionData: {
placementName,
isBlocked,
segment: item.segment
},
id: item.segment,
placementName,
impressions: item.impressions.toLocaleString(),
clicks: item.clicks.toLocaleString(),
ctr: `${item.ctr} %`,
avgCpm: `${item.avgCpm} ${currencyName}`,
paid: `${item.paid.toFixed(4)} ${currencyName}`
}

return data
}) || [],
[placements, campaign.targetingInput.inputs.publishers.nin, placement, currencyName]
)
return <CustomTable headings={headings} elements={elements} />

const actions = useMemo(() => {
const placementActions: TableRowAction[] = [
CampaignStatus.active,
CampaignStatus.paused
].includes(campaign.status)
? [
{
action: (props: PlacementsTableElement['actionData']) =>
toggleBlockedSource(campaign.id, props.placementName, props.segment),
label: ({ isBlocked, placementName }: PlacementsTableElement['actionData']) =>
`${isBlocked ? 'Unblock' : 'Block'} "${placementName}"`,
icon: ({ isBlocked }: PlacementsTableElement['actionData']) =>
isBlocked ? <VisibilityIcon size="inherit" /> : <InvisibilityIcon size="inherit" />
}
]
: []

return placementActions
}, [campaign.id, campaign.status, toggleBlockedSource])

return <CustomTable headings={headings} elements={elements} actions={actions} />
}

export default Placements
77 changes: 19 additions & 58 deletions src/components/EditCampaign/EditCampaign.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import {
getRecommendedCPMRange
} from 'helpers/createCampaignHelpers'
import useAccount from 'hooks/useAccount'
import { useAdExApi } from 'hooks/useAdexServices'
import useCustomNotifications from 'hooks/useCustomNotifications'
import { useCallback, useEffect, useMemo } from 'react'
import { useCampaignsData } from 'hooks/useCampaignsData'
import type {
Expand Down Expand Up @@ -57,12 +55,10 @@ type FormProps = {
}

const EditCampaign = ({ campaign }: { campaign: Campaign }) => {
const { adexServicesRequest } = useAdExApi()
const { showNotification } = useCustomNotifications()
const {
adexAccount: { balanceToken }
} = useAccount()
const { updateCampaignDataById, supplyStats } = useCampaignsData()
const { supplyStats, editCampaign } = useCampaignsData()

const recommendedPaymentBounds = useMemo(
() => getRecommendedCPMRange(supplyStats, campaign),
Expand Down Expand Up @@ -211,73 +207,38 @@ const EditCampaign = ({ campaign }: { campaign: Campaign }) => {
[form]
)

const editCampaign = useCallback(
const handleEditCampaign = useCallback(
async (values: FormProps) => {
const impression = {
min: Number(
parseToBigNumPrecision(
Number(values.pricingBounds.IMPRESSION?.min) / 1000,
balanceToken.decimals
)
min: parseToBigNumPrecision(
Number(values.pricingBounds.IMPRESSION?.min) / 1000,
balanceToken.decimals
),
max: Number(
parseToBigNumPrecision(
Number(values.pricingBounds.IMPRESSION?.max) / 1000,
balanceToken.decimals
)
max: parseToBigNumPrecision(
Number(values.pricingBounds.IMPRESSION?.max) / 1000,
balanceToken.decimals
)
}

const body: FormProps = {
pricingBounds: {
CLICK: { min: 0, max: 0 },
IMPRESSION: impression
},
targetingInput: {
version: '1',
inputs: {
categories: values.targetingInput.inputs.categories,
location: values.targetingInput.inputs.location,
advanced: {
disableFrequencyCapping:
values.targetingInput.inputs.advanced.disableFrequencyCapping,
includeIncentivized: values.targetingInput.inputs.advanced.includeIncentivized,
limitDailyAverageSpending:
values.targetingInput.inputs.advanced.limitDailyAverageSpending
}
}
}
const pricingBounds: Partial<Campaign['pricingBounds']> = {
IMPRESSION: impression
}
const inputs: Partial<Campaign['targetingInput']['inputs']> = {
categories: values.targetingInput.inputs.categories,
location: values.targetingInput.inputs.location
}

try {
await adexServicesRequest('backend', {
route: `/dsp/campaigns/edit/${campaign.id}`,
method: 'PUT',
body,
headers: {
'Content-Type': 'application/json'
}
})
const { success } = await editCampaign(campaign.id, pricingBounds, inputs)
if (success) {
form.resetDirty()
showNotification('info', 'Successfully updated Campaign data!')
updateCampaignDataById(campaign.id)
} catch {
return showNotification('error', "Couldn't update the Campaign data!")
}
},
[
balanceToken.decimals,
adexServicesRequest,
campaign.id,
form,
showNotification,
updateCampaignDataById
]
[balanceToken.decimals, editCampaign, campaign.id, form]
)

const throttledSbm = useMemo(() => {
return throttle(editCampaign, 3000, { leading: true })
}, [editCampaign])
return throttle(handleEditCampaign, 3000, { leading: true })
}, [handleEditCampaign])

if (!campaign) return <div>Invalid Campaign ID</div>
return (
Expand Down
14 changes: 11 additions & 3 deletions src/components/common/CustomTable/CustomTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export type TableRowAction = {
action: (e: TableElement['actionData']) => any
label: ((e: TableElement['actionData']) => string) | string
color?: MantineColor
icon: ReactNode
icon: ((e: TableElement['actionData']) => ReactNode) | ReactNode
disabled?: (e?: TableElement['actionData']) => boolean
hide?: (e?: TableElement['actionData']) => boolean
}
Expand All @@ -53,6 +53,14 @@ const getLabel = (label: TableRowAction['label'], actionData: TableElement['acti
return label
}

const getIcon = (icon: TableRowAction['icon'], actionData: TableElement['actionData']) => {
if (typeof icon === 'function') {
return icon(actionData)
}

return icon
}

export const CustomTable = ({
headings,
elements,
Expand Down Expand Up @@ -100,7 +108,7 @@ export const CustomTable = ({
onClick={() => a.action(e.actionData || e)}
disabled={a.disabled?.(e.actionData || e)}
>
{a.icon}
{getIcon(a.icon, e.actionData)}
</ActionIcon>
</Tooltip>
)
Expand All @@ -123,7 +131,7 @@ export const CustomTable = ({
key={label}
leftSection={
<ThemeIcon size="20px" variant="transparent" color={a.color || 'mainText'}>
{a.icon}
{getIcon(a.icon, e.actionData)}
</ThemeIcon>
}
onClick={() => a.action(e.actionData || e)}
Expand Down
41 changes: 22 additions & 19 deletions src/contexts/AccountContext/AccountContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { AmbireLoginSDK } from '@ambire/login-sdk-core'
import { DAPP_ICON_PATH, DAPP_NAME, DEFAULT_CHAIN_ID } from 'constants/login'
import useCustomNotifications from 'hooks/useCustomNotifications'
import { fetchService, getReqErr, RequestOptions } from 'services'
import SuperJSON from 'superjson'

const ambireLoginSDK = new AmbireLoginSDK({
dappName: DAPP_NAME,
Expand All @@ -31,7 +32,7 @@ export const VALIDATOR_BASE_URL = process.env.REACT_APP_VALIDATOR_BASE_URL
const UNAUTHORIZED_ERR_STR = 'Unauthorized!'

console.log({ BACKEND_BASE_URL })
const processResponse = (res: any) => {
const processResponse = <R extends any>(res: Response): Promise<R> => {
// console.log('res', res)
if (res.status >= 200 && res.status < 400) {
return res.json()
Expand All @@ -49,8 +50,9 @@ const processResponse = (res: any) => {

type AdExService = 'backend' | 'validator'

type ApiRequestOptions<T> = Omit<RequestOptions<T>, 'url'> & {
type ApiRequestOptions = Omit<RequestOptions, 'url' | 'body'> & {
route: string
body?: BodyInit | object | string | null
noAuth?: boolean
onErrMsg?: string
}
Expand All @@ -64,10 +66,10 @@ interface IAccountContext {
disconnectWallet: () => void
updateAccessToken: () => Promise<any>
resetAdexAccount: () => void
adexServicesRequest: <T extends any>(
adexServicesRequest: <R extends any>(
service: AdExService,
reqOptions: ApiRequestOptions<T>
) => Promise<T>
reqOptions: ApiRequestOptions
) => Promise<R>
updateBalance: () => Promise<void>
updateBillingDetails: (billingDetails: BillingDetails) => Promise<void>
isLoading: boolean
Expand Down Expand Up @@ -235,7 +237,7 @@ const AccountProvider: FC<PropsWithChildren> = ({ children }) => {

const adexServicesRequest = useCallback(
// Note
async <T extends any>(service: AdExService, reqOptions: ApiRequestOptions<T>): Promise<T> => {
async <R extends any>(service: AdExService, reqOptions: ApiRequestOptions): Promise<R> => {
// temp hax for using the same token fot validator auth
const authHeaderProp = service === 'backend' ? 'X-DSP-AUTH' : 'authorization'

Expand All @@ -244,10 +246,10 @@ const AccountProvider: FC<PropsWithChildren> = ({ children }) => {
const baseUrl = (service === 'backend' ? BACKEND_BASE_URL : VALIDATOR_BASE_URL) || ''
const urlCheck = reqOptions.route.replace(baseUrl, '').replace(/^\//, '')

const req: RequestOptions<T> = {
const req: RequestOptions = {
url: `${baseUrl}/${urlCheck}`,
method: reqOptions.method,
body: reqOptions.body,
body: reqOptions.body ? JSON.stringify(SuperJSON.serialize(reqOptions.body).json) : null,
queryParams: reqOptions.queryParams,
headers: reqOptions.headers
}
Expand Down Expand Up @@ -277,16 +279,17 @@ const AccountProvider: FC<PropsWithChildren> = ({ children }) => {

console.log('req', req)

return fetchService(req)
.then(processResponse)
.catch((err) => {
console.log(err)
// TODO: better check
if (err && err.message && err.message.includes(UNAUTHORIZED_ERR_STR)) {
resetAdexAccount()
}
showNotification('error', err.message, reqOptions.onErrMsg || 'Data error')
})
try {
const res = await fetchService(req)
return await processResponse<R>(res)
} catch (err: any) {
console.log(err)
if (err && err.message && err.message.includes(UNAUTHORIZED_ERR_STR)) {
resetAdexAccount()
}
showNotification('error', err.message, reqOptions.onErrMsg || 'Data error')
return Promise.reject<R>()
}
},
[adexAccount.accessToken, resetAdexAccount, showNotification, updateAccessToken]
)
Expand Down Expand Up @@ -458,7 +461,7 @@ const AccountProvider: FC<PropsWithChildren> = ({ children }) => {
const updateBillingDetails = useCallback(
async (billingDetails: BillingDetails) => {
try {
const updated = await adexServicesRequest<unknown>('backend', {
const updated = await adexServicesRequest<{ success?: boolean }>('backend', {
route: '/dsp/accounts/billing-details',
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
Expand Down
Loading

0 comments on commit 620517f

Please sign in to comment.