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

Add admin for multidimensional data pages #4505

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open

Add admin for multidimensional data pages #4505

wants to merge 4 commits into from

Conversation

rakyi
Copy link
Contributor

@rakyi rakyi commented Jan 30, 2025

Resolves #4458

ETL path and a new/changed admin API will be in a follow-up PR.

@owidbot
Copy link
Contributor

owidbot commented Jan 30, 2025

Quick links (staging server):

Site Dev Site Preview Admin Wizard Docs

Login: ssh owid@staging-site-mdim-admin

SVG tester:

Number of differences (default views): 2153 (ca33c1) ❌
Number of differences (all views): 1496 (b5db44) ❌

Edited: 2025-02-12 13:55:41 UTC
Execution time: 1.28 seconds

@rakyi rakyi force-pushed the mdim-admin branch 5 times, most recently from 3b776df to b74eab7 Compare February 6, 2025 16:24
slug={match.params.slug}
gitCmsBranchName={gitCmsBranchName}
manager={admin}
<QueryClientProvider client={queryClient}>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The changes below are mostly whitespace, except the new route and the new provider.

handleMultiDimDataPageRequest
)
getRouteWithROTransaction(apiRouter, "/multi-dims.json", handleGetMultiDims)
putRouteWithRWTransaction(apiRouter, "/multi-dim/:slug", handlePutMultiDim)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We will change the API in a follow-up PR (including making it plural).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Most of the code moved from adminSiteServer/apiRoutes/charts.ts.

row: DbRawChartConfig
): DbEnrichedChartConfig {
row: Pick<DbRawChartConfig, "patch" | "full">
): Pick<DbEnrichedChartConfig, "patch" | "full"> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

To make it work for cases when we don't pass the whole config around.

constructor(message: string, status?: number) {
super(message)
constructor(message: string, status?: number, options?: ErrorOptions) {
super(message, options)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

To make the cause work.

@rakyi rakyi requested a review from marcelgerber February 6, 2025 16:32
@rakyi rakyi marked this pull request as ready for review February 6, 2025 16:32

import { AdminLayout } from "./AdminLayout.js"
import { AdminAppContext } from "./AdminAppContext.js"
import { Timeago } from "./Forms.js"
import { ColumnsType } from "antd/es/table/InternalTable.js"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can use the public type instead of the internal one.

@rakyi rakyi force-pushed the mdim-admin branch 3 times, most recently from 9c3aa88 to a73c680 Compare February 7, 2025 09:25
Copy link
Member

@marcelgerber marcelgerber left a comment

Choose a reason for hiding this comment

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

Nicely done!

Comment on lines +833 to +836
!(await multiDimDataPageExists(knex, {
slug: link.target,
published: true,
}))
Copy link
Member

Choose a reason for hiding this comment

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

Why this change? And, if we change it, why not get rid of isMultiDimDataPagePublished then (which is now unused)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because I wanted to get rid of isMultiDimDataPagePublished after I needed the existence check based on other stuff than published and then I forgot to remove it. Removing!

Comment on lines 291 to 293
if (newConfig.slug) {
await validateGrapherSlug(knex, newConfig.slug, existingConfig?.id)
}
Copy link
Member

Choose a reason for hiding this comment

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

We need a slug to be present if the chart is published, and should make sure to fail if it's not present.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, of course! Good catch, fixing.

multiDim.config.views.map((view) => view.fullConfigId)
)

await Promise.all(
Copy link
Member

Choose a reason for hiding this comment

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

I'm unsure about the limitless concurrency this can run in, if the mdim has many views.
Maybe using p-map with {concurrency: 10} or something is better suited here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm happy to change it, and I'd be curious to hear if this was an issue in practice before.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not quite sure honestly!
I think undici (Node's fetch implementation) limits the number of open connections by itself, and I believe we have run into some kind of error related to that in the past?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With {concurrency: 10} it's a lot slower. Do you think 32 would be ok? Or even higher?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, go for it! It's all just vibes-based anyway 😁

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, in that case, I'd probably leave it as Promise.all() and fix it if we ever hit a problem. The S3 client doesn't seem to use undici.

Comment on lines 71 to 74
if (
FEATURE_FLAGS.has(FeatureFlagFeature.MultiDimDataPage) &&
(await isMultiDimDataPagePublished(trx, slug))
(await multiDimDataPageExists(trx, { slug, published: true }))
) {
Copy link
Member

Choose a reason for hiding this comment

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

This code path here should also call validateGrapherSlug

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, true. I didn't realize we are checking only for published graphers in the validation. That's even more complex.

multiDim = await setMultiDimPublished(trx, multiDim, published)
action = published ? "publish" : "unpublish"
}
if (slug !== undefined && slug !== multiDim.slug) {
Copy link
Member

Choose a reason for hiding this comment

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

A few things here:

  • I think we should first handle slug, then publish state.
  • In theory, we only need to call validateGrapherSlug if the mdim is published.
  • And we should probably also call it at the point when a mdim is published (i.e. publish state changes to true).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  • Why do you think we should handle the slug first? I don't see it.
  • I'd prefer if we kept the mdim slug valid even before publishing. I'd change the logic when we'll have a use case for it.
  • I added the additional validation — I think it's fine to validate the slug twice in when the mdim is published instead of making the logic more complex.

Copy link
Member

Choose a reason for hiding this comment

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

  • (1) otherwise we could, in theory, end up in a state where we mark the mdim as published (under the old slug), and then the slug rename fails, i.e. we have it published under the old slug.
    I realize that this cannot currently happen, because we either change the slug or the published state, not both at once - but just to be a tiny bit on the safer side, I still think switching these around makes sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, because of the R2! If it was just the DB it wouldn't matter, because the transaction gets rolled back. Changing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I switched the order and also moved the validation logic earlier, so it can also fail before we update stuff to R2.

render: (slug, { id }) => (
<SlugField id={id} slug={slug} slugMutation={slugMutation} />
),
sorter: (a, b) => a.title.localeCompare(b.title),
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
sorter: (a, b) => a.title.localeCompare(b.title),
sorter: (a, b) => a.slug.localeCompare(b.slug),

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch!


return (
<AdminLayout title="Multidimensional Data Pages">
<NotificationContext.Provider value={null}>
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 we need this extra context, since it's always null and not antd-specific anyhow?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, you are right. I based my code on the images admin, it was a total cargo culting. 🙂

Copy link
Member

Choose a reason for hiding this comment

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

The combination of react-query and antd makes this page feel really nice! 🎉

@rakyi rakyi requested a review from marcelgerber February 12, 2025 08:44
Copy link
Member

@marcelgerber marcelgerber left a comment

Choose a reason for hiding this comment

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

All good now! Thanks for addressing these.

@rakyi rakyi requested a review from marcelgerber February 12, 2025 10:11
@rakyi
Copy link
Contributor Author

rakyi commented Feb 12, 2025

Thanks for the review! Can you check again the last commit, pls?

Comment on lines +68 to +70
if (!(await multiDimDataPageExists(trx, { slug }))) {
await validateNewGrapherSlug(trx, slug)
}
Copy link
Member

Choose a reason for hiding this comment

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

hm, do we need that if here?
If a mdim already exists under this slug, and we're trying to create a new one, then this will fail either way: at the very least, it'll fail at the UNIQUE index on the DB table.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need the if, because this handler does an upsert. It won't fail on the unique constraint, it will update the existing row(s) if the mdim already exists.

Copy link
Member

Choose a reason for hiding this comment

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

I see, but then we need to actually fail if an mdim exists. Which we currently don't do!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I renamed the function we call to clear up the confusion about create vs upsert.

action = "publish"
}
}
// Note: Keep this change last, since we don't want to update the configs
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 know precisely when a DB rollback will happen, honestly.
Could it happen that we change the slug (and propagate that to R2), and then roll that back in the DB afterwards?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It can still happen, it can roll back any time we throw an error before the helper knex function somewhere in our Express handler returns. But the later we do the R2 upload, the better.

We have this problem in many places, and it's fine. The proper way to do this would be with https://en.wikipedia.org/wiki/Change_data_capture, if we really needed the correctness, i.e. first commit the changes to the DB and then propagate the changes to external systems.

Copy link
Contributor Author

rakyi commented Feb 12, 2025

This stack of pull requests is managed by Graphite. Learn more about stacking.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

An admin to publish mdims and set the publication slug
3 participants