Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🎉 share newly created DIs in Slack #4571

Open
wants to merge 1 commit into
base: create-di-topic-tags
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's anything we can do to catch this, but the request was 200'ing for me, but not posting to slack when I put zalgo text in it.

I don't think that'll be something we have to worry about from our authors though 😅

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 ?? ""