sadpandajoe commented on code in PR #45090:
URL: https://github.com/apache/superset/pull/45090#discussion_r4230831042


##########
superset-frontend/src/features/canvas/useCanvasLayout.ts:
##########
@@ -0,0 +1,187 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { getClientErrorObject, SupersetClient } from '@superset-ui/core';
+import { t } from '@apache-superset/core/translation';
+import type {
+  ApplyOperationsResult,
+  CanvasDefinitionResult,
+  GridPlacement,
+} from './types';
+
+/** The newest layout this client knows of, and the revision it belongs to. */
+interface AppliedLayout {
+  revision: number;
+  placements: Record<string, GridPlacement>;
+}
+
+export interface CanvasLayout {
+  /** Placements to render: the newest local ones, else the server's. */
+  placements: Record<string, GridPlacement>;
+  /** Persist a node's new placement. A no-op when the user can't edit. */
+  place: (nodeId: string, placement: GridPlacement) => void;
+  /** Why the last write failed, for a dismissable notice. */
+  error?: string;
+  dismissError: () => void;
+}
+
+/**
+ * Persists drag and resize as `place` operations.
+ *
+ * The moved widget appears where it was dropped straight away, then takes the
+ * placements the server resolved — which may differ, since the server pushes
+ * overlapping widgets down.
+ *
+ * Each write is based on the newest revision the server has *confirmed*, taken
+ * from the previous write's response rather than from the definition. The
+ * definition's revision only advances when a refetch lands, and that refetch
+ * is driven by a realtime nudge; without one (no realtime service, a dropped
+ * socket) it would stay put and every drag after the first would collide with
+ * the user's own previous drag.
+ *
+ * A write while another is still in flight is queued rather than sent with a
+ * revision the server hasn't reached yet, so dragging quickly coalesces into
+ * one follow-up request instead of conflicting.
+ */
+export function useCanvasLayout(
+  canvasId: number,
+  result: CanvasDefinitionResult,
+  reload: () => void,
+): CanvasLayout {
+  const [applied, setApplied] = useState<AppliedLayout>();
+  const [error, setError] = useState<string>();
+  const { revision, canEdit } = result;
+
+  // Last revision the server acknowledged. Never an optimistic guess, so a
+  // write is never based on a revision the server hasn't reached.
+  const confirmed = useRef<number>();
+  // The definition's own revision, for the write that hasn't had a response
+  // yet (first drag after a load or a refetch).
+  const fetched = useRef(revision);
+  const writing = useRef(false);
+  // Newest intent per node while a write is in flight; sent as one batch.
+  const queued = useRef(new Map<string, GridPlacement>());
+  // The definition's placements, for an optimistic view that has no newer
+  // local layout to build on.
+  const fetchedPlacements = useRef(result.placements);
+
+  useEffect(() => {
+    fetched.current = revision;
+    fetchedPlacements.current = result.placements;
+  }, [revision, result.placements]);
+
+  useEffect(() => {
+    // A different canvas shares none of this state.
+    confirmed.current = undefined;
+    writing.current = false;
+    queued.current.clear();
+    setApplied(undefined);
+    setError(undefined);
+  }, [canvasId]);
+
+  const send = useCallback(
+    (ops: Map<string, GridPlacement>) => {
+      writing.current = true;
+      const base = Math.max(fetched.current, confirmed.current ?? 0);
+      SupersetClient.request({
+        endpoint: `/api/v1/canvas/${canvasId}/definition`,
+        method: 'PATCH',
+        jsonPayload: {
+          base_revision: base,
+          ops: [...ops].map(([id, layout]) => ({ op: 'place', id, layout })),
+        },
+      })
+        .then(({ json }) => {
+          const written = json?.result as ApplyOperationsResult | undefined;
+          if (written) {
+            confirmed.current = written.revision;
+            setApplied({
+              revision: written.revision,
+              placements: written.placements,
+            });
+          }
+        })
+        .catch(async response => {
+          queued.current.clear();
+          setApplied(undefined);

Review Comment:
   If a second write fails after an earlier one was acknowledged (say a drag 
saved at revision 4, then a resize gets a 500 or 422 before any refetch lands), 
`setApplied(undefined)` drops the view back to the definition's revision-3 
placements while `confirmed.current` is still 4. The first drag then visually 
snaps back although the server kept it, and a later gesture on that widget 
starts from the stale geometry and is sent with `base_revision: 4`, so the 
saved position is silently overwritten with no conflict. Should the rollback 
keep the last acknowledged placements, or call `reload()` for every failure 
rather than only 409? The failure test in `useCanvasLayout.test.ts` only 
rejects the first write, so it wouldn't catch this; a case with one successful 
PATCH followed by a rejected one, asserting the first move is still shown, 
would.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to