Skip to content

Commit

Permalink
Merge pull request #4252 from owid/cloudflare-images-reupload
Browse files Browse the repository at this point in the history
🎉 Cloudflare images image updating
  • Loading branch information
ikesau authored Dec 5, 2024
2 parents 8c8f040 + d6fb57b commit 14e165a
Show file tree
Hide file tree
Showing 5 changed files with 319 additions and 127 deletions.
197 changes: 144 additions & 53 deletions adminSiteClient/ImagesIndexPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,17 @@ import React, {
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react"
import {
Button,
Flex,
Input,
Mentions,
Popconfirm,
Space,
Table,
Upload,
} from "antd"
import { Button, Flex, Input, Mentions, Popconfirm, Table, Upload } 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, faUpload } from "@fortawesome/free-solid-svg-icons"
import { Admin } from "./Admin.js"
import { RcFile } from "antd/es/upload/interface.js"
import TextArea from "antd/es/input/TextArea.js"
import { CLOUDFLARE_IMAGES_URL } from "../settings/clientSettings.js"
Expand All @@ -37,6 +28,19 @@ type ImageEditorApi = {
image: DbEnrichedImageWithUserId,
patch: Partial<DbEnrichedImageWithUserId>
) => void
putImage: (
id: number,
payload: {
filename: string
content?: string
type: string
}
) => void
postImage: (payload: {
filename: string
content?: string
type: string
}) => void
deleteImage: (image: DbEnrichedImageWithUserId) => void
getImages: () => void
getUsers: () => void
Expand Down Expand Up @@ -114,7 +118,11 @@ function UserSelect({
}

const handleSelect = async (option: { value?: string; label?: string }) => {
const selectedUser = usersMap[option.value!]
// iterating because we only have the label when using the admin context
const selectedUser = Object.values(usersMap).find(
(user) => user.fullName === option.label
)

if (selectedUser) {
setValue(selectedUser.fullName)
await onUserSelect(selectedUser)
Expand All @@ -141,7 +149,7 @@ function UserSelect({
<div>
<Button
type="text"
onClick={() => handleSelect({ value: admin.username })}
onClick={() => handleSelect({ label: admin.username })}
>
+ {admin.username}
</Button>
Expand All @@ -152,6 +160,33 @@ function UserSelect({
)
}

// when updatedAt changes, the image will reload the src
// but it looks like sometimes cloudflare doesn't update in time :(
function ImgWithRefresh({
src,
updatedAt,
}: {
src: string
updatedAt: number | null
}) {
const ref = useRef<HTMLImageElement>(null)
useEffect(() => {
if (ref.current && updatedAt) {
ref.current.src = ""
fetch(src, { cache: "reload" })
.then(() => {
if (ref.current) {
ref.current.src = src
}
})
.catch((e) => {
console.log("Something went wrong refreshing the image", e)
})
}
})
return <img ref={ref} src={src} style={{ maxHeight: 100, maxWidth: 100 }} />
}

function createColumns({
api,
users,
Expand All @@ -165,7 +200,7 @@ function createColumns({
dataIndex: "cloudflareId",
width: 100,
key: "cloudflareId",
render: (cloudflareId, { originalWidth }) => {
render: (cloudflareId, { originalWidth, updatedAt }) => {
const srcFor = (w: number) =>
`${CLOUDFLARE_IMAGES_URL}/${encodeURIComponent(
cloudflareId
Expand All @@ -177,9 +212,9 @@ function createColumns({
href={`${srcFor(originalWidth!)}`}
rel="noopener"
>
<img
<ImgWithRefresh
src={`${srcFor(200)}`}
style={{ maxHeight: 100, maxWidth: 100 }}
updatedAt={updatedAt}
/>
</a>
</div>
Expand Down Expand Up @@ -285,7 +320,8 @@ function createColumns({
key: "action",
width: 100,
render: (_, image) => (
<Space size="middle">
<Flex vertical>
<PutImageButton putImage={api.putImage} id={image.id} />
<Popconfirm
title="Are you sure?"
description="This will delete the image being used in production."
Expand All @@ -297,48 +333,77 @@ function createColumns({
Delete
</Button>
</Popconfirm>
</Space>
</Flex>
),
},
]
}

function ImageUploadButton({
setImages,
admin,
}: {
setImages: React.Dispatch<React.SetStateAction<ImageMap>>
admin: Admin
}) {
function uploadImage({ file }: { file: string | Blob | RcFile }) {
if (typeof file === "string") return
type File = string | Blob | RcFile

const reader = new FileReader()
reader.onload = async () => {
const base64Data = reader.result?.toString()
type FileToBase64Result = {
filename: string
content: string
type: string
}

const payload = {
/**
* Uploading as base64, because otherwise we'd need multipart/form-data parsing middleware in the server.
* This seems easier as a one-off.
**/
function fileToBase64(file: File): Promise<FileToBase64Result | null> {
if (typeof file === "string") return Promise.resolve(null)

return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = () => {
resolve({
filename: file.name,
content: base64Data,
content: reader.result?.toString() ?? "",
type: file.type,
}
})
}
reader.readAsDataURL(file)
})
}

const { image } = await admin.requestJSON<{
sucess: true
image: DbEnrichedImageWithUserId
}>("/api/image", payload, "POST")
function PostImageButton({
postImage,
}: {
postImage: ImageEditorApi["postImage"]
}) {
async function uploadImage({ file }: { file: File }) {
const result = await fileToBase64(file)
if (result) {
postImage(result)
}
}
return (
<Upload showUploadList={false} customRequest={uploadImage}>
<Button type="primary">
<FontAwesomeIcon icon={faUpload} /> Upload
</Button>
</Upload>
)
}

setImages((imagesMap) => ({
...imagesMap,
[image.id]: image,
}))
function PutImageButton({
putImage,
id,
}: {
putImage: ImageEditorApi["putImage"]
id: number
}) {
async function uploadImage({ file }: { file: File }) {
const result = await fileToBase64(file)
if (result) {
putImage(id, result)
}
reader.readAsDataURL(file)
}
return (
<Upload showUploadList={false} customRequest={uploadImage}>
<Button type="primary" icon={<FontAwesomeIcon icon={faUpload} />}>
Upload image
<Button className="ImageIndexPage__update-image-button" type="text">
Upload new version
</Button>
</Upload>
)
Expand Down Expand Up @@ -377,18 +442,44 @@ export function ImageIndexPage() {
success: true
image: DbEnrichedImageWithUserId
}>(`/api/images/${image.id}`, patch, "PATCH")
setImages((prevMap) => ({
...prevMap,
[image.id]: response.image,
}))
if (response.success) {
setImages((prevMap) => ({
...prevMap,
[image.id]: response.image,
}))
}
},
postImage: async (image) => {
const response = await admin.requestJSON<{
success: true
image: DbEnrichedImageWithUserId
}>(`/api/images`, image, "POST")
if (response.success) {
setImages((prevMap) => ({
...prevMap,
[response.image.id]: response.image,
}))
}
},
putImage: async (id, payload) => {
const response = await admin.requestJSON<{
success: true
image: DbEnrichedImageWithUserId
}>(`/api/images/${id}`, payload, "PUT")
if (response.success) {
setImages((prevMap) => ({
...prevMap,
[id]: response.image,
}))
}
},
postUserImage: async (user, image) => {
const result = await admin.requestJSON(
`/api/users/${user.id}/image/${image.id}`,
const response = await admin.requestJSON(
`/api/users/${user.id}/images/${image.id}`,
{},
"POST"
)
if (result.success) {
if (response.success) {
setImages((prevMap) => ({
...prevMap,
[image.id]: { ...prevMap[image.id], userId: user.id },
Expand All @@ -397,7 +488,7 @@ export function ImageIndexPage() {
},
deleteUserImage: async (user, image) => {
const result = await admin.requestJSON(
`/api/users/${user.id}/image/${image.id}`,
`/api/users/${user.id}/images/${image.id}`,
{},
"DELETE"
)
Expand Down Expand Up @@ -439,7 +530,7 @@ export function ImageIndexPage() {
onChange={(e) => setFilenameSearchValue(e.target.value)}
style={{ width: 500, marginBottom: 20 }}
/>
<ImageUploadButton setImages={setImages} admin={admin} />
<PostImageButton postImage={api.postImage} />
</Flex>
<Table columns={columns} dataSource={filteredImages} />
</main>
Expand Down
5 changes: 5 additions & 0 deletions adminSiteClient/admin.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1219,4 +1219,9 @@ main:not(.ChartEditorPage):not(.GdocsEditPage) {
width: 16px;
vertical-align: -2px;
}

.ImageIndexPage__update-image-button {
color: #007bff;
margin-bottom: 8px;
}
}
Loading

0 comments on commit 14e165a

Please sign in to comment.