Skip to content

Commit

Permalink
Add admin for multidimensional data pages
Browse files Browse the repository at this point in the history
  • Loading branch information
rakyi committed Jan 30, 2025
1 parent 0b15689 commit a6d1a53
Show file tree
Hide file tree
Showing 16 changed files with 731 additions and 291 deletions.
542 changes: 287 additions & 255 deletions adminSiteClient/AdminApp.tsx

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions adminSiteClient/AdminSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as React from "react"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js"
import {
faChartBar,
faChartLine,
faFile,
faTable,
faSkullCrossbones,
Expand Down Expand Up @@ -38,6 +39,11 @@ export const AdminSidebar = (): React.ReactElement => (
<FontAwesomeIcon icon={faPanorama} /> Narrative charts
</Link>
</li>
<li>
<Link to="/multi-dims">
<FontAwesomeIcon icon={faChartLine} /> Multi-dims
</Link>
</li>
<li>
<Link to="/posts">
<FontAwesomeIcon icon={faFile} /> Posts
Expand Down
5 changes: 2 additions & 3 deletions adminSiteClient/ChartViewIndexPage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { useCallback, useContext, useEffect, useMemo, useState } from "react"
import * as React from "react"
import { Button, Flex, Input, Space, Table } from "antd"
import { Button, Flex, Input, Space, Table, TableColumnsType } from "antd"

import { AdminLayout } from "./AdminLayout.js"
import { AdminAppContext } from "./AdminAppContext.js"
import { Timeago } from "./Forms.js"
import { ColumnsType } from "antd/es/table/InternalTable.js"
import { ApiChartViewOverview } from "../adminShared/AdminTypes.js"
import { GRAPHER_DYNAMIC_THUMBNAIL_URL } from "../settings/clientSettings.js"
import { Link } from "./Link.js"
Expand All @@ -20,7 +19,7 @@ function createColumns(ctx: {
text: string | null | undefined
) => React.ReactElement | string
deleteFn: (chartViewId: number) => void
}): ColumnsType<ApiChartViewOverview> {
}): TableColumnsType<ApiChartViewOverview> {
return [
{
title: "Preview",
Expand Down
4 changes: 2 additions & 2 deletions adminSiteClient/ImagesIndexPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import {
Popconfirm,
Popover,
Table,
TableColumnsType,
Upload,
notification,
} from "antd"
import { AdminLayout } from "./AdminLayout.js"
import { AdminAppContext } from "./AdminAppContext.js"
import { DbEnrichedImageWithUserId, DbPlainUser } from "@ourworldindata/types"
import { Timeago } from "./Forms.js"
import { ColumnsType } from "antd/es/table/InternalTable.js"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import {
faClose,
Expand Down Expand Up @@ -246,7 +246,7 @@ function createColumns({
users: UserMap
usage: Dictionary<UsageInfo[]>
notificationApi: NotificationInstance
}): ColumnsType<DbEnrichedImageWithUserId> {
}): TableColumnsType<DbEnrichedImageWithUserId> {
return [
{
title: "Preview",
Expand Down
255 changes: 255 additions & 0 deletions adminSiteClient/MultiDimIndexPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import {
useMutation,
UseMutationResult,
useQuery,
useQueryClient,
} from "@tanstack/react-query"
import { createContext, useContext, useEffect, useMemo, useState } from "react"
import {
Flex,
Input,
Popconfirm,
Switch,
Table,
TableColumnsType,
notification,
} from "antd"
import { Admin } from "./Admin.js"
import { AdminLayout } from "./AdminLayout.js"
import { AdminAppContext } from "./AdminAppContext.js"
import urljoin from "url-join"
import {
BAKED_BASE_URL,
BAKED_GRAPHER_URL,
GRAPHER_DYNAMIC_THUMBNAIL_URL,
} from "../settings/clientSettings.js"
import { dayjs, Json } from "@ourworldindata/utils"
import { Link } from "./Link.js"

type ApiMultiDim = {
id: number
title: string
slug: string
updatedAt: string
published: boolean
}

type MultiDim = Omit<ApiMultiDim, "updatedAt"> & {
updatedAt: Date
}

function createColumns(
publishMutation: UseMutationResult<
unknown,
unknown,
{ id: number; published: boolean },
unknown
>
): TableColumnsType<MultiDim> {
return [
{
title: "Preview",
dataIndex: "thumbnailUrl",
width: 100,
key: "thumbnail",
render: (_, record) => {
return (
<div style={{ width: 100, height: 100 }}>
<a
href={urljoin(
BAKED_BASE_URL,
`/admin/grapher/${record.slug}`
)}
target="_blank"
rel="noopener"
>
<img
src={urljoin(
GRAPHER_DYNAMIC_THUMBNAIL_URL,
`/${record.slug}.png?imType=square&imSquareSize=100`
)}
style={{ maxWidth: "100%", maxHeight: "100%" }}
alt="Preview"
/>
</a>
</div>
)
},
},
{
title: "Title",
dataIndex: "title",
key: "title",
render: (text, record) =>
record.published ? (
<a
href={urljoin(BAKED_GRAPHER_URL, record.slug)}
target="_blank"
rel="noopener"
>
{text}
</a>
) : (
text
),
sorter: (a, b) => a.title.localeCompare(b.title),
},
{
title: "Slug",
dataIndex: "slug",
key: "slug",
// render: (text, record) => {
// const handleSave = async () => {
// await api.patchMdim(record.id, { slug: value })
// setIsEditing(false)
// notificationApi.success({
// message: "Slug updated successfully",
// placement: "bottomRight",
// })
// }

// return isEditing ? (
// <Input
// value={value}
// onChange={(e) => setValue(e.target.value)}
// onPressEnter={handleSave}
// onBlur={handleSave}
// autoFocus
// />
// ) : (
// <div
// onClick={() => setIsEditing(true)}
// style={{ cursor: "pointer" }}
// >
// {value}
// <span
// className="ant-typography-secondary"
// style={{ marginLeft: 8 }}
// >
// (click to edit)
// </span>
// </div>
// )
// },
},
{
title: "Last updated",
dataIndex: "updatedAt",
key: "updatedAt",
defaultSortOrder: "descend",
render: (updatedAt) => {
const date = dayjs(updatedAt)
return <span title={date.format()}>{date.fromNow()}</span>
},
sorter: (a, b) => a.updatedAt.getTime() - b.updatedAt.getTime(),
},
{
title: "Published",
dataIndex: "published",
key: "published",
align: "center",
render: (published, record) => (
<Popconfirm
title={`Are you sure you want to ${published ? "unpublish" : "publish"} this page?`}
onConfirm={() =>
publishMutation.mutate({
id: record.id,
published: !published,
})
}
okText="Yes"
cancelText="No"
>
<Switch
checked={published}
disabled={publishMutation.isLoading}
/>
</Popconfirm>
),
},
]
}

const NotificationContext = createContext(null)

async function fetchMultiDims(admin: Admin) {
const result = await admin.getJSON<{ multiDims: MultiDim[] }>(
"/api/multi-dims.json"
)
return result.multiDims.map((mdim) => ({
...mdim,
updatedAt: new Date(mdim.updatedAt), // TODO: Is the date correct in staging/prod?
}))
}

async function patchMultiDim(admin: Admin, id: number, data: Json) {
return await admin.requestJSON(`/api/multi-dims/${id}`, data, "PATCH")
}

export function MultiDimIndexPage() {
const { admin } = useContext(AdminAppContext)
const [notificationApi, notificationContextHolder] =
notification.useNotification()
const [searchValue, setSearchValue] = useState("")
const queryClient = useQueryClient()

useEffect(() => {
admin.loadingIndicatorSetting = "off"
return () => {
admin.loadingIndicatorSetting = "default"
}
}, [admin])

const { data } = useQuery({
queryKey: ["multiDims"],
queryFn: () => fetchMultiDims(admin),
})

const mutation = useMutation({
mutationFn: ({ id, published }: { id: number; published: boolean }) =>
patchMultiDim(admin, id, { published }),
onSuccess: (_, { published }) => {
notificationApi.info({
message: published ? "Published" : "Unpublished",
description: <Link to="/deploys">Check deploy progress</Link>,
placement: "bottomRight",
})
return queryClient.invalidateQueries({ queryKey: ["multiDims"] })
},
})

const filteredMdims = useMemo(
() =>
data?.filter((mdim) =>
[mdim.title, mdim.slug].some((field) =>
field.toLowerCase().includes(searchValue.toLowerCase())
)
),
[data, searchValue]
)

const columns = createColumns(mutation)

return (
<AdminLayout title="Multidimensional Data Pages">
<NotificationContext.Provider value={null}>
{notificationContextHolder}
<main className="MultiDimIndexPage">
<Flex justify="space-between">
<Input
placeholder="Search by title or slug"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
style={{ width: 500, marginBottom: 20 }}
/>
</Flex>
<Table
columns={columns}
dataSource={filteredMdims}
rowKey={(x) => x.id}
/>
</main>
</NotificationContext.Provider>
</AdminLayout>
)
}
14 changes: 8 additions & 6 deletions adminSiteServer/apiRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ import {
deleteImageHandler,
getImageUsageHandler,
} from "./apiRoutes/images.js"
import { handleMultiDimDataPageRequest } from "./apiRoutes/mdims.js"
import {
handlePutMultiDim,
handleGetMultiDims,
handlePatchMultiDim,
} from "./apiRoutes/mdims.js"
import {
fetchAllWork,
fetchNamespaces,
Expand Down Expand Up @@ -258,11 +262,9 @@ deleteRouteWithRWTransaction(apiRouter, "/images/:id", deleteImageHandler)
getRouteWithROTransaction(apiRouter, "/images/usage", getImageUsageHandler)

// Mdim routes
putRouteWithRWTransaction(
apiRouter,
"/multi-dim/:slug",
handleMultiDimDataPageRequest
)
getRouteWithROTransaction(apiRouter, "/multi-dims.json", handleGetMultiDims)
putRouteWithRWTransaction(apiRouter, "/multi-dim/:slug", handlePutMultiDim)
patchRouteWithRWTransaction(apiRouter, "/multi-dims/:id", handlePatchMultiDim)

// Misc routes
getRouteWithROTransaction(apiRouter, "/all-work", fetchAllWork)
Expand Down
4 changes: 2 additions & 2 deletions adminSiteServer/apiRoutes/charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ const updateExistingChart = async (
shouldInherit?: boolean
}
): Promise<{
chartConfigId: Base64String
chartConfigId: string
patchConfig: GrapherInterface
fullConfig: GrapherInterface
}> => {
Expand Down Expand Up @@ -367,7 +367,7 @@ export const saveGrapher = async (

// Execute the actual database update or creation
let chartId: number
let chartConfigId: Base64String
let chartConfigId: string
let patchConfig: GrapherInterface
let fullConfig: GrapherInterface
if (existingConfig) {
Expand Down
Loading

0 comments on commit a6d1a53

Please sign in to comment.