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


##########
superset-frontend/src/features/alerts/hooks/useSlackChannels.ts:
##########
@@ -0,0 +1,191 @@
+/**
+ * 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, useRef, useState } from 'react';
+import { logging, SupersetClient, t } from '@superset-ui/core';
+import rison from 'rison';
+import { SlackChannel } from '../types';
+
+export interface SlackChannelOption {
+  label: string;
+  value: string;
+}
+
+export interface SlackChannelsResult {
+  data: SlackChannelOption[];
+  totalCount: number;
+  has_more?: boolean;
+  next_cursor?: string | null;
+}
+
+export interface FetchChannelsParams {
+  search: string;
+  page: number;
+  pageSize: number;
+  force?: boolean;
+}
+
+export interface UseSlackChannelsResult {
+  fetchChannels: (params: FetchChannelsParams) => Promise<SlackChannelsResult>;
+  refreshChannels: () => Promise<void>;
+  isRefreshing: boolean;
+}
+
+/**
+ * Cache types for managing Slack channel data
+ */
+type CursorCache = Record<string, string | null>;
+type DataCache = Record<string, SlackChannelsResult>;
+type PendingRequestsCache = Record<string, Promise<SlackChannelsResult>>;
+
+/**
+ * Custom hook for managing Slack channels with caching and pagination
+ */
+export function useSlackChannels(
+  onError?: (message: string) => void,
+): UseSlackChannelsResult {
+  const cursorRef = useRef<CursorCache>({});
+  const dataCache = useRef<DataCache>({});
+  const pendingRequests = useRef<PendingRequestsCache>({});
+  const [isRefreshing, setIsRefreshing] = useState(false);
+
+  const fetchChannels = useCallback(
+    async ({
+      search,
+      page,
+      pageSize,
+      force = false,
+    }: FetchChannelsParams): Promise<SlackChannelsResult> => {
+      const cacheKey = `${search}:${page}`;
+
+      if (!force && dataCache.current[cacheKey]) {
+        return dataCache.current[cacheKey];
+      }
+
+      if (!force && cacheKey in pendingRequests.current) {
+        return pendingRequests.current[cacheKey];
+      }
+
+      const cursor = page > 0 ? cursorRef.current[cacheKey] : null;
+
+      const params: Record<string, any> = {
+        types: ['public_channel', 'private_channel'],
+        limit: pageSize,
+      };
+
+      if (search) {
+        params.search_string = search;
+      }
+
+      if (cursor) {
+        params.cursor = cursor;
+      }
+
+      if (force) {
+        params.force = true;
+      }
+
+      const queryString = rison.encode(params);
+      const endpoint = `/api/v1/report/slack_channels/?q=${queryString}`;
+
+      const fetchPromise = (async () => {
+        try {
+          const response = await SupersetClient.get({ endpoint });
+
+          const {
+            result,
+            next_cursor: nextCursor,
+            has_more: hasMore,
+          } = response.json;
+
+          if (nextCursor) {
+            cursorRef.current[`${search}:${page + 1}`] = nextCursor;
+          }
+
+          const options = result.map((channel: SlackChannel) => ({
+            label: channel.name,
+            value: channel.id,
+          }));
+
+          const totalCount = hasMore
+            ? (page + 1) * pageSize + 1
+            : page * pageSize + options.length;
+
+          const responseData = {
+            data: options,
+            totalCount,
+            has_more: hasMore,
+            next_cursor: nextCursor,
+          };
+
+          dataCache.current[cacheKey] = responseData;
+
+          return responseData;
+        } catch (error) {
+          logging.error('Failed to fetch Slack channels:', error);
+
+          if (onError) {
+            onError(
+              t(
+                'Unable to load Slack channels. Please check your Slack API 
token configuration.',
+              ),
+            );
+          }
+
+          return {

Review Comment:
   Resolving failures as an empty successful result means 
`NotificationMethod`'s catch block never switches Slack V2 users to the 
advertised manual-input fallback; an invalid token or Slack outage leaves an 
empty picker instead. Should this reject after showing the hook-level error so 
the component can perform the fallback?



##########
superset-frontend/src/features/alerts/hooks/useSlackChannels.ts:
##########
@@ -0,0 +1,191 @@
+/**
+ * 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, useRef, useState } from 'react';
+import { logging, SupersetClient, t } from '@superset-ui/core';
+import rison from 'rison';
+import { SlackChannel } from '../types';
+
+export interface SlackChannelOption {
+  label: string;
+  value: string;
+}
+
+export interface SlackChannelsResult {
+  data: SlackChannelOption[];
+  totalCount: number;
+  has_more?: boolean;
+  next_cursor?: string | null;
+}
+
+export interface FetchChannelsParams {
+  search: string;
+  page: number;
+  pageSize: number;
+  force?: boolean;
+}
+
+export interface UseSlackChannelsResult {
+  fetchChannels: (params: FetchChannelsParams) => Promise<SlackChannelsResult>;
+  refreshChannels: () => Promise<void>;
+  isRefreshing: boolean;
+}
+
+/**
+ * Cache types for managing Slack channel data
+ */
+type CursorCache = Record<string, string | null>;
+type DataCache = Record<string, SlackChannelsResult>;
+type PendingRequestsCache = Record<string, Promise<SlackChannelsResult>>;
+
+/**
+ * Custom hook for managing Slack channels with caching and pagination
+ */
+export function useSlackChannels(
+  onError?: (message: string) => void,
+): UseSlackChannelsResult {
+  const cursorRef = useRef<CursorCache>({});
+  const dataCache = useRef<DataCache>({});
+  const pendingRequests = useRef<PendingRequestsCache>({});
+  const [isRefreshing, setIsRefreshing] = useState(false);
+
+  const fetchChannels = useCallback(
+    async ({
+      search,
+      page,
+      pageSize,
+      force = false,
+    }: FetchChannelsParams): Promise<SlackChannelsResult> => {
+      const cacheKey = `${search}:${page}`;
+
+      if (!force && dataCache.current[cacheKey]) {
+        return dataCache.current[cacheKey];
+      }
+
+      if (!force && cacheKey in pendingRequests.current) {
+        return pendingRequests.current[cacheKey];
+      }
+
+      const cursor = page > 0 ? cursorRef.current[cacheKey] : null;
+
+      const params: Record<string, any> = {
+        types: ['public_channel', 'private_channel'],
+        limit: pageSize,
+      };
+
+      if (search) {
+        params.search_string = search;
+      }
+
+      if (cursor) {
+        params.cursor = cursor;
+      }
+
+      if (force) {
+        params.force = true;
+      }
+
+      const queryString = rison.encode(params);
+      const endpoint = `/api/v1/report/slack_channels/?q=${queryString}`;
+
+      const fetchPromise = (async () => {
+        try {
+          const response = await SupersetClient.get({ endpoint });
+
+          const {
+            result,
+            next_cursor: nextCursor,
+            has_more: hasMore,
+          } = response.json;
+
+          if (nextCursor) {
+            cursorRef.current[`${search}:${page + 1}`] = nextCursor;
+          }
+
+          const options = result.map((channel: SlackChannel) => ({
+            label: channel.name,
+            value: channel.id,
+          }));
+
+          const totalCount = hasMore
+            ? (page + 1) * pageSize + 1
+            : page * pageSize + options.length;
+
+          const responseData = {
+            data: options,
+            totalCount,
+            has_more: hasMore,
+            next_cursor: nextCursor,
+          };
+
+          dataCache.current[cacheKey] = responseData;

Review Comment:
   An older request that resolves after `refreshChannels` can still write stale 
data into the newly reset cache and overwrite the forced refresh result. Should 
writes be generation-guarded or the pre-refresh requests be cancelled/ignored?



##########
superset/tasks/slack.py:
##########
@@ -37,7 +65,50 @@ def cache_channels() -> None:
     )
 
     try:
-        get_channels(force=True, cache_timeout=cache_timeout)
+        all_channels = []
+        cursor: Optional[str] = None
+        page_count = 0
+
+        while True:
+            page_count += 1
+
+            result = get_channels_with_search(

Review Comment:
   When this scheduled warm-up runs with an existing cache entry, 
`get_channels_with_search` reads that cache instead of Slack and the task 
writes the same stale list back with a fresh timeout, so removed/new channels 
may never converge. Should the warm-up bypass or clear the cache before 
collecting pages?



##########
superset-frontend/src/features/alerts/hooks/useSlackChannels.ts:
##########
@@ -0,0 +1,191 @@
+/**
+ * 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, useRef, useState } from 'react';
+import { logging, SupersetClient, t } from '@superset-ui/core';
+import rison from 'rison';
+import { SlackChannel } from '../types';
+
+export interface SlackChannelOption {
+  label: string;
+  value: string;
+}
+
+export interface SlackChannelsResult {
+  data: SlackChannelOption[];
+  totalCount: number;
+  has_more?: boolean;
+  next_cursor?: string | null;
+}
+
+export interface FetchChannelsParams {
+  search: string;
+  page: number;
+  pageSize: number;
+  force?: boolean;
+}
+
+export interface UseSlackChannelsResult {
+  fetchChannels: (params: FetchChannelsParams) => Promise<SlackChannelsResult>;
+  refreshChannels: () => Promise<void>;
+  isRefreshing: boolean;
+}
+
+/**
+ * Cache types for managing Slack channel data
+ */
+type CursorCache = Record<string, string | null>;
+type DataCache = Record<string, SlackChannelsResult>;
+type PendingRequestsCache = Record<string, Promise<SlackChannelsResult>>;
+
+/**
+ * Custom hook for managing Slack channels with caching and pagination
+ */
+export function useSlackChannels(
+  onError?: (message: string) => void,
+): UseSlackChannelsResult {
+  const cursorRef = useRef<CursorCache>({});
+  const dataCache = useRef<DataCache>({});
+  const pendingRequests = useRef<PendingRequestsCache>({});
+  const [isRefreshing, setIsRefreshing] = useState(false);
+
+  const fetchChannels = useCallback(
+    async ({
+      search,
+      page,
+      pageSize,
+      force = false,
+    }: FetchChannelsParams): Promise<SlackChannelsResult> => {
+      const cacheKey = `${search}:${page}`;

Review Comment:
   This cache key ignores `pageSize`, so a prior 100-item initialization 
request can satisfy the picker's later 999-item request and permanently expose 
only the smaller subset. Should `pageSize` be part of the data, 
pending-request, and cursor cache identity?



-- 
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