diff --git a/adminSiteClient/AdminApp.tsx b/adminSiteClient/AdminApp.tsx index 32d5ec94223..56804458a34 100644 --- a/adminSiteClient/AdminApp.tsx +++ b/adminSiteClient/AdminApp.tsx @@ -23,8 +23,6 @@ import { NotFoundPage } from "./NotFoundPage.js" import { PostEditorPage } from "./PostEditorPage.js" import { DeployStatusPage } from "./DeployStatusPage.js" import { ExplorerTagsPage } from "./ExplorerTagsPage.js" -import { SuggestedChartRevisionApproverPage } from "./SuggestedChartRevisionApproverPage.js" -import { SuggestedChartRevisionListPage } from "./SuggestedChartRevisionListPage.js" import { BulkDownloadPage } from "./BulkDownloadPage.js" import { BrowserRouter as Router, @@ -320,28 +318,6 @@ export class AdminApp extends React.Component<{ path="/explorer-tags" component={ExplorerTagsPage} /> - - - ( - - )} - /> ( Bulk downloads -
  • - - Suggested chart - revisions - -
  • SETTINGS
  • diff --git a/adminSiteClient/SuggestedChartRevision.ts b/adminSiteClient/SuggestedChartRevision.ts deleted file mode 100644 index 5ba4e4ca7c5..00000000000 --- a/adminSiteClient/SuggestedChartRevision.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { GrapherInterface } from "@ourworldindata/types" -import { SuggestedChartRevisionStatus } from "@ourworldindata/utils" - -export interface SuggestedChartRevisionSerialized { - id: number - chartId: number - - createdAt: string - updatedAt?: string - - chartCreatedAt: string - chartUpdatedAt?: string - - createdById: number - updatedById?: number - - createdByFullName: string - updatedByFullName?: string - - originalConfig: GrapherInterface - suggestedConfig: GrapherInterface - existingConfig: GrapherInterface - - status: SuggestedChartRevisionStatus - suggestedReason?: string - decisionReason?: string - changesInDataSummary?: string - - canApprove?: boolean - canReject?: boolean - canFlag?: boolean - canPending?: boolean - - experimental?: SuggestedChartRevisionExperimentalSerialized -} - -export interface SuggestedChartRevisionExperimentalSerialized { - gpt?: { - model?: string - suggestions?: GPTSuggestionsSerialized[] - } - [key: string]: any -} - -export interface GPTSuggestionsSerialized { - title?: string - subtitle?: string -} diff --git a/adminSiteClient/SuggestedChartRevisionApproverPage.tsx b/adminSiteClient/SuggestedChartRevisionApproverPage.tsx deleted file mode 100644 index a7087a720fe..00000000000 --- a/adminSiteClient/SuggestedChartRevisionApproverPage.tsx +++ /dev/null @@ -1,1645 +0,0 @@ -import React from "react" -import { observer } from "mobx-react" -import { observable, computed, action, runInAction } from "mobx" -import { Link } from "react-router-dom" -import { Base64 } from "js-base64" -import Select from "react-select" -import { - Bounds, - getStylesForTargetHeight, - SortOrder, - SuggestedChartRevisionStatus, - Tippy, - uniqBy, -} from "@ourworldindata/utils" -import { Grapher } from "@ourworldindata/grapher" -import { - TextAreaField, - NumberField, - RadioGroup, - Toggle, - Timeago, -} from "./Forms.js" -import { References } from "./ChartEditor.js" -import { AdminLayout } from "./AdminLayout.js" -import { SuggestedChartRevisionStatusIcon } from "./SuggestedChartRevisionList.js" -import { AdminAppContext, AdminAppContextType } from "./AdminAppContext.js" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js" -import { - faMobile, - faDesktop, - faExternalLinkAlt, - faAngleLeft, - faAngleRight, - faAngleDoubleLeft, - faAngleDoubleRight, - faSortAlphaDown, - faSortAlphaUpAlt, - faRandom, - faMagicWandSparkles, -} from "@fortawesome/free-solid-svg-icons" -import { - VisionDeficiency, - VisionDeficiencySvgFilters, - VisionDeficiencyDropdown, - VisionDeficiencyEntity, -} from "./VisionDeficiencies.js" -import { SuggestedChartRevisionSerialized } from "./SuggestedChartRevision.js" -import { match } from "ts-pattern" -import { ReferencesSection } from "./EditorReferencesTab.js" - -interface UserSelectOption { - userName: string - userId: number | undefined -} - -@observer -export class SuggestedChartRevisionApproverPage extends React.Component<{ - suggestedChartRevisionId?: number -}> { - @observable.ref suggestedChartRevisions?: SuggestedChartRevisionSerialized[] - @observable currentlyActiveUserId?: number - @observable.ref originalGrapherElement?: React.ReactElement - @observable.ref suggestedGrapherElement?: React.ReactElement - @observable.ref existingGrapherElement?: React.ReactElement - @observable.ref chartReferences: References | undefined = undefined - - // HACK: In order for the - {userOptions.map((user) => ( - - ))} - - - ) - } - - renderGraphers() { - // Render both charts next to each other - console.log("renderGraphers") - const gpt_model_name = this.getGPTModelNameUsed() - return ( - -
    - {/* Original chart */} -
    - {this.currentSuggestedChartRevision && ( - -
    - -

    - Original -

    -
    - - {`(#${this.currentSuggestedChartRevision.chartId}, v${this.currentSuggestedChartRevision.originalConfig.version})`} - - - Edit{" "} - - -
    -
    - )} - {this._isGraphersSet && - this.currentSuggestedChartRevision && - this.renderGrapher( - this.currentSuggestedChartRevision - .originalConfig - )} -
    - {this.showExistingChart && ( -
    - {this.currentSuggestedChartRevision && ( - -
    - -

    - Existing -

    -
    - - {`(#${this.currentSuggestedChartRevision.chartId}, V${this.currentSuggestedChartRevision.existingConfig.version})`} - - - Edit{" "} - - -
    - {/*

    - This is what the chart looks like right now on the OWID website. -

    */} -
    - )} - {this._isGraphersSet && - this.currentSuggestedChartRevision && - this.renderGrapher( - this.currentSuggestedChartRevision - .existingConfig - )} -
    - )} - {/* Suggested chart */} -
    - {this.currentSuggestedChartRevision && ( - -
    - {/* Title and link to edit */} -
    - -

    - Suggested -

    -
    - - {/* {`(#${this.currentSuggestedChartRevision.chartId}, V${this.currentSuggestedChartRevision.suggestedConfig.version})`} */} - - - Edit as chart{" "} - { - this - .currentSuggestedChartRevision - .chartId - }{" "} - - -
    - {/* GPT section */} -
    - {/* */} - - {/* */} - -
    -
    -
    - )} - {this._isGraphersSet && - this.currentSuggestedChartRevision && - this.renderGrapher( - this.currentSuggestedChartRevision - .suggestedConfig - )} -
    -
    -
    - ) - } - - renderGrapher(grapherConfig: any) { - console.log("renderGrapher") - return ( -
    - {this.previewSvgOrJson === "json" ? ( -
    -
    -                            
    -                                {JSON.stringify(grapherConfig, null, 2)}
    -                            
    -                        
    -
    - ) : ( -
    - -
    - )} -
    - ) - } - - renderControls() { - // Render controls on how to navigate the approval - return ( -
    - {this.renderControlsNotes()} - {this.renderControlsButtons()} - {this.renderControlsNumberOfRevisions()} -
    - ) - } - - renderControlsNotes() { - // Render textarea in the controls block - return ( - - ) - } - - renderControlsButtons() { - // Render buttons in controls section - return ( -
    - {this.listMode && ( - - - - - )} - - - - {this.listMode && ( - - - - - - )} -
    - ) - } - - renderControlsNumberOfRevisions() { - // Render number of revisions block - return ( - this.listMode && ( -
    - Suggested revision - - - of {this.numAvailableRowsForSelectedUser} - {this.showPendingOnly ? " remaining" : ""} ( - View all) - -
    - ) - ) - } - - renderMeta() { - // Renders metadata block - return ( - -
    -

    Metadata

    -
      -
    • - Suggested revision ID:{" "} - {this.currentSuggestedChartRevision - ? this.currentSuggestedChartRevision.id - : ""} -
    • -
    • - Chart ID:{" "} - {this.currentSuggestedChartRevision - ? this.currentSuggestedChartRevision.chartId - : ""} -
    • -
    • - Suggested revision created:{" "} - {this.currentSuggestedChartRevision && ( - - )} -
    • - -
    • - Suggested revision last updated:{" "} - {this.currentSuggestedChartRevision?.updatedAt && ( - - )} -
    • -
    • - Reason for suggested revision:{" "} - {this.currentSuggestedChartRevision && - this.currentSuggestedChartRevision.suggestedReason - ? this.currentSuggestedChartRevision - .suggestedReason - : "None provided."} -
    • -
    -
    -
    -

    References to original chart

    - -
    -
    -

    Indicator changes

    {" "} - {this.currentSuggestedChartRevision && - this.currentSuggestedChartRevision.changesInDataSummary ? ( -
    - ) : ( - "No summary provided." - )} -
    -
    - ) - } - - renderReadme() { - // Render the readme (instructions on how to use the approval tool) - return ( -
    -

    Terminology

    -
      -
    • - Suggested (chart revision). A suggested chart - revision is simply an amended OWID chart, but where the - amendments have not yet been applied to the chart in - question. A suggested chart revision is housed in the{" "} - suggested_chart_revisions table in{" "} - MySQL. If the suggested chart revision gets - approved, then the amendments are applied to the chart - (which overwrites and republishes the chart). -
    • -
    • - Original (Original chart). The chart as it - originally was when the suggested chart revision was - created. -
    • -
    • - Existing (Existing chart). The chart as it - currently exists on the OWID website. -
    • -
    -

    How to use

    -

    - You are shown one suggested chart revision at a time, - alongside the corresponding original chart as it was when - the suggested chart revision was created. -

    -

    - For each suggested revision, choose one of the following - actions: -

    -
      -
    1. - Approve the revision by clicking{" "} - - . This approves the suggestion, replacing the original - chart with the suggested chart (also republishes the - chart). Note: if a chart has been edited since the - suggested revision was created, you will not be allowed - to approve the suggested revision. -
    2. -
    3. - Reject the suggested revision by clicking{" "} - - . This rejects the suggestion, keeping the original - chart as it is. -
    4. -
    5. - Flag the suggested revision for further - inspection by clicking{" "} - - . -
    6. -
    7. - Edit the original chart by clicking{" "} - - Edit - - . This opens the original chart in the chart editor. If - you save your changes to the original chart within the - chart editor, you will no longer have the option to - approve the suggested revision. -
    8. -
    9. - - Edit the suggested chart revision as the original - chart - {" "} - by clicking{" "} - - Edit as chart [chartId]{" "} - - - . This opens the suggested chart revision in the chart - editor. If you make and save changes to the chart within - the chart editor,{" "} - - your config and data changes will be applied to the - original chart, equivalent to approving it. - {" "} - Currently, the suggestion is not updated and the - approval is left as pending, but you will no longer be - able to approve it (since the chart has changed). - Because it has actually been applied, you can now reject - it. -
    10. -
    -

    Other useful information

    -
      -
    • - When you click the{" "} - {" "} - ,{" "} - {" "} - or{" "} - {" "} - button, anything you write in the "Notes" text field - will be saved. You can view these saved notes in the - "Decision reason" column{" "} - here. If - you reject or flag a suggested chart revision, it is{" "} - strongly recommended that you describe your - reasoning in the "Notes" field. -
    • -
    • - If a suggested revision has been approved and the chart - has not changed since the revision was approved, then - you can undo the revision by clicking the{" "} - {" "} - button. -
    • -
    • - If a suggested revision has been rejected and the chart - has not changed since the revision was rejected, then - you can still approve the revision by clicking the{" "} - {" "} - button. -
    • -
    • - If one or more of the{" "} - {" "} - ,{" "} - {" "} - or{" "} - {" "} - buttons are disabled, this is because these actions are - not allowed for the suggested revision in question. For - example, if a chart has changed since the suggested - revision was created, you will not be allowed to approve - the revision. -
    • -
    -
    - ) - } - - renderSettings() { - console.log("renderSettings") - // Render settings - return ( -
    - {/* {this.listMode && ( -
    - -
    - )} */} -
    - -
    -
    -
    - Preview mode: -
    -
    - - -
    -
    -
    - Preview size (desktop only): - -
    -
    - {this.listMode && ( -
    -
    - Sort by:{" "} - - this.onSortOrderChange( - SortOrder.asc - ) - } - name="sortOrder" - id="asc" - checked={ - this.sortOrder === SortOrder.asc - } - />{" "} - - - -
    -
    -
    - )} -
    - View SVG or JSON? - -
    -
    - Emulate vision deficiency:{" "} - - (this.simulateVisionDeficiency = - option.deficiency) - )} - /> - -
    - - ) - } -} diff --git a/adminSiteClient/SuggestedChartRevisionList.tsx b/adminSiteClient/SuggestedChartRevisionList.tsx deleted file mode 100644 index a7ddb83e963..00000000000 --- a/adminSiteClient/SuggestedChartRevisionList.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import React from "react" -import { observer } from "mobx-react" -import * as lodash from "lodash" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js" -import { - faQuestionCircle, - faCheckCircle, - faTimesCircle, - faExclamationCircle, -} from "@fortawesome/free-solid-svg-icons" - -import { AdminAppContext, AdminAppContextType } from "./AdminAppContext.js" -import { BAKED_GRAPHER_URL } from "../settings/clientSettings.js" -import { ChartListItem } from "./ChartList.js" -import { Link } from "./Link.js" -import { SuggestedChartRevisionStatus } from "@ourworldindata/utils" -import { Timeago } from "./Forms.js" - -export interface SuggestedChartRevisionListItem { - id: number - chartId: number - createdByFullName: string - updatedByFullName: string - status: SuggestedChartRevisionStatus - decisionReason: string - suggestedReason: string - createdAt: Date - updatedAt: Date - originalConfig: ChartListItem -} - -@observer -class SuggestedChartRevisionRow extends React.Component<{ - suggestedChartRevision: SuggestedChartRevisionListItem - searchHighlight?: (text: string) => any -}> { - static contextType = AdminAppContext - context!: AdminAppContextType - - render() { - const { suggestedChartRevision, searchHighlight } = this.props - - const highlight = searchHighlight || lodash.identity - - return ( - - - - {suggestedChartRevision.id} - - - - - {suggestedChartRevision.originalConfig.id} - - - - {suggestedChartRevision.originalConfig.isPublished ? ( - - {highlight( - suggestedChartRevision.originalConfig.title ?? - "" - )} - - ) : ( - - Draft: {" "} - {highlight( - suggestedChartRevision.originalConfig.title ?? - "" - )} - - )}{" "} - {suggestedChartRevision.originalConfig.variantName ? ( - - ( - {highlight( - suggestedChartRevision.originalConfig - .variantName - )} - ) - - ) : undefined} - {suggestedChartRevision.originalConfig.internalNotes && ( -
    - {highlight( - suggestedChartRevision.originalConfig - .internalNotes - )} -
    - )} - - - {suggestedChartRevision.suggestedReason - ? suggestedChartRevision.suggestedReason - : ""} - - - - - - {" "} - {highlight( - ( - suggestedChartRevision.status as unknown as string - ).toUpperCase() - )} - {suggestedChartRevision.updatedByFullName && ( - - )} - - - {suggestedChartRevision.decisionReason - ? suggestedChartRevision.decisionReason - : ""} - - - ) - } -} - -@observer -export class SuggestedChartRevisionList extends React.Component<{ - suggestedChartRevisions: SuggestedChartRevisionListItem[] - searchHighlight?: (text: string) => any -}> { - static contextType = AdminAppContext - context!: AdminAppContextType - - render() { - const { suggestedChartRevisions, searchHighlight } = this.props - return ( - - - - - - - - - - - - - - {suggestedChartRevisions.map((suggestedChartRevision) => ( - - ))} - -
    Suggested revision IdChart IdTitleReason suggestedRevision suggested byStatusDecision reason
    - ) - } -} - -@observer -export class SuggestedChartRevisionStatusIcon extends React.Component<{ - status: SuggestedChartRevisionStatus - setColor?: boolean -}> { - static defaultProps = { - setColor: true, - } - render() { - const { status, setColor } = this.props - let color = "#9E9E9E" - let icon = faQuestionCircle - if (status === SuggestedChartRevisionStatus.approved) { - color = "#0275d8" - icon = faCheckCircle - } else if (status === SuggestedChartRevisionStatus.rejected) { - color = "#d9534f" - icon = faTimesCircle - } else if (status === SuggestedChartRevisionStatus.flagged) { - color = "#f0ad4e" - icon = faExclamationCircle - } - return - } -} diff --git a/adminSiteClient/SuggestedChartRevisionListPage.tsx b/adminSiteClient/SuggestedChartRevisionListPage.tsx deleted file mode 100644 index 92e5a7add60..00000000000 --- a/adminSiteClient/SuggestedChartRevisionListPage.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import React from "react" -import { observer } from "mobx-react" -import { observable, computed, action } from "mobx" -import fuzzysort from "fuzzysort" -import { Link } from "react-router-dom" - -import { TextField } from "./Forms.js" -import { AdminLayout } from "./AdminLayout.js" -import { - uniq, - SortOrder, - getStylesForTargetHeight, -} from "@ourworldindata/utils" -import Select from "react-select" -import { highlight as fuzzyHighlight } from "@ourworldindata/grapher" -import { - SuggestedChartRevisionList, - SuggestedChartRevisionListItem, -} from "./SuggestedChartRevisionList.js" -import { AdminAppContext, AdminAppContextType } from "./AdminAppContext.js" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js" -import { - faSortAlphaDown, - faSortAlphaUpAlt, -} from "@fortawesome/free-solid-svg-icons" - -interface Searchable { - suggestedChartRevision: SuggestedChartRevisionListItem - term?: Fuzzysort.Prepared -} - -@observer -export class SuggestedChartRevisionListPage extends React.Component { - static contextType = AdminAppContext - context!: AdminAppContextType - - @observable searchInput?: string - @observable maxVisibleCharts = 50 - @observable suggestedChartRevisions: SuggestedChartRevisionListItem[] = [] - @observable numTotalRows?: number - - @observable sortBy: string = "updatedAt" - @observable sortOrder: SortOrder = SortOrder.desc - - @computed get searchIndex(): Searchable[] { - const searchIndex: Searchable[] = [] - for (const suggestedChartRevision of this.suggestedChartRevisions) { - const originalConfig = suggestedChartRevision.originalConfig - searchIndex.push({ - suggestedChartRevision: suggestedChartRevision, - term: fuzzysort.prepare(` - ${suggestedChartRevision.status || ""} - ${originalConfig.title} - ${originalConfig.variantName || ""} - ${originalConfig.internalNotes || ""} - `), - }) - } - - return searchIndex - } - - @computed - get suggestedChartRevisionsToShow(): SuggestedChartRevisionListItem[] { - const { searchInput, searchIndex, maxVisibleCharts } = this - if (searchInput) { - const results = fuzzysort.go(searchInput, searchIndex, { - limit: 50, - key: "term", - }) - return uniq( - results.map((result: any) => result.obj.suggestedChartRevision) - ) - } else { - return this.suggestedChartRevisions.slice(0, maxVisibleCharts) - } - } - - @action.bound async getData() { - const { admin } = this.context - const json = await admin.getJSON("/api/suggested-chart-revisions", { - sortBy: this.sortBy, - sortOrder: this.sortOrder, - }) - this.suggestedChartRevisions = json.suggestedChartRevisions - this.numTotalRows = json.numTotalRows - } - - @action.bound onSearchInput(input: string) { - this.searchInput = input - } - - @action.bound onShowMore() { - this.maxVisibleCharts += 50 - } - - @action.bound onSortByChange(selected: any) { - this.sortBy = selected.value - void this.getData() - } - - @action.bound onSortOrderChange(value: SortOrder) { - this.sortOrder = value - void this.getData() - } - - componentDidMount() { - void this.getData() - } - - render() { - const { suggestedChartRevisionsToShow, searchInput, numTotalRows } = - this - - const highlight = (text: string) => { - if (this.searchInput) { - const html = - fuzzyHighlight(fuzzysort.single(this.searchInput, text)) ?? - text - return - } else return text - } - - return ( - -
    -
    -
    - - Showing {suggestedChartRevisionsToShow.length}{" "} - of {numTotalRows} suggested revisions - - - Go to approval tool - - - Upload revisions - -
    - -
    -
    -
    - Sort by:{" "} - - this.onSortOrderChange( - SortOrder.asc - ) - } - name="sortOrder" - id="asc" - checked={ - this.sortOrder === SortOrder.asc - } - />{" "} - - - -
    -
    - - - {!searchInput && ( - - )} -
    -
    - ) - } -} diff --git a/adminSiteClient/admin.scss b/adminSiteClient/admin.scss index ebfbeafeffc..943d164c532 100644 --- a/adminSiteClient/admin.scss +++ b/adminSiteClient/admin.scss @@ -700,8 +700,7 @@ main:not(.ChartEditorPage):not(.GdocsEditPage) { } .ChartIndexPage, -.UsersIndexPage, -.SuggestedChartRevisionListPage { +.UsersIndexPage { .topRow { display: flex; align-items: center; @@ -996,245 +995,6 @@ main:not(.ChartEditorPage):not(.GdocsEditPage) { } } -.SuggestedChartRevisionApproverPage { - display: flex; - flex-direction: column; - flex-grow: 1; - - .params { - display: flex; - flex-direction: column; - justify-content: space-evenly; - background-color: #f9f9f9; - padding: 1.2em; - border-radius: 0.4em; - margin: 0.4em 0; - // box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); - } - - .collapsible { - padding: 0.2em 0em; - // background-color: #f9f9f9; - // padding: 0.5em; - // border-radius: 0.4em; - // margin: 0.4em 0; - } - - .readme { - ul, - ol { - margin-left: 1.2em; - padding-top: 5px; - } - - li { - padding-bottom: 5px; - } - } - .settings { - .flex-row { - display: flex; - flex-direction: row; - } - } - - .settings > * { - margin-bottom: 10px; - } - - .btn-group .btn { - width: 50px; - } - - .form-group > label { - margin-bottom: 0; - } - - .meta { - list-style-type: none; - } - - .charts-view { - display: flex; - flex-direction: row; - flex-grow: 1; - flex-wrap: wrap; - align-items: center; - justify-content: center; - - .chart-view { - padding: 1em; - - .header { - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: center; - justify-content: flex-start; - margin-bottom: 5px; - - h5 { - margin-bottom: 0; - margin-right: 5px; - } - - span { - margin-right: 10px; - } - } - - .json-view { - // width: 100%; - // height: 100%; - background-color: #eeeeee; - overflow-y: auto; - border: 1px solid rgba(0, 0, 0, 0.2); - border-radius: 0.4em; - - pre { - white-space: pre-wrap; - } - } - } - } - - .controls { - margin: 0.1em auto 0 auto; - display: flex; - flex-direction: column; - flex-wrap: wrap; - justify-content: center; - align-items: center; - min-width: 400px; - - .btn { - margin-left: 5px; - margin-right: 5px; - } - - .row-input { - padding-bottom: 10px; - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: center; - justify-content: center; - padding-top: 0.5rem; - - .form-group { - width: 80px; - padding-top: 0px; - padding-bottom: 0px; - padding-left: 10px; - padding-right: 10px; - margin: 0px; - input { - text-align: center; - } - } - } - - .form-group { - padding: 10px; - align-self: stretch; - padding-left: 25%; - padding-right: 25%; - - textarea { - height: 3rem; - } - } - } - - .warning { - padding: 10px; - margin: 10px auto; - text-align: center; - max-width: 720px; - } - - .references { - .list-group { - margin-top: 0; - margin-bottom: 0.5em; - } - } - - h3.grapherChart { - // border-bottom: 1px darkgray dotted; - text-decoration: underline; - text-decoration-style: dotted; - text-decoration-color: darkgray; - } -} - -.SuggestedChartRevisionListPage { - .settings { - display: flex; - flex-direction: row; - flex-wrap: wrap; - } - - .settings > div { - margin-right: 10px; - } -} - -.SuggestedChartRevisionImportPage { - section, - .collapsible { - background-color: #f9f9f9; - padding: 1.2em; - border-radius: 0.4em; - margin: 0.4em 0; - box-shadow: - 0 1px 3px rgba(0, 0, 0, 0.12), - 0 1px 2px rgba(0, 0, 0, 0.24); - } - .import-form { - input { - max-width: 1000px; - } - - label { - font-size: 1.2rem; - } - } - - .readme { - ul, - ol { - margin-left: 1.2em; - padding-top: 5px; - } - - li { - padding-bottom: 5px; - } - - .snippet { - background-color: #e8e8e8; - padding: 10px; - border-radius: 0.4rem; - } - } - - .message { - margin: 1rem 0; - padding: 1rem; - border-radius: 0.4rem; - opacity: 0.87; - color: #fff; - box-shadow: - 0 1px 3px rgba(0, 0, 0, 0.12), - 0 1px 2px rgba(0, 0, 0, 0.24); - } - - .message.bg-warning { - color: #000; - } -} - .BulkDownloadPage { section { background-color: #f9f9f9; diff --git a/adminSiteServer/apiRouter.ts b/adminSiteServer/apiRouter.ts index 58ecda1740d..27c0f0e726c 100644 --- a/adminSiteServer/apiRouter.ts +++ b/adminSiteServer/apiRouter.ts @@ -36,7 +36,6 @@ import { OwidGdocPostInterface, parseIntOrUndefined, DbRawPostWithGdocPublishStatus, - SuggestedChartRevisionStatus, OwidVariableWithSource, OwidChartDimensionInterface, DimensionProperty, @@ -95,11 +94,6 @@ import { syncDatasetToGitRepo, removeDatasetFromGitRepo, } from "./gitDataExport.js" -import { - getQueryEnrichedSuggestedChartRevision, - getQueryEnrichedSuggestedChartRevisions, - isValidStatus, -} from "../db/model/SuggestedChartRevision.js" import { denormalizeLatestCountryData } from "../baker/countryProfiles.js" import { indexIndividualGdocPost, @@ -765,11 +759,6 @@ deleteRouteWithRWTransaction( `DELETE FROM chart_slug_redirects WHERE chart_id=?`, [chart.id] ) - await db.knexRaw( - trx, - `DELETE FROM suggested_chart_revisions WHERE chartId=?`, - [chart.id] - ) await db.knexRaw(trx, `DELETE FROM charts WHERE id=?`, [chart.id]) if (chart.isPublished) @@ -782,216 +771,6 @@ deleteRouteWithRWTransaction( } ) -getRouteWithROTransaction( - apiRouter, - "/suggested-chart-revisions", - async (req, res, trx) => { - const isValidSortBy = (sortBy: string) => { - return [ - "updatedAt", - "createdAt", - "suggestedReason", - "id", - "chartId", - "status", - "variableId", - "chartUpdatedAt", - "chartCreatedAt", - ].includes(sortBy) - } - const isValidSortOrder = (sortOrder: string) => { - return ( - sortOrder !== undefined && - sortOrder !== null && - ["ASC", "DESC"].includes(sortOrder.toUpperCase()) - ) - } - const limit = - req.query.limit !== undefined ? expectInt(req.query.limit) : 10000 - const offset = - req.query.offset !== undefined ? expectInt(req.query.offset) : 0 - const sortBy = isValidSortBy(req.query.sortBy as string) - ? req.query.sortBy - : "updatedAt" - const sortOrder = isValidSortOrder(req.query.sortOrder as string) - ? (req.query.sortOrder as string).toUpperCase() - : "DESC" - const status: string | null = isValidStatus( - req.query.status as SuggestedChartRevisionStatus - ) - ? (req.query.status as string) - : null - - let orderBy - if (sortBy === "variableId") { - orderBy = - "CAST(scr.suggestedConfig->>'$.dimensions[0].variableId' as SIGNED)" - } else if (sortBy === "chartUpdatedAt") { - orderBy = "c.updatedAt" - } else if (sortBy === "chartCreatedAt") { - orderBy = "c.createdAt" - } else { - orderBy = `scr.${sortBy}` - } - - const numTotalRows = ( - await db.knexRaw<{ count: number }>( - trx, - ` - SELECT COUNT(*) as count - FROM suggested_chart_revisions - ${status ? "WHERE status = ?" : ""} - `, - status ? [status] : [] - ) - )[0].count - - const enrichedSuggestedChartRevisions = - await getQueryEnrichedSuggestedChartRevisions( - trx, - orderBy, - sortOrder, - status, - limit, - offset - ) - - return { - suggestedChartRevisions: enrichedSuggestedChartRevisions, - numTotalRows: numTotalRows, - } - } -) - -getRouteWithROTransaction( - apiRouter, - "/suggested-chart-revisions/:suggestedChartRevisionId", - async (req, res, trx) => { - const suggestedChartRevisionId = expectInt( - req.params.suggestedChartRevisionId - ) - - const suggestedChartRevision = getQueryEnrichedSuggestedChartRevision( - trx, - suggestedChartRevisionId - ) - - if (!suggestedChartRevision) { - throw new JsonError( - `No suggested chart revision by id '${suggestedChartRevisionId}'`, - 404 - ) - } - - return { - suggestedChartRevision: suggestedChartRevision, - } - } -) - -postRouteWithRWTransaction( - apiRouter, - "/suggested-chart-revisions/:suggestedChartRevisionId/update", - async (req, res, trx) => { - const suggestedChartRevisionId = expectInt( - req.params.suggestedChartRevisionId - ) - - // Note: there was a suggestedConfig here that was not used - might have been a - // mistake in a refactoring that wasn't found before? - const { status, decisionReason } = req.body as { - status: string - decisionReason: string - } - - const suggestedChartRevision = - await getQueryEnrichedSuggestedChartRevision( - trx, - suggestedChartRevisionId - ) - - if (!suggestedChartRevision) { - throw new JsonError( - `No suggested chart revision found for id '${suggestedChartRevisionId}'`, - 404 - ) - } - - const canUpdate = - (status === "approved" && suggestedChartRevision.canApprove) || - (status === "rejected" && suggestedChartRevision.canReject) || - (status === "pending" && suggestedChartRevision.canPending) || - (status === "flagged" && suggestedChartRevision.canFlag) - if (!canUpdate) { - throw new JsonError( - `Suggest chart revision ${suggestedChartRevisionId} cannot be ` + - `updated with status="${status}".`, - 404 - ) - } - - await db.knexRaw( - trx, - ` - UPDATE suggested_chart_revisions - SET status=?, decisionReason=?, updatedAt=?, updatedBy=? - WHERE id = ? - `, - [ - status, - decisionReason, - new Date(), - res.locals.user.id, - suggestedChartRevisionId, - ] - ) - - // Update config ONLY when APPROVE button is clicked - // Makes sense when the suggested config is a sugegstion by GPT, otherwise is redundant but we are cool with it - if (status === SuggestedChartRevisionStatus.approved) { - await db.knexRaw( - trx, - ` - UPDATE suggested_chart_revisions - SET suggestedConfig=? - WHERE id = ? - `, - [ - JSON.stringify(suggestedChartRevision.suggestedConfig), - suggestedChartRevisionId, - ] - ) - } - // note: the calls to saveGrapher() below will never overwrite a config - // that has been changed since the suggestedConfig was created, because - // if the config has been changed since the suggestedConfig was created - // then canUpdate will be false (so an error would have been raised - // above). - - if (status === "approved" && suggestedChartRevision.canApprove) { - await saveGrapher( - trx, - res.locals.user, - suggestedChartRevision.suggestedConfig, - suggestedChartRevision.existingConfig - ) - } else if ( - status === "rejected" && - suggestedChartRevision.canReject && - suggestedChartRevision.status === "approved" - ) { - await saveGrapher( - trx, - res.locals.user, - suggestedChartRevision.originalConfig, - suggestedChartRevision.existingConfig - ) - } - - return { success: true } - } -) - getRouteWithROTransaction(apiRouter, "/users.json", async (req, res, trx) => ({ users: await trx .select( diff --git a/db/model/SuggestedChartRevision.ts b/db/model/SuggestedChartRevision.ts deleted file mode 100644 index 928e8ce4a48..00000000000 --- a/db/model/SuggestedChartRevision.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { - DbEnrichedSuggestedChartRevision, - DbRawSuggestedChartRevision, - GrapherInterface, - SuggestedChartRevisionStatus, -} from "@ourworldindata/utils" -import { KnexReadonlyTransaction, knexRaw, knexRawFirst } from "../db.js" - -export function isValidStatus(status: SuggestedChartRevisionStatus): boolean { - return Object.values(SuggestedChartRevisionStatus).includes(status) -} - -type DbRawQuerySuggestedChartRevisions = Pick< - DbRawSuggestedChartRevision, - | "id" - | "chartId" - | "updatedAt" - | "createdAt" - | "suggestedReason" - | "decisionReason" - | "status" - | "suggestedConfig" - | "originalConfig" - | "changesInDataSummary" - | "experimental" -> & { - createdById: number - updatedById: number - createdByFullName: string - updatedByFullName: string - existingConfig: string - chartUpdatedAt: Date - chartCreatedAt: Date -} - -type DbSemiEnrichedQuerySuggestedChartRevisions = Pick< - DbEnrichedSuggestedChartRevision, - | "id" - | "chartId" - | "updatedAt" - | "createdAt" - | "suggestedReason" - | "decisionReason" - | "status" - | "suggestedConfig" - | "originalConfig" - | "changesInDataSummary" - | "experimental" -> & { - createdById: number - updatedById: number - createdByFullName: string - updatedByFullName: string - existingConfig: GrapherInterface - chartUpdatedAt: Date - chartCreatedAt: Date -} - -type DbEnrichedQuerySuggestedChartRevisions = - DbSemiEnrichedQuerySuggestedChartRevisions & { - canApprove: boolean - canReject: boolean - canFlag: boolean - canPending: boolean - } - -function parseQuerySuggestedChartRevision( - row: DbRawQuerySuggestedChartRevisions -): DbEnrichedQuerySuggestedChartRevisions { - const suggestedConfig = JSON.parse(row.suggestedConfig) - const existingConfig = JSON.parse(row.existingConfig) - const originalConfig = JSON.parse(row.originalConfig) - const experimental = row.experimental ? JSON.parse(row.experimental) : null - const semiEnriched = { - ...row, - suggestedConfig, - existingConfig, - originalConfig, - experimental, - } - const canApprove = checkCanApprove(semiEnriched) - const canReject = checkCanReject(semiEnriched) - const canFlag = checkCanFlag(semiEnriched) - const canPending = checkCanPending(semiEnriched) - return { - ...semiEnriched, - canApprove, - canReject, - canFlag, - canPending, - } -} - -const selectFields = `scr.id, scr.chartId, scr.updatedAt, scr.createdAt, - scr.suggestedReason, scr.decisionReason, scr.status, - scr.suggestedConfig, scr.originalConfig, scr.changesInDataSummary, - scr.experimental, - createdByUser.id as createdById, - updatedByUser.id as updatedById, - createdByUser.fullName as createdByFullName, - updatedByUser.fullName as updatedByFullName, - c.config as existingConfig, c.updatedAt as chartUpdatedAt, - c.createdAt as chartCreatedAt -` - -export async function getQueryEnrichedSuggestedChartRevisions( - trx: KnexReadonlyTransaction, - orderBy: string, - sortOrder: string, - status: string | null, - limit: number, - offset: number -): Promise { - const rawSuggestedChartRevisions = - await knexRaw( - trx, - `-- sql - SELECT ${selectFields} - FROM suggested_chart_revisions as scr - LEFT JOIN charts c on c.id = scr.chartId - LEFT JOIN users createdByUser on createdByUser.id = scr.createdBy - LEFT JOIN users updatedByUser on updatedByUser.id = scr.updatedBy - ${status ? "WHERE scr.status = ?" : ""} - ORDER BY ${orderBy} ${sortOrder} - LIMIT ? OFFSET ? - `, - status ? [status, limit, offset] : [limit, offset] - ) - - const enrichedSuggestedChartRevisions = rawSuggestedChartRevisions.map( - parseQuerySuggestedChartRevision - ) - return enrichedSuggestedChartRevisions -} - -export async function getQueryEnrichedSuggestedChartRevision( - trx: KnexReadonlyTransaction, - id: number -): Promise { - const rawSuggestedChartRevisions = - await knexRawFirst( - trx, - `-- sql - SELECT ${selectFields} - FROM suggested_chart_revisions as scr - LEFT JOIN charts c on c.id = scr.chartId - LEFT JOIN users createdByUser on createdByUser.id = scr.createdBy - LEFT JOIN users updatedByUser on updatedByUser.id = scr.updatedBy - WHERE scr.id = ? - `, - [id] - ) - - if (!rawSuggestedChartRevisions) return null - - const enrichedSuggestedChartRevisions = parseQuerySuggestedChartRevision( - rawSuggestedChartRevisions - ) - return enrichedSuggestedChartRevisions -} - -export function checkCanApprove( - suggestedChartRevision: DbSemiEnrichedQuerySuggestedChartRevisions -): boolean { - // note: a suggestion can be approved if status == "rejected" | - // "flagged" | "pending" AND the original config version equals - // the existing config version (i.e. the existing chart has not - // been changed since the suggestion was created). - const status = suggestedChartRevision.status - const originalVersion = suggestedChartRevision.originalConfig?.version - const existingVersion = suggestedChartRevision.existingConfig?.version - const originalVersionExists = - originalVersion !== null && originalVersion !== undefined - const existingVersionExists = - existingVersion !== null && existingVersion !== undefined - if ( - [ - SuggestedChartRevisionStatus.rejected, - SuggestedChartRevisionStatus.flagged, - SuggestedChartRevisionStatus.pending, - ].includes(status as any) && - originalVersionExists && - existingVersionExists && - originalVersion === existingVersion - ) { - return true - } - return false -} - -export function checkCanReject( - suggestedChartRevision: DbSemiEnrichedQuerySuggestedChartRevisions -): boolean { - // note: a suggestion can be rejected if: (1) status == - // "pending" | "flagged"; or (2) status == "approved" and the - // suggested config version equals the existing chart version - // (i.e. the existing chart has not changed since the suggestion - // was approved). - const status = suggestedChartRevision.status - const suggestedVersion = suggestedChartRevision.suggestedConfig?.version - const existingVersion = suggestedChartRevision.existingConfig?.version - const suggestedVersionExists = - suggestedVersion !== null && suggestedVersion !== undefined - const existingVersionExists = - existingVersion !== null && existingVersion !== undefined - if ( - [ - SuggestedChartRevisionStatus.flagged, - SuggestedChartRevisionStatus.pending, - ].includes(status as any) - ) { - return true - } - if ( - status === "approved" && - suggestedVersionExists && - existingVersionExists && - suggestedVersion === existingVersion - ) { - return true - } - return false -} - -export function checkCanFlag( - suggestedChartRevision: DbSemiEnrichedQuerySuggestedChartRevisions -): boolean { - // note: a suggestion can be flagged if status == "pending" or - // if it is already flagged. Flagging a suggestion that is - // already flagged is a hack for updating the decisionReason - // column in the SuggestedChartRevisionApproverPage UI without - // changing the status column. - const status = suggestedChartRevision.status - if ( - [ - SuggestedChartRevisionStatus.flagged, - SuggestedChartRevisionStatus.pending, - ].includes(status as any) - ) { - return true - } - return false -} - -export function checkCanPending( - _suggestedChartRevision: DbSemiEnrichedQuerySuggestedChartRevisions -): boolean { - // note: a suggestion cannot be altered to pending from another status - return false -} diff --git a/packages/@ourworldindata/types/src/dbTypes/SuggestedChartRevisions.ts b/packages/@ourworldindata/types/src/dbTypes/SuggestedChartRevisions.ts deleted file mode 100644 index 5248fea42c3..00000000000 --- a/packages/@ourworldindata/types/src/dbTypes/SuggestedChartRevisions.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { JsonString } from "../domainTypes/Various.js" -import { GrapherInterface } from "../grapherTypes/GrapherTypes.js" -import { parseChartConfig, serializeChartConfig } from "./Charts.js" - -export interface SuggestedChartRevisionsExperimental { - gpt: { - model: string - suggestions: { - title: string - subtitle: string - }[] - } -} - -export const SuggestedChartRevisionsTableName = "suggested_chart_revisions" -export interface DbInsertSuggestedChartRevision { - changesInDataSummary?: string | null - chartId: number - createdAt?: Date - createdBy: number - decisionReason?: string | null - experimental?: JsonString | null - id?: string - isPendingOrFlagged?: number | null - originalConfig: JsonString - originalVersion: number - status: string - suggestedConfig: JsonString - suggestedReason?: string | null - suggestedVersion: number - updatedAt?: Date | null - updatedBy?: number | null -} -export type DbRawSuggestedChartRevision = - Required - -export type DbEnrichedSuggestedChartRevision = Omit< - DbRawSuggestedChartRevision, - "originalConfig" | "suggestedConfig" | "experimental" -> & { - originalConfig: GrapherInterface - suggestedConfig: GrapherInterface - experimental: SuggestedChartRevisionsExperimental | null -} - -export function parseSuggestedChartRevisionsExperimental( - experimental: JsonString | null -): SuggestedChartRevisionsExperimental | null { - return experimental ? JSON.parse(experimental) : null -} - -export function serializeSuggestedChartRevisionsExperimental( - experimental: SuggestedChartRevisionsExperimental | null -): JsonString | null { - return experimental ? JSON.stringify(experimental) : null -} - -export function parseSuggestedChartRevisionsRow( - row: DbRawSuggestedChartRevision -): DbEnrichedSuggestedChartRevision { - return { - ...row, - originalConfig: parseChartConfig(row.originalConfig), - suggestedConfig: parseChartConfig(row.suggestedConfig), - experimental: parseSuggestedChartRevisionsExperimental( - row.experimental - ), - } -} - -export function serializeSuggestedChartRevisionsRow( - row: DbEnrichedSuggestedChartRevision -): DbRawSuggestedChartRevision { - return { - ...row, - originalConfig: serializeChartConfig(row.originalConfig), - suggestedConfig: serializeChartConfig(row.suggestedConfig), - experimental: serializeSuggestedChartRevisionsExperimental( - row.experimental - ), - } -} diff --git a/packages/@ourworldindata/types/src/domainTypes/Various.ts b/packages/@ourworldindata/types/src/domainTypes/Various.ts index 0f491eff454..946339baa14 100644 --- a/packages/@ourworldindata/types/src/domainTypes/Various.ts +++ b/packages/@ourworldindata/types/src/domainTypes/Various.ts @@ -53,13 +53,6 @@ export enum TaggableType { export type TopicId = number -export enum SuggestedChartRevisionStatus { - pending = "pending", - approved = "approved", - rejected = "rejected", - flagged = "flagged", -} - // Exception format that can be easily given as an API error export class JsonError extends Error { status: number diff --git a/packages/@ourworldindata/types/src/index.ts b/packages/@ourworldindata/types/src/index.ts index 7dc9ee05c91..0b9a1f9ca41 100644 --- a/packages/@ourworldindata/types/src/index.ts +++ b/packages/@ourworldindata/types/src/index.ts @@ -12,7 +12,6 @@ export { JsonError, type SerializedGridProgram, SiteFooterContext, - SuggestedChartRevisionStatus, TaggableType, type TopicId, type OwidVariableId, @@ -596,16 +595,6 @@ export { parseSourcesRow, serializeSourcesRow, } from "./dbTypes/Sources.js" -export { - type DbInsertSuggestedChartRevision, - type DbRawSuggestedChartRevision, - type DbEnrichedSuggestedChartRevision, - SuggestedChartRevisionsTableName, - parseSuggestedChartRevisionsExperimental, - serializeSuggestedChartRevisionsExperimental, - parseSuggestedChartRevisionsRow, - serializeSuggestedChartRevisionsRow, -} from "./dbTypes/SuggestedChartRevisions.js" export { type DbInsertTag, type DbPlainTag,