Skip to content

Commit

Permalink
* Got new subtree0operations dialog working for the "set access polic…
Browse files Browse the repository at this point in the history
…y" operation. Closes #262.
  • Loading branch information
Venryx committed Apr 26, 2024
1 parent f9dc28b commit 5477a70
Show file tree
Hide file tree
Showing 14 changed files with 201 additions and 50 deletions.
7 changes: 7 additions & 0 deletions Packages/app-server/src/db/commands/run_command_batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::db::commands::_command::command_boilerplate;
use crate::db::commands::_shared::increment_edit_counts::increment_edit_counts_if_valid;
use crate::db::commands::_shared::record_command_run::record_command_run;
use crate::db::commands::add_node_link::{add_node_link, AddNodeLinkInput};
use crate::db::commands::update_node::update_node;
use crate::db::general::permission_helpers::{assert_user_can_add_phrasing, assert_user_can_add_child};
use crate::db::general::sign_in_::jwt_utils::{resolve_jwt_to_user_info, get_user_info_from_gql_ctx};
use crate::db::node_links::{NodeLinkInput, NodeLink};
Expand All @@ -34,6 +35,7 @@ use crate::utils::general::data_anchor::{DataAnchorFor1};
use super::_command::{upsert_db_entry_by_id_for_struct, NoExtras, tbd};
use super::_shared::add_node::add_node;
use super::add_child_node::{AddChildNodeInput, add_child_node, AddChildNodeExtras};
use super::update_node::UpdateNodeInput;

wrap_slow_macros!{

Expand Down Expand Up @@ -75,6 +77,10 @@ wrap_slow_macros!{

let result = add_child_node(&ctx, &actor, false, command_input_final, AddChildNodeExtras { avoid_recording_command_run: true }).await.map_err(to_sub_err)?;
command_results.push(serde_json::to_value(result).map_err(to_sub_err)?);
} else if let Some(command_input) = &command.updateNode {
let command_input_final = command_input.clone();
let result = update_node(&ctx, &actor, false, command_input_final, NoExtras::default()).await.map_err(to_sub_err)?;
command_results.push(serde_json::to_value(result).map_err(to_sub_err)?);
} else {
Err(anyhow!("Command #{} had no recognized command subfield.", index)).map_err(to_sub_err)?;
}
Expand All @@ -99,6 +105,7 @@ pub struct RunCommandBatchInput {
#[derive(InputObject, Deserialize, Serialize, Clone)]
pub struct CommandEntry {
pub addChildNode: Option<AddChildNodeInput>,
pub updateNode: Option<UpdateNodeInput>,

// extras
pub setParentNodeToResultOfCommandAtIndex: Option<usize>, // used by: addChildNode
Expand Down
4 changes: 2 additions & 2 deletions Packages/app-server/src/db/commands/update_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ wrap_slow_macros!{
}
}

#[derive(InputObject, Serialize, Deserialize)]
#[derive(InputObject, Serialize, Deserialize, Clone)]
pub struct UpdateNodeInput {
pub id: String,
pub updates: NodeUpdates,
}

#[derive(SimpleObject, Debug)]
#[derive(SimpleObject, Debug, Serialize)]
pub struct UpdateNodeResult {
#[graphql(name = "_useTypenameFieldInstead")] __: String,
}
Expand Down
2 changes: 1 addition & 1 deletion Packages/app-server/src/db/nodes_/_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ pub struct NodeInput {
pub extras: CanOmit<JSONValue>,
}

#[derive(InputObject, Serialize, Deserialize)]
#[derive(InputObject, Serialize, Deserialize, Clone)]
pub struct NodeUpdates {
pub accessPolicy: CanOmit<String>,
//pub multiPremiseArgument: CanNullOrOmit<bool>, // excluded, since updating this field has external side-effects that would be unexpected in a generic update_x command
Expand Down
8 changes: 6 additions & 2 deletions Packages/client/Source/Store/main/maps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import {MapState} from "./maps/mapStates/@MapState.js";
import {GetMapView, GetNodeView} from "./maps/mapViews/$mapView.js";
import {GetPlaybackInfo} from "./maps/mapStates/PlaybackAccessors/Basic.js";
import {GetPathVisibilityInfoAfterEffects, GetPlaybackEffects} from "./maps/mapStates/PlaybackAccessors/ForEffects.js";
import {SubtreeOperation} from "../../UI/@Shared/Maps/Node/NodeUI_Menu/MI_SubtreeOps.js";
import {SubtreeIncludeKeys} from "../../UI/@Shared/Maps/Node/NodeUI_Menu/Dialogs/SubtreeOpsHelpers.js";
import {SubtreeIncludeKeys, SubtreeOperation} from "../../UI/@Shared/Maps/Node/NodeUI_Menu/Dialogs/SubtreeOpsStructs.js";

export enum RatingPreviewType {
none = "none",
Expand Down Expand Up @@ -171,6 +170,11 @@ export class SubtreeOperationsDialogState {

// export
@O @ignore export_includeKeys = new SubtreeIncludeKeys();

// set access policy
//@O @ignore setPolicy_oldParentCounts = [] as number[];
@O @ignore setPolicy_oldAccessPolicies = [] as string[];
@O @ignore setPolicy_newPolicyID: string|n;
}

export const GetLastAcknowledgementTime = CreateAccessor({ctx: 1}, function(nodeID: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,7 @@ import {GetExpandedByDefaultAttachment, GetMedia, GetNode, GetNodeChildren, GetN
import {gql} from "web-vcore/nm/@apollo/client";
import {Assert, NN} from "web-vcore/nm/js-vextensions.js";
import {ClassKeys, CreateAccessor} from "web-vcore/nm/mobx-graphlink.js";

export class SubtreeIncludeKeys {
constructor(data?: Partial<SubtreeIncludeKeys>) {
Object.assign(this, data);
}
//nodes = ClassKeys<NodeL3>("id", "type", "rootNodeForMap", "c_currentRevision", "multiPremiseArgument", "argumentType");
nodes = ClassKeys<NodeL1>("id", "type", "rootNodeForMap", "c_currentRevision", "multiPremiseArgument", "argumentType");
nodeLinks = ClassKeys<NodeLink>("id", "parent", "child", "form", "polarity");
nodeRevisions = ClassKeys<NodeRevision>("id", "node", "phrasing", "attachments");
nodePhrasings = ClassKeys<NodePhrasing>("id", "node", "type", "text_base", "text_negation", "text_question", "text_narrative", "note", "terms", "references");
terms = ClassKeys<Term>("id", "name", "forms", "disambiguation", "type", "definition", "note");
medias = ClassKeys<Media>("id", "name", "type", "url", "description");
}
import {SubtreeIncludeKeys} from "./SubtreeOpsStructs.js";

export class SubtreeData_Server {
constructor(data?: Partial<SubtreeData_Server>) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {Media, NodeL1, NodeLink, NodePhrasing, NodeRevision, Term} from "dm_common";
import {ClassKeys} from "web-vcore/nm/mobx-graphlink.js";

export enum SubtreeOperation {
export = "export",
setAccessPolicy = "setAccessPolicy",
}

export class SubtreeIncludeKeys {
constructor(data?: Partial<SubtreeIncludeKeys>) {
Object.assign(this, data);
}
//nodes = ClassKeys<NodeL3>("id", "type", "rootNodeForMap", "c_currentRevision", "multiPremiseArgument", "argumentType");
nodes = ClassKeys<NodeL1>("id", "type", "rootNodeForMap", "c_currentRevision", "multiPremiseArgument", "argumentType");
nodeLinks = ClassKeys<NodeLink>("id", "parent", "child", "form", "polarity");
nodeRevisions = ClassKeys<NodeRevision>("id", "node", "phrasing", "attachments");
nodePhrasings = ClassKeys<NodePhrasing>("id", "node", "type", "text_base", "text_negation", "text_question", "text_narrative", "note", "terms", "references");
terms = ClassKeys<Term>("id", "name", "forms", "disambiguation", "type", "definition", "note");
medias = ClassKeys<Media>("id", "name", "type", "url", "description");
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ import {Clone, ModifyString, StartDownload} from "web-vcore/nm/js-vextensions.js
import {GetSchemaJSON, TableNameToDocSchemaName} from "web-vcore/nm/mobx-graphlink.js";
import {Button, CheckBox, Column, Row, RowLR, Select, Text, TextArea} from "web-vcore/nm/react-vcomponents.js";
import {BaseComponent, BaseComponentPlus} from "web-vcore/nm/react-vextensions.js";
import {gql, useQuery} from "web-vcore/nm/@apollo/client";
import {MI_SharedProps} from "../../NodeUI_Menu.js";
import {ConvertLocalSubtreeDataToServerStructure, GetServerSubtreeData_GQLQuery, PopulateLocalSubtreeData, SubtreeData_Server, SubtreeIncludeKeys} from "./SubtreeOpsHelpers.js";
import {useSubtreeRetrievalQueryOrAccessors} from "../MI_SubtreeOps.js";
import {ExportRetrievalMethod} from "../../../../../../Store/main/maps.js";
import {SubtreeIncludeKeys} from "./SubtreeOpsStructs.js";

enum ExportSubtreeUI_MidTab {
Nodes = 10,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import {store} from "Store";
import React from "react";
import {CheckBox, Column, Row, RowLR, Text} from "web-vcore/nm/react-vcomponents.js";
import React, {useState} from "react";
import {Button, Button_styles, CheckBox, Column, Row, RowLR, Text} from "web-vcore/nm/react-vcomponents.js";
import {BaseComponent, BaseComponentPlus} from "web-vcore/nm/react-vextensions.js";
import {Observer} from "web-vcore";
import {GetAccessPolicy} from "dm_common";
import {InfoButton, Observer, RunInAction_Set} from "web-vcore";
import {AccessPolicy, GetAccessPolicy, NodeL1} from "dm_common";
import {E} from "js-vextensions";
import {ShowMessageBox} from "web-vcore/nm/react-vmessagebox.js";
import {MI_SharedProps} from "../../NodeUI_Menu.js";
import {useSubtreeRetrievalQueryOrAccessors} from "../MI_SubtreeOps.js";
import {PolicyPicker, PolicyPicker_Button} from "../../../../../Database/Policies/PolicyPicker.js";
import {SubtreeIncludeKeys} from "./SubtreeOpsHelpers.js";
import {CommandEntry, RunCommandBatch} from "../../../../../../Utils/DB/RunCommandBatch.js";
import {SubtreeIncludeKeys} from "./SubtreeOpsStructs.js";

const splitAt = 150;

Expand All @@ -31,39 +34,170 @@ export class SubtreeOpsUI_SetAccessPolicy_Left extends BaseComponentPlus(
}

@Observer
export class SubtreeOpsUI_SetAccessPolicy_Right extends BaseComponent<{} & MI_SharedProps, {retrievalActive: boolean, newPolicyID: string}> {
export class SubtreeOpsUI_SetAccessPolicy_Right extends BaseComponent<{} & MI_SharedProps, {retrievalActive: boolean, serverImportInProgress: boolean, serverImport_commandsCompleted: number}> {
static initialState = {serverImport_commandsCompleted: 0};
render() {
const {mapID, node: rootNode, path: rootNodePath} = this.props;
const {retrievalActive, newPolicyID} = this.state;
const {retrievalActive, serverImportInProgress, serverImport_commandsCompleted} = this.state;
const dialogState = store.main.maps.subtreeOperationsDialog;
//const includeKeys = dialogState.export_includeKeys;
const {setPolicy_oldParentCounts, setPolicy_oldAccessPolicies, setPolicy_newPolicyID} = dialogState;
const includeKeys_minimal = new SubtreeIncludeKeys({
nodes: ["id"],
nodeLinks: [],
nodes: ["id", "accessPolicy"],
nodeLinks: ["parent", "child"],
nodeRevisions: [],
nodePhrasings: [],
terms: [],
medias: [],
});

const newPolicy = GetAccessPolicy(this.state.newPolicyID);
const newPolicy = GetAccessPolicy(setPolicy_newPolicyID);

const {subtreeData} = useSubtreeRetrievalQueryOrAccessors(rootNode, rootNodePath, includeKeys_minimal, dialogState.retrievalMethod, dialogState.maxExportDepth, retrievalActive);
const nodesRetrieved = subtreeData?.nodes?.length ?? 0;
const nodesRetrieved_orig = subtreeData?.nodes ?? [];
const nodesRetrieved_filtered = subtreeData?.nodes?.filter(node=>{
const parentIDs = subtreeData.nodeLinks?.filter(a=>a.child == node.id).map(a=>a.parent).Distinct() ?? [];
const parentCount = parentIDs.length;
if (!setPolicy_oldParentCounts.includes(parentCount)) return false;
if (!setPolicy_oldAccessPolicies.includes(node.accessPolicy)) return false;
return true;
}) ?? [];

const nodesInSubtree = subtreeData?.nodes?.map(node=>node.id).Distinct() ?? [];
const nodesInSubtree_parents = nodesInSubtree.map(nodeID=>subtreeData?.nodeLinks?.filter(a=>a.child == nodeID).map(a=>a.parent).Distinct() ?? []);
const parentCountsInSubtree = [
...nodesInSubtree_parents.map(a=>a.length),
...setPolicy_oldParentCounts, // add in any parent-counts that are already "checked" as a filter, even if there are currently no node matches (allows user to uncheck it)
].Distinct();
const parentCountsInSubtree_matches = parentCountsInSubtree.ToMap(count=>count, count=>nodesInSubtree_parents.filter(a=>a.length == count).length);

const accessPoliciesInSubtree = [
...(subtreeData?.nodes?.map(a=>a.accessPolicy) ?? []),
...setPolicy_oldAccessPolicies, // add in any polices that are already "checked" as a filter, even if there are currently no node matches (allows user to uncheck it)
].Distinct();
const accessPoliciesInSubtree_matches = accessPoliciesInSubtree.ToMap(policyID=>policyID, policyID=>subtreeData?.nodes?.filter(a=>a.accessPolicy == policyID).length ?? 0);

return (
<Column style={{flex: 1}}>
<Row>
<CheckBox text={`Start retrieval${nodesRetrieved > 0 ? ` (node count: ${nodesRetrieved})` : ""}`} value={retrievalActive} onChange={val=>this.SetState({retrievalActive: val})}/>
<CheckBox enabled={!serverImportInProgress} text={`Start retrieval${nodesRetrieved_orig.length > 0 ? ` (nodes in subtree: ${nodesRetrieved_orig.length}, after filtering: ${nodesRetrieved_filtered.length})` : ""}`}
value={retrievalActive} onChange={val=>this.SetState({retrievalActive: val})}/>
<Row ml="auto"></Row>
</Row>

<Row mt={20} style={{fontWeight: "bold"}}>Filtering</Row>
<Column mt={5}>
<Text>Filter by parent counts:</Text>
<Row mt={5} style={{flexWrap: "wrap", gap: 5}}>
{parentCountsInSubtree.OrderBy(a=>a).map(count=>{
return <Button key={count} enabled={!serverImportInProgress}
text={`${count} parent${count == 1 ? "" : "s"} (${parentCountsInSubtree_matches.get(count)} matches)`}
style={E(
{padding: "2px 5px"},
setPolicy_oldParentCounts.includes(count) && {backgroundColor: "rgba(30,100,30,.5)"},
)}
onClick={()=>{
RunInAction_Set(this, ()=>{
if (setPolicy_oldParentCounts.includes(count)) dialogState.setPolicy_oldParentCounts.Remove(count);
else dialogState.setPolicy_oldParentCounts.push(count);
});
}}/>;
})}
</Row>
</Column>
<Column mt={5}>
<Text>Filter by old access-policies:</Text>
<Column>
{accessPoliciesInSubtree.OrderByDescending(id=>accessPoliciesInSubtree_matches.get(id)).map(policyID=>{
//return <div key={policyID} style={E(filterEntry_styleBase)}>{policyID} [{accessPoliciesInSubtree_matches.get(policyID)}]</div>;
return <Row key={policyID} mt={5}>
<PolicyPicker_Button policyID={policyID} idTrimLength={3} enabled={!serverImportInProgress} style={E(
{flex: 1, padding: "3px 10px"},
//setPolicy_oldAccessPolicies.includes(policyID) && {backgroundColor: "rgba(30,100,30,.5)"},
// the above does not work fsr (never becomes green); to fix, we use this hack of using "background" key instead, and just ensure we always set it
!setPolicy_oldAccessPolicies.includes(policyID) && {background: Button_styles.root.backgroundColor},
setPolicy_oldAccessPolicies.includes(policyID) && {background: "rgba(30,100,30,.5)"},
)} onClick={()=>{
RunInAction_Set(this, ()=>{
if (setPolicy_oldAccessPolicies.includes(policyID)) dialogState.setPolicy_oldAccessPolicies.Remove(policyID);
else dialogState.setPolicy_oldAccessPolicies.push(policyID);
});
}}/>
<Text ml={5} style={{minWidth: 100, justifyContent: "center", fontSize: 13}}>({accessPoliciesInSubtree_matches.get(policyID)} matches)</Text>
</Row>;
})}
</Column>
</Column>

<Row mt={20} style={{fontWeight: "bold"}}>Modifications</Row>
<RowLR mt={5} splitAt={splitAt}>
<Text>New access-policy:</Text>
<PolicyPicker containerStyle={{flex: null}} value={newPolicyID} onChange={policyID=>this.SetState({newPolicyID: policyID})}>
<PolicyPicker_Button policyID={newPolicyID} idTrimLength={3} style={{padding: "3px 10px"}}/>
<PolicyPicker containerStyle={{flex: null}} value={setPolicy_newPolicyID} onChange={val=>RunInAction_Set(this, ()=>dialogState.setPolicy_newPolicyID = val)}>
<PolicyPicker_Button policyID={setPolicy_newPolicyID} idTrimLength={3} enabled={!serverImportInProgress} style={{padding: "3px 10px"}}/>
</PolicyPicker>
</RowLR>

<Row mt={20} style={{fontWeight: "bold"}}>Execution</Row>
<Row mt={5} p={3} style={{textAlign: "center", background: "rgba(255,255,0,.3)", color: "red", borderRadius: 25, border: "1px solid rgba(0,0,0,.3)"}}>
<div>
Warning: Once started, the batch operation cannot be stopped. It also cannot be undone. (short of a full database restore)
<InfoButton ml={5} style={{verticalAlign: "middle"}} text={`
Technically, you may be able to cancel the operation by quickly closing/refreshing the page to early-drop the graphql subscription.
Because these batch operations can complete quickly in some cases though, this is unreliable and shouldn't be relied on!
`.AsMultiline(0)}/>
</div>
</Row>
<Row mt={5}>
<Button enabled={newPolicy != null && nodesRetrieved_filtered.length > 0 && !serverImportInProgress} style={{flex: 1}}
text={`Start batch-setting of access-policies (${nodesRetrieved_filtered.length} nodes)`}
onClick={()=>{
ShowMessageBox({
title: `Start batch operation on ${nodesRetrieved_filtered.length} nodes?`, cancelButton: true,
message: `
If the node subtree is large, this could take a long time. You can view the progress in the progress label below the start button.
`.AsMultiline(0),
onOK: async()=>{
this.SetState({serverImportInProgress: true, serverImport_commandsCompleted: 0});
try {
const result = await PerformBatchOperation_SetNodeAccessPolicies(nodesRetrieved_filtered, newPolicy!, subcommandsCompleted=>{
this.SetState({serverImport_commandsCompleted: subcommandsCompleted});
});
ShowMessageBox({
title: "Batch operation succeeded",
message: `Batch operation has completed. Commands in batch completed: ${result.results.length ?? 0}`,
});
} catch (ex) {
ShowMessageBox({
title: `Batch operation failed`,
message: `Batch operation has failed (no changes *should* have been persisted). Error details: ${ex}`,
});
}
this.SetState({
retrievalActive: false, // also reset retrieval-active to false (so that UI doesn't show the stale retrieval results anymore)
serverImportInProgress: false, serverImport_commandsCompleted: 0,
});
},
});
}}/>
</Row>
{serverImportInProgress &&
<Row>
Progress: {serverImport_commandsCompleted}/{nodesRetrieved_filtered.length}
</Row>}
</Column>
);
}
}

async function PerformBatchOperation_SetNodeAccessPolicies(nodes: NodeL1[], newAccessPolicy: AccessPolicy, onProgress: (subcommandsCompleted: number)=>void) {
const commandEntries = nodes.map(node=>{
return {
updateNode: {
id: node.id,
updates: {accessPolicy: newAccessPolicy.id},
},
} as CommandEntry;
});

return await RunCommandBatch(commandEntries, onProgress);
}
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ class ImportSubtreeUI extends BaseComponent<
} catch (ex) {
ShowMessageBox({
title: `Import failed`,
message: `Import has failed. Error details: ${ex}`,
message: `Import has failed (no changes *should* have been persisted). Error details: ${ex}`,
});
}
this.SetState({serverImportInProgress: false, serverImport_commandsCompleted: 0});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {ExportRetrievalMethod} from "../../../../../Store/main/maps.js";
import {MI_SharedProps} from "../NodeUI_Menu.js";
import {SubtreeOpsUI_Export_Left, SubtreeOpsUI_Export_Right} from "./Dialogs/SubtreeOpsUI_Export.js";
import {SubtreeOpsUI_SetAccessPolicy_Left, SubtreeOpsUI_SetAccessPolicy_Right} from "./Dialogs/SubtreeOpsUI_SetAccessPolicy.js";
import {ConvertLocalSubtreeDataToServerStructure, GetServerSubtreeData_GQLQuery, PopulateLocalSubtreeData, SubtreeData_Server, SubtreeIncludeKeys} from "./Dialogs/SubtreeOpsHelpers.js";
import {ConvertLocalSubtreeDataToServerStructure, GetServerSubtreeData_GQLQuery, PopulateLocalSubtreeData, SubtreeData_Server} from "./Dialogs/SubtreeOpsHelpers.js";
import {SubtreeIncludeKeys, SubtreeOperation} from "./Dialogs/SubtreeOpsStructs.js";

@Observer
export class MI_SubtreeOps extends BaseComponentPlus({} as MI_SharedProps, {}) {
Expand All @@ -35,11 +36,6 @@ export class MI_SubtreeOps extends BaseComponentPlus({} as MI_SharedProps, {}) {
}
}

export enum SubtreeOperation {
export = "export",
setAccessPolicy = "setAccessPolicy",
}

const splitAt = 140;

@Observer
Expand Down
Loading

0 comments on commit 5477a70

Please sign in to comment.