Skip to content

Commit

Permalink
🎉 (admin) share newly created DIs in Slack
Browse files Browse the repository at this point in the history
  • Loading branch information
sophiamersmann committed Feb 20, 2025
1 parent 7d78a3e commit 3d4609d
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 6 deletions.
3 changes: 3 additions & 0 deletions .env.example-full
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,6 @@ ALGOLIA_SECRET_KEY= # optional
ALGOLIA_INDEXING=false # optional

DATA_API_URL= # optional

SLACK_BOT_OAUTH_TOKEN= # optional
SLACK_DI_PITCHES_CHANNEL_ID= # optional; #data-insight-pitches channel id
124 changes: 120 additions & 4 deletions adminSiteClient/CreateDataInsightModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ import cx from "classnames"
import {
fetchFigmaProvidedImageUrl,
ImageUploadResponse,
makeImageSrc,
uploadImageFromSourceUrl,
} from "./imagesHelpers"
import { AdminAppContext } from "./AdminAppContext"
import { GRAPHER_DYNAMIC_THUMBNAIL_URL } from "../settings/clientSettings"
import {
GRAPHER_DYNAMIC_THUMBNAIL_URL,
SLACK_DI_PITCHES_CHANNEL_ID,
} from "../settings/clientSettings"
import { LoadingImage } from "./ReuploadImageForDataInsightModal"
import { ApiChartViewOverview } from "../adminShared/AdminTypes"
import {
Expand All @@ -44,13 +48,15 @@ import {
RequiredBy,
} from "@ourworldindata/utils"
import { match } from "ts-pattern"
import { Checkbox } from "antd/lib"

const DEFAULT_RUNNING_MESSAGE: Record<Task, string> = {
createDI: "Creating data insight...",
uploadImage: "Uploading image...",
loadFigmaImage: "Loading Figma image...",
suggestAltText: "Suggesting alt text...",
setTopicTags: "Setting topic tags...",
sendSlackMessage: "Sending Slack message...",
} as const

const DEFAULT_SUCCESS_MESSAGE: Record<Task, string> = {
Expand All @@ -59,6 +65,7 @@ const DEFAULT_SUCCESS_MESSAGE: Record<Task, string> = {
loadFigmaImage: "Figma image loaded successfully",
suggestAltText: "Alt text suggested successfully",
setTopicTags: "Topic tags assigned",
sendSlackMessage: "Slack message sent",
} as const

const DEFAULT_ERROR_MESSAGE: Record<Task, string> = {
Expand All @@ -67,6 +74,7 @@ const DEFAULT_ERROR_MESSAGE: Record<Task, string> = {
loadFigmaImage: "Loading Figma image failed",
suggestAltText: "Suggesting alt text failed",
setTopicTags: "Setting topic tags failed",
sendSlackMessage: "Sending Slack message failed",
} as const

type Task =
Expand All @@ -75,6 +83,7 @@ type Task =
| "loadFigmaImage"
| "suggestAltText"
| "setTopicTags"
| "sendSlackMessage"

type Progress =
| { status: "idle" }
Expand All @@ -90,6 +99,7 @@ type FormFieldName =
| "figmaUrl"
| "imageFilename"
| "imageAltText"
| "slackNote"
type ImageFormFieldName = "imageFilename" | "imageAltText"

type FormData = Partial<
Expand Down Expand Up @@ -152,9 +162,13 @@ export function CreateDataInsightModal(props: {
"uploadImage",
"loadFigmaImage",
"suggestAltText",
"setTopicTags"
"setTopicTags",
"sendSlackMessage"
)

const [shouldSendMessageToSlack, setShouldSendMessageToSlack] =
useState(true)

// loaded from Figma if a Figma URL is provided
const [figmaImageUrl, setFigmaImageUrl] = useState<string | undefined>()

Expand Down Expand Up @@ -263,6 +277,7 @@ export function CreateDataInsightModal(props: {

setProgress("createDI", "running")

let cloudflareImageId: string | undefined
if (imageUrl && isValidForImageUpload(formData)) {
setProgress("uploadImage", "running")

Expand All @@ -276,6 +291,8 @@ export function CreateDataInsightModal(props: {
"Not attempted since image upload failed"
)
return
} else {
cloudflareImageId = response.image.cloudflareId ?? undefined
}
} catch (error) {
const errorMessage =
Expand Down Expand Up @@ -309,6 +326,22 @@ export function CreateDataInsightModal(props: {
}
}

// Send a message to Slack if requested
if (shouldSendMessageToSlack && cloudflareImageId) {
setProgress("sendSlackMessage", "running")
try {
await sendDataInsightToSlack({
formData,
imageUrl: makeImageSrc(cloudflareImageId, 1250),
})
setProgress("sendSlackMessage", "success")
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error)
setProgress("sendSlackMessage", "failure", errorMessage)
}
}

props.onFinish?.(createResponse)
}

Expand Down Expand Up @@ -373,6 +406,40 @@ export function CreateDataInsightModal(props: {
)
}

const sendDataInsightToSlack = async ({
formData,
imageUrl,
}: {
formData: FormDataWithTitle
imageUrl: string
}) => {
const { title, slackNote, authors } = formData

let text = `*${title}*`
if (slackNote) text += `\n\n${slackNote}`
if (authors) text += `\n\nby ${authors}`

const blocks = [
{
type: "section",
text: { type: "mrkdwn", text },
},
{
type: "image",
image_url: imageUrl,
alt_text: formData.imageAltText,
},
]

const payload = {
blocks,
channel: SLACK_DI_PITCHES_CHANNEL_ID,
username: "Data insight bot",
}

void admin.requestJSON(`/api/slack/sendMessage`, payload, "POST")
}

const fetchFigmaImage = async (figmaUrl: string) => {
setProgress("loadFigmaImage", "running")
try {
Expand Down Expand Up @@ -592,7 +659,6 @@ export function CreateDataInsightModal(props: {
},
]}
/>

<Space
size="small"
direction="vertical"
Expand Down Expand Up @@ -635,7 +701,6 @@ export function CreateDataInsightModal(props: {
Suggest
</Button>
</Space>

<div className="image-preview">
<h3>Image preview</h3>

Expand Down Expand Up @@ -665,6 +730,31 @@ export function CreateDataInsightModal(props: {
</>
)}

{imageUrl && (
<p>
<Checkbox
checked={shouldSendMessageToSlack}
onChange={(e) => {
setShouldSendMessageToSlack(
e.target.checked
)
}}
>
Share data insight in the #data-insight-pitches
channel
</Checkbox>
{shouldSendMessageToSlack && (
<FormField
name="slackNote"
className="slackNote"
aria-label="Note (shared on Slack)"
>
<Input.TextArea placeholder="Note (shared on Slack)" />
</FormField>
)}
</p>
)}

{showFeedbackBox({ formData, progress, imageUrl }) && (
<div className="feedback-box">
<h2>This data insight will be created by:</h2>
Expand All @@ -685,6 +775,13 @@ export function CreateDataInsightModal(props: {
progress={progress.setTopicTags}
/>
</ul>

<SendMessageToSlackFeedback
shouldSend={
!!imageUrl && shouldSendMessageToSlack
}
progress={progress.sendSlackMessage}
/>
</div>
)}

Expand Down Expand Up @@ -924,6 +1021,25 @@ function TopicTagsFeedback({
)
}

function SendMessageToSlackFeedback({
shouldSend,
progress,
}: {
shouldSend: boolean
progress: Progress
}) {
if (!shouldSend) return null
return (
<p>
<span>
The newly created data insight will be shared in the
#data-insight-pitches channel.
</span>
<FeedbackTag progress={progress} />
</p>
)
}

function FeedbackTag({ progress }: { progress: Progress }) {
if (progress.status === "idle") return null

Expand Down
11 changes: 9 additions & 2 deletions adminSiteClient/admin.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1369,8 +1369,12 @@ main:not(.ChartEditorPage):not(.GdocsEditPage) {
}
}

.slackNote {
margin-top: 12px;
}

.image-preview {
margin-top: 24px;
margin: 24px 0;

h3 {
font-size: 1em;
Expand All @@ -1391,7 +1395,7 @@ main:not(.ChartEditorPage):not(.GdocsEditPage) {
}

p {
margin: 0;
margin: 12px 0 0 0;
}

ul {
Expand All @@ -1401,7 +1405,10 @@ main:not(.ChartEditorPage):not(.GdocsEditPage) {

li {
line-height: 1.5;
}

li,
p {
// add a little margin before the feedback tag
span:first-of-type {
margin-right: 12px;
Expand Down
4 changes: 4 additions & 0 deletions adminSiteServer/apiRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ import {
getAllDataInsightIndexItems,
} from "./apiRoutes/dataInsights.js"
import { getFigmaImageUrl } from "./apiRoutes/figma.js"
import { sendMessageToSlack } from "./apiRoutes/slack.js"

const apiRouter = new FunctionalRouter()

Expand Down Expand Up @@ -464,6 +465,9 @@ getRouteWithROTransaction(
// Figma routes
getRouteWithROTransaction(apiRouter, "/figma/image", getFigmaImageUrl)

// Slack routes
postRouteWithRWTransaction(apiRouter, "/slack/sendMessage", sendMessageToSlack)

// Deploy helpers
apiRouter.get("/deploys.json", async () => ({
deploys: await new DeployQueueServer().getDeploys(),
Expand Down
39 changes: 39 additions & 0 deletions adminSiteServer/apiRoutes/slack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import e from "express"
import * as db from "../../db/db.js"
import { Request } from "../authentication.js"
import { SLACK_BOT_OAUTH_TOKEN } from "../../settings/serverSettings"
import { JsonError } from "@ourworldindata/types"

export async function sendMessageToSlack(
req: Request,
_res: e.Response<any, Record<string, any>>,
_trx: db.KnexReadWriteTransaction
) {
const url = "https://slack.com/api/chat.postMessage"

const { channel, blocks, username } = req.body

if (!channel) throw new JsonError("Channel missing")
if (!blocks) throw new JsonError("Blocks missing")

const data = { channel, blocks, username }

const fetchData = {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${SLACK_BOT_OAUTH_TOKEN}`,
},
}

const response = await fetch(url, fetchData)

if (!response.ok) {
throw new JsonError(
`Slack API error: ${response.status} ${response.statusText}`
)
}

return { success: true }
}
3 changes: 3 additions & 0 deletions settings/clientSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,6 @@ export const FEATURE_FLAGS: Set<FeatureFlagFeature> = new Set(
featureFlagsRaw.includes(key)
) as FeatureFlagFeature[]
)

export const SLACK_DI_PITCHES_CHANNEL_ID: string =
process.env.SLACK_DI_PITCHES_CHANNEL_ID ?? ""

0 comments on commit 3d4609d

Please sign in to comment.