You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@superset.apache.org by "mikebridge (via GitHub)" <gi...@apache.org> on 2026/10/22 19:32:15 UTC

Re: [PR] fix(extensions): keep CSRF protection on the extensions APIs, fix their OpenAPI docs, log startup loading, and explain a missing API on the Extensions page [superset]

mikebridge commented on code in PR #45550:
URL: https://github.com/apache/superset/pull/45550#discussion_r4352652096


##########
superset/initialization/__init__.py:
##########
@@ -331,12 +332,11 @@ def init_views(self) -> None:
         appbuilder.add_api(SubjectRestApi)
         appbuilder.add_api(LogRestApi)
 
-        if feature_flag_manager.is_feature_enabled("ENABLE_EXTENSIONS"):

Review Comment:
   Thanks for the pointer, that discussion is exactly the question here. Rather than pre-empt it, this revision drops the per-request gating: the extensions APIs are registered at startup only when the flag is on, as on master, and there is no blueprint guard or load-state tracking any more. What is left is the CSRF fix, the OpenAPI docstring fixes, the startup logging, and a clearer message on the Extensions page when the API was never registered. If per-request evaluation of this flag is taken up at a town hall, happy to pick the gating back up on top of whatever is decided.



##########
superset-frontend/src/extensions/ExtensionsList.tsx:
##########
@@ -40,15 +61,56 @@ const ExtensionsList: FunctionComponent<ExtensionsListProps> = ({
   addDangerToast,
   addSuccessToast,
 }) => {
-  const {
-    state: { loading, resourceCount, resourceCollection },
-    fetchData,
-    refreshData,
-  } = useListViewResource<Extension>(
-    'extensions',
-    t('Extensions'),
-    addDangerToast,
-  );
+  // The rows and the load status come from the same list response, so the
+  // restart notice always describes the list on screen, even when requests
+  // reach different servers during a rolling restart.
+  const [{ loading, collection, count, notLoaded }, setState] =
+    useState<ExtensionsListState>({
+      loading: true,
+      collection: [],
+      count: 0,
+      notLoaded: false,
+    });
+  const addDangerToastRef = useRef(addDangerToast);
+  addDangerToastRef.current = addDangerToast;
+  const latestRequestIdRef = useRef(0);
+
+  // The endpoint returns every servable extension in one page and ignores
+  // paging and sorting parameters.
+  const fetchData = useCallback(() => {
+    const requestId = latestRequestIdRef.current + 1;
+    latestRequestIdRef.current = requestId;
+    const isLatest = () => latestRequestIdRef.current === requestId;

Review Comment:
   Thanks, that was a real gap. It has become moot in the latest revision: the page is back on the shared `useListViewResource` hook, so the stale-response handling is the hook's own again and the custom guard and its race test are gone. The only addition on the page now is a single `_info` request before the list mounts, to tell a 404 (API not registered at startup) apart from other failures.



##########
superset/semantic_layers/api.py:
##########
@@ -706,14 +710,40 @@ def types(self) -> FlaskResponse:
           responses:
             200:
               description: A list of semantic layer types
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      result:
+                        type: array
+                        items:
+                          type: object
+                          properties:
+                            id:
+                              type: string
+                            name:
+                              type: string
+                            description:
+                              type: string

Review Comment:
   Agreed, and it would have been `nullable: true`, but the `/types` response schema was only added by this PR. The latest revision no longer touches `/semantic_layer/types` at all (the `extensions_status` field and the per-request type filtering were dropped along with the gating), so that schema is back to master, where it isn't documented. The nullable `description` is worth fixing whenever that schema is added; it isn't part of this PR any more.



##########
docs/static/resources/openapi.json:
##########
@@ -26581,6 +26581,870 @@
         ]
       }
     },
+    "/api/v1/extensions/": {
+      "get": {
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "properties": {
+                    "count": {
+                      "type": "integer"
+                    },
+                    "message": {
+                      "type": "string"
+                    },
+                    "result": {
+                      "items": {
+                        "properties": {
+                          "remoteEntry": {
+                            "type": "string"
+                          }
+                        },
+                        "type": "object"
+                      },
+                      "type": "array"
+                    },
+                    "status": {
+                      "enum": [
+                        "loaded",
+                        "not_loaded"
+                      ],
+                      "type": "string"
+                    }
+                  },
+                  "type": "object"
+                }
+              }
+            },
+            "description": "List of all enabled extensions"
+          },
+          "400": {
+            "$ref": "#/components/responses/400"
+          },
+          "401": {
+            "$ref": "#/components/responses/401"
+          },
+          "422": {
+            "$ref": "#/components/responses/422"
+          },
+          "500": {
+            "$ref": "#/components/responses/500"
+          }
+        },
+        "security": [
+          {
+            "jwt": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "summary": "List all enabled extensions.",
+        "tags": [
+          "ExtensionsRestApi"
+        ]
+      }
+    },
+    "/api/v1/extensions/_info": {
+      "get": {
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "properties": {
+                    "message": {
+                      "type": "string"
+                    },
+                    "permissions": {
+                      "items": {
+                        "type": "string"
+                      },
+                      "type": "array"
+                    },
+                    "status": {
+                      "description": "`not_loaded` when extensions are enabled for this request but were not loaded at server startup",
+                      "enum": [
+                        "loaded",
+                        "not_loaded"
+                      ],
+                      "type": "string"
+                    }
+                  },
+                  "type": "object"
+                }
+              }
+            },
+            "description": "API info"
+          }
+        },
+        "security": [
+          {
+            "jwt": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "summary": "Get API info",
+        "tags": [
+          "ExtensionsRestApi"
+        ]
+      }
+    },
+    "/api/v1/extensions/{publisher}/{name}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "publisher",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "in": "path",
+            "name": "name",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "properties": {
+                    "result": {
+                      "items": {
+                        "properties": {
+                          "remoteEntry": {
+                            "type": "string"
+                          }
+                        },
+                        "type": "object"
+                      },
+                      "type": "array"
+                    }

Review Comment:
   Fair point: that schema came from the detail endpoint's docstring, which declared `result` as an array while the route returns one extension object. The docstring now describes a single object (the list endpoint keeps the array), and a test checks both shapes in the generated spec.



##########
docs/static/resources/openapi.json:
##########
@@ -26581,6 +26581,870 @@
         ]
       }
     },
+    "/api/v1/extensions/": {
+      "get": {
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "properties": {
+                    "count": {
+                      "type": "integer"
+                    },
+                    "message": {
+                      "type": "string"
+                    },
+                    "result": {
+                      "items": {
+                        "properties": {
+                          "remoteEntry": {
+                            "type": "string"
+                          }
+                        },
+                        "type": "object"
+                      },
+                      "type": "array"
+                    },
+                    "status": {
+                      "enum": [
+                        "loaded",
+                        "not_loaded"
+                      ],
+                      "type": "string"
+                    }
+                  },
+                  "type": "object"
+                }
+              }
+            },
+            "description": "List of all enabled extensions"
+          },
+          "400": {
+            "$ref": "#/components/responses/400"
+          },
+          "401": {
+            "$ref": "#/components/responses/401"
+          },
+          "422": {
+            "$ref": "#/components/responses/422"
+          },
+          "500": {
+            "$ref": "#/components/responses/500"
+          }
+        },
+        "security": [
+          {
+            "jwt": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "summary": "List all enabled extensions.",
+        "tags": [
+          "ExtensionsRestApi"
+        ]
+      }
+    },
+    "/api/v1/extensions/_info": {
+      "get": {
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "properties": {
+                    "message": {
+                      "type": "string"
+                    },
+                    "permissions": {
+                      "items": {
+                        "type": "string"
+                      },
+                      "type": "array"
+                    },
+                    "status": {
+                      "description": "`not_loaded` when extensions are enabled for this request but were not loaded at server startup",
+                      "enum": [
+                        "loaded",
+                        "not_loaded"
+                      ],
+                      "type": "string"
+                    }
+                  },
+                  "type": "object"
+                }
+              }
+            },
+            "description": "API info"
+          }
+        },
+        "security": [
+          {
+            "jwt": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "summary": "Get API info",
+        "tags": [
+          "ExtensionsRestApi"
+        ]
+      }
+    },
+    "/api/v1/extensions/{publisher}/{name}": {
+      "get": {
+        "parameters": [
+          {
+            "in": "path",
+            "name": "publisher",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "in": "path",
+            "name": "name",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "properties": {
+                    "result": {
+                      "items": {
+                        "properties": {
+                          "remoteEntry": {
+                            "type": "string"
+                          }
+                        },
+                        "type": "object"
+                      },
+                      "type": "array"
+                    }
+                  },
+                  "type": "object"
+                }
+              }
+            },
+            "description": "Extension details"
+          },
+          "400": {
+            "$ref": "#/components/responses/400"
+          },
+          "401": {
+            "$ref": "#/components/responses/401"
+          },
+          "422": {
+            "$ref": "#/components/responses/422"
+          },
+          "500": {
+            "$ref": "#/components/responses/500"
+          }
+        },
+        "security": [
+          {
+            "jwt": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "summary": "Get an extension by its publisher and name.",
+        "tags": [
+          "ExtensionsRestApi"
+        ]
+      }
+    },
+    "/api/v1/extensions/{publisher}/{name}/storage/ephemeral/{key}": {
+      "delete": {
+        "parameters": [
+          {
+            "description": "Extension publisher",
+            "in": "path",
+            "name": "publisher",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "Extension name",
+            "in": "path",
+            "name": "name",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "Storage key",
+            "in": "path",
+            "name": "key",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "If true, delete from shared state",
+            "in": "query",
+            "name": "shared",
+            "required": false,
+            "schema": {
+              "type": "boolean"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Value deleted successfully"
+          },
+          "404": {
+            "description": "Extension not found"
+          }
+        },
+        "security": [
+          {
+            "jwt": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "summary": "Delete a value from ephemeral state",
+        "tags": [
+          "ExtensionStorageRestApi"
+        ]
+      },
+      "get": {
+        "parameters": [
+          {
+            "description": "Extension publisher",
+            "in": "path",
+            "name": "publisher",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "Extension name",
+            "in": "path",
+            "name": "name",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "Storage key",
+            "in": "path",
+            "name": "key",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "If true, read from shared state visible to all users",
+            "in": "query",
+            "name": "shared",
+            "required": false,
+            "schema": {
+              "type": "boolean"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "properties": {
+                    "codec": {
+                      "description": "Name of the codec 'result' was encoded with, e.g. `json` (default) or `binary`",
+                      "type": "string"
+                    },
+                    "isBinary": {
+                      "description": "Whether the stored value is binary data",
+                      "type": "boolean"
+                    },
+                    "result": {
+                      "description": "The stored value. When 'isBinary' is true, this is a base64 string that must be decoded to get the actual value."
+                    }
+                  },
+                  "type": "object"
+                }
+              }
+            },
+            "description": "Value retrieved successfully"
+          },
+          "400": {
+            "description": "Value was stored with a codec unavailable over the API"
+          },
+          "404": {
+            "description": "Extension not found"
+          }
+        },
+        "security": [
+          {
+            "jwt": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "summary": "Get a value from ephemeral state",
+        "tags": [
+          "ExtensionStorageRestApi"
+        ]
+      },
+      "put": {
+        "parameters": [
+          {
+            "description": "Extension publisher",
+            "in": "path",
+            "name": "publisher",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "Extension name",
+            "in": "path",
+            "name": "name",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "Storage key",
+            "in": "path",
+            "name": "key",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "If true, store as shared state visible to all users",
+            "in": "query",
+            "name": "shared",
+            "required": false,
+            "schema": {
+              "type": "boolean"
+            }
+          }
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "properties": {
+                  "codec": {
+                    "description": "Name of the codec used to encode 'value', e.g. `json` (default). Must be one of the codecs allowed over the REST API.",
+                    "type": "string"
+                  },
+                  "isBinary": {
+                    "description": "Whether 'value' is binary data",
+                    "type": "boolean"
+                  },
+                  "ttl": {
+                    "description": "Time-to-live in seconds (must be a positive integer not exceeding MAX_TTL)",
+                    "type": "integer"
+                  },
+                  "value": {
+                    "description": "The value to store (must not exceed MAX_VALUE_SIZE bytes once encoded with 'codec'). When 'isBinary' is true, this must be a base64 string, decoded before being handed to 'codec'."
+                  }
+                },
+                "required": [
+                  "value",
+                  "ttl"
+                ],
+                "type": "object"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "Value stored successfully"
+          },
+          "400": {
+            "description": "Invalid request body, or codec not allowed over the API"
+          },
+          "404": {
+            "description": "Extension not found"
+          }
+        },
+        "security": [
+          {
+            "jwt": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "summary": "Set a value in ephemeral state",
+        "tags": [
+          "ExtensionStorageRestApi"
+        ]
+      }
+    },
+    "/api/v1/extensions/{publisher}/{name}/storage/persistent": {
+      "get": {
+        "parameters": [
+          {
+            "description": "Extension publisher",
+            "in": "path",
+            "name": "publisher",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "Extension name",
+            "in": "path",
+            "name": "name",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "If true, list shared state visible to all users",
+            "in": "query",
+            "name": "shared",
+            "required": false,
+            "schema": {
+              "type": "boolean"
+            }
+          },
+          {
+            "description": "Filter by resource type",
+            "in": "query",
+            "name": "resource_type",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "Filter by resource UUID (requires resource_type)",
+            "in": "query",
+            "name": "resource_uuid",
+            "required": false,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "Zero-indexed page number. Defaults to 0.",
+            "in": "query",
+            "name": "page",
+            "required": false,
+            "schema": {
+              "type": "integer"
+            }
+          },
+          {
+            "description": "Number of entries per page. Defaults to 10. There is no fixed ceiling, but a page whose combined value size exceeds MAX_LIST_PAYLOAD_SIZE is rejected \u2014 reduce page_size and retry if that happens.",
+            "in": "query",
+            "name": "page_size",
+            "required": false,
+            "schema": {
+              "type": "integer"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "properties": {
+                    "count": {
+                      "description": "Total number of entries matching the given scope/filters, across all pages",
+                      "type": "integer"
+                    },
+                    "result": {
+                      "items": {
+                        "properties": {
+                          "codec": {
+                            "type": "string"
+                          },
+                          "isBinary": {
+                            "description": "Whether the stored value is binary data",
+                            "type": "boolean"
+                          },
+                          "key": {
+                            "type": "string"
+                          },
+                          "value": {
+                            "description": "The stored value, or null if its codec cannot be read over the REST API. When 'isBinary' is true, this is a base64 string that must be decoded to get the actual value."
+                          }
+                        },
+                        "type": "object"
+                      },
+                      "type": "array"
+                    }
+                  },
+                  "type": "object"
+                }
+              }
+            },
+            "description": "Entries retrieved successfully"
+          },
+          "400": {
+            "description": "The requested page's combined value size exceeds MAX_LIST_PAYLOAD_SIZE"
+          },
+          "404": {
+            "description": "Extension not found"
+          }
+        },
+        "security": [
+          {
+            "jwt": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "summary": "List entries in persistent state",
+        "tags": [
+          "ExtensionStorageRestApi"
+        ]
+      }
+    },
+    "/api/v1/extensions/{publisher}/{name}/storage/persistent/{key}": {
+      "delete": {
+        "parameters": [
+          {
+            "description": "Extension publisher",
+            "in": "path",
+            "name": "publisher",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "Extension name",
+            "in": "path",
+            "name": "name",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "Storage key",
+            "in": "path",
+            "name": "key",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "If true, delete from shared state",
+            "in": "query",
+            "name": "shared",
+            "required": false,
+            "schema": {
+              "type": "boolean"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Value deleted successfully"
+          },
+          "404": {
+            "description": "Extension not found"
+          }
+        },
+        "security": [
+          {
+            "jwt": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "summary": "Delete a value from persistent state",
+        "tags": [
+          "ExtensionStorageRestApi"
+        ]
+      },
+      "get": {
+        "parameters": [
+          {
+            "description": "Extension publisher",
+            "in": "path",
+            "name": "publisher",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "Extension name",
+            "in": "path",
+            "name": "name",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "Storage key",
+            "in": "path",
+            "name": "key",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "If true, read from shared state visible to all users",
+            "in": "query",
+            "name": "shared",
+            "required": false,
+            "schema": {
+              "type": "boolean"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "properties": {
+                    "codec": {
+                      "description": "Name of the codec 'result' was encoded with, e.g. `json` (default) or `binary`",
+                      "type": "string"
+                    },
+                    "isBinary": {
+                      "description": "Whether the stored value is binary data",
+                      "type": "boolean"
+                    },
+                    "result": {
+                      "description": "The stored value. When 'isBinary' is true, this is a base64 string that must be decoded to get the actual value."
+                    }
+                  },
+                  "type": "object"
+                }
+              }
+            },
+            "description": "Value retrieved successfully"
+          },
+          "400": {
+            "description": "Value was stored with a codec unavailable over the API"
+          },
+          "404": {
+            "description": "Extension not found"
+          }
+        },
+        "security": [
+          {
+            "jwt": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "summary": "Get a value from persistent state",
+        "tags": [
+          "ExtensionStorageRestApi"
+        ]
+      },
+      "put": {
+        "parameters": [
+          {
+            "description": "Extension publisher",
+            "in": "path",
+            "name": "publisher",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "Extension name",
+            "in": "path",
+            "name": "name",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "Storage key",
+            "in": "path",
+            "name": "key",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "If true, store as shared state visible to all users",
+            "in": "query",
+            "name": "shared",
+            "required": false,
+            "schema": {
+              "type": "boolean"
+            }
+          }
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "properties": {
+                  "codec": {
+                    "description": "Name of the codec used to encode 'value', e.g. `json` (default). Must be one of the codecs allowed over the REST API.",
+                    "type": "string"
+                  },
+                  "encrypt": {
+                    "description": "If true, the value is encrypted at rest",
+                    "type": "boolean"
+                  },
+                  "isBinary": {
+                    "description": "Whether 'value' is binary data",
+                    "type": "boolean"
+                  },
+                  "value": {
+                    "description": "The value to store. When 'isBinary' is true, this must be a base64 string, decoded before being handed to 'codec'."
+                  }
+                },
+                "required": [
+                  "value"
+                ],
+                "type": "object"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "Value stored successfully"
+          },
+          "400": {
+            "description": "Invalid request body, codec not allowed over the API, or value exceeds MAX_VALUE_SIZE"
+          },
+          "404": {
+            "description": "Extension not found"
+          },
+          "413": {
+            "description": "Extension persistent storage quota exceeded"
+          }
+        },
+        "security": [
+          {
+            "jwt": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "summary": "Set a value in persistent state",
+        "tags": [
+          "ExtensionStorageRestApi"
+        ]
+      }
+    },
+    "/api/v1/extensions/{publisher}/{name}/{file}": {
+      "get": {
+        "parameters": [
+          {
+            "description": "publisher of the extension",
+            "in": "path",
+            "name": "publisher",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "technical name of the extension",
+            "in": "path",
+            "name": "name",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "description": "name of the requested chunk",
+            "in": "path",
+            "name": "file",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "properties": {
+                    "message": {
+                      "type": "string"
+                    }
+                  },
+                  "type": "object"
+                }
+              }

Review Comment:
   Agreed. The route serves the file's bytes through `send_file` with a media type guessed from the file name, so the docstring now documents the response as binary content (`application/octet-stream`, `format: binary`) instead of a JSON message object.



##########
superset-frontend/src/extensions/ExtensionsList.tsx:
##########
@@ -40,15 +61,56 @@ const ExtensionsList: FunctionComponent<ExtensionsListProps> = ({
   addDangerToast,
   addSuccessToast,
 }) => {
-  const {
-    state: { loading, resourceCount, resourceCollection },
-    fetchData,
-    refreshData,
-  } = useListViewResource<Extension>(
-    'extensions',
-    t('Extensions'),
-    addDangerToast,
-  );
+  // The rows and the load status come from the same list response, so the
+  // restart notice always describes the list on screen, even when requests
+  // reach different servers during a rolling restart.
+  const [{ loading, collection, count, notLoaded }, setState] =
+    useState<ExtensionsListState>({
+      loading: true,
+      collection: [],
+      count: 0,
+      notLoaded: false,
+    });
+  const addDangerToastRef = useRef(addDangerToast);
+  addDangerToastRef.current = addDangerToast;
+  const latestRequestIdRef = useRef(0);
+
+  // The endpoint returns every servable extension in one page and ignores
+  // paging and sorting parameters.
+  const fetchData = useCallback(() => {
+    const requestId = latestRequestIdRef.current + 1;
+    latestRequestIdRef.current = requestId;
+    const isLatest = () => latestRequestIdRef.current === requestId;
+    setState(current => ({ ...current, loading: true }));
+    return SupersetClient.get({ endpoint: '/api/v1/extensions/' })

Review Comment:
   This no longer applies: the page-local fetch was removed in the latest revision, and the page is back on the shared `useListViewResource` hook, which passes the sort parameters as before.



-- 
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: notifications-unsubscribe@superset.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org