github.com/minio/console@v1.4.1/web-app/src/screens/Console/CommandBar.tsx (about)

     1  //  This file is part of MinIO Console Server
     2  //  Copyright (c) 2022 MinIO, Inc.
     3  //
     4  //  This program is free software: you can redistribute it and/or modify
     5  //  it under the terms of the GNU Affero General Public License as published by
     6  //  the Free Software Foundation, either version 3 of the License, or
     7  //  (at your option) any later version.
     8  //
     9  //  This program is distributed in the hope that it will be useful,
    10  //  but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  //  GNU Affero General Public License for more details.
    13  //
    14  //  You should have received a copy of the GNU Affero General Public License
    15  //  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16  import * as React from "react";
    17  import { useCallback, useEffect, useState } from "react";
    18  import { useNavigate } from "react-router-dom";
    19  import {
    20    ActionId,
    21    ActionImpl,
    22    KBarAnimator,
    23    KBarPortal,
    24    KBarPositioner,
    25    KBarResults,
    26    KBarSearch,
    27    KBarState,
    28    useKBar,
    29    useMatches,
    30    useRegisterActions,
    31  } from "kbar";
    32  import { Action } from "kbar/lib/types";
    33  import { routesAsKbarActions } from "./kbar-actions";
    34  
    35  import { Box, MenuExpandedIcon } from "mds";
    36  import { useSelector } from "react-redux";
    37  import { selFeatures } from "./consoleSlice";
    38  import { Bucket } from "../../api/consoleApi";
    39  import { api } from "../../api";
    40  
    41  const searchStyle = {
    42    padding: "12px 16px",
    43    width: "100%",
    44    boxSizing: "border-box" as React.CSSProperties["boxSizing"],
    45    outline: "none",
    46    border: "none",
    47    color: "#858585",
    48    boxShadow: "0px 3px 5px #00000017",
    49    borderRadius: "4px 4px 0px 0px",
    50    fontSize: "14px",
    51    backgroundImage: "url(/images/search-icn.svg)",
    52    backgroundRepeat: "no-repeat",
    53    backgroundPosition: "95%",
    54  };
    55  
    56  const animatorStyle = {
    57    maxWidth: "600px",
    58    width: "100%",
    59    background: "white",
    60    color: "black",
    61    borderRadius: "4px",
    62    overflow: "hidden",
    63    boxShadow: "0px 3px 20px #00000055",
    64  };
    65  
    66  const groupNameStyle = {
    67    marginLeft: "30px",
    68    padding: "19px 0px 14px 0px",
    69    fontSize: "10px",
    70    textTransform: "uppercase" as const,
    71    color: "#858585",
    72    borderBottom: "1px solid #eaeaea",
    73  };
    74  
    75  const KBarStateChangeMonitor = ({
    76    onShow,
    77    onHide,
    78  }: {
    79    onShow?: () => void;
    80    onHide?: () => void;
    81  }) => {
    82    const [isOpen, setIsOpen] = useState(false);
    83    const { visualState } = useKBar((state: KBarState) => {
    84      return {
    85        visualState: state.visualState,
    86      };
    87    });
    88  
    89    useEffect(() => {
    90      if (visualState === "showing") {
    91        setIsOpen(true);
    92      } else {
    93        setIsOpen(false);
    94      }
    95      // eslint-disable-next-line react-hooks/exhaustive-deps
    96    }, [visualState]);
    97  
    98    useEffect(() => {
    99      if (isOpen) {
   100        onShow?.();
   101      } else {
   102        onHide?.();
   103      }
   104      // eslint-disable-next-line react-hooks/exhaustive-deps
   105    }, [isOpen]);
   106  
   107    //just to hook into the internal state of KBar. !
   108    return null;
   109  };
   110  
   111  const CommandBar = () => {
   112    const features = useSelector(selFeatures);
   113    const navigate = useNavigate();
   114  
   115    const [buckets, setBuckets] = useState<Bucket[]>([]);
   116  
   117    const invokeListBucketsApi = () => {
   118      api.buckets.listBuckets().then((res) => {
   119        if (res.data !== undefined) {
   120          setBuckets(res.data.buckets || []);
   121        }
   122      });
   123    };
   124  
   125    const fetchBuckets = useCallback(() => {
   126      invokeListBucketsApi();
   127      // eslint-disable-next-line react-hooks/exhaustive-deps
   128    }, []);
   129  
   130    const initialActions: Action[] = routesAsKbarActions(
   131      buckets,
   132      navigate,
   133      features,
   134    );
   135  
   136    useRegisterActions(initialActions, [buckets, features]);
   137  
   138    //fetch buckets everytime the kbar is shown so that new buckets created elsewhere , within first page is also shown
   139  
   140    return (
   141      <KBarPortal>
   142        <KBarStateChangeMonitor
   143          onShow={fetchBuckets}
   144          onHide={() => {
   145            setBuckets([]);
   146          }}
   147        />
   148        <KBarPositioner
   149          style={{
   150            zIndex: 9999,
   151            boxShadow: "0px 3px 20px #00000055",
   152            borderRadius: "4px",
   153          }}
   154        >
   155          <KBarAnimator style={animatorStyle}>
   156            <KBarSearch style={searchStyle} />
   157            <RenderResults />
   158          </KBarAnimator>
   159        </KBarPositioner>
   160      </KBarPortal>
   161    );
   162  };
   163  
   164  function RenderResults() {
   165    const { results, rootActionId } = useMatches();
   166  
   167    return (
   168      <KBarResults
   169        items={results}
   170        onRender={({ item, active }) =>
   171          typeof item === "string" ? (
   172            <Box style={groupNameStyle}>{item}</Box>
   173          ) : (
   174            <ResultItem
   175              action={item}
   176              active={active}
   177              currentRootActionId={`${rootActionId}`}
   178            />
   179          )
   180        }
   181      />
   182    );
   183  }
   184  
   185  const ResultItem = React.forwardRef(
   186    (
   187      {
   188        action,
   189        active,
   190        currentRootActionId,
   191      }: {
   192        action: ActionImpl;
   193        active: boolean;
   194        currentRootActionId: ActionId;
   195      },
   196      ref: React.Ref<HTMLDivElement>,
   197    ) => {
   198      const ancestors = React.useMemo(() => {
   199        if (!currentRootActionId) return action.ancestors;
   200        const index = action.ancestors.findIndex(
   201          (ancestor) => ancestor.id === currentRootActionId,
   202        );
   203        // +1 removes the currentRootAction; e.g.
   204        // if we are on the "Set theme" parent action,
   205        // the UI should not display "Set theme… > Dark"
   206        // but rather just "Dark"
   207        return action.ancestors.slice(index + 1);
   208      }, [action.ancestors, currentRootActionId]);
   209  
   210      return (
   211        <div
   212          ref={ref}
   213          style={{
   214            padding: "12px 12px 12px 36px",
   215            marginTop: "2px",
   216            background: active ? "#dddddd" : "transparent",
   217            display: "flex",
   218            alignItems: "center",
   219            justifyContent: "space-between",
   220            cursor: "pointer",
   221          }}
   222        >
   223          <Box
   224            sx={{
   225              display: "flex",
   226              gap: "8px",
   227              alignItems: "center",
   228              fontSize: 14,
   229              flex: 1,
   230              justifyContent: "space-between",
   231              "& .min-icon": {
   232                width: "17px",
   233                height: "17px",
   234              },
   235            }}
   236          >
   237            <Box sx={{ height: "15px", width: "15px", marginRight: "36px" }}>
   238              {action.icon && action.icon}
   239            </Box>
   240            <div style={{ display: "flex", flexDirection: "column", flex: 2 }}>
   241              <Box>
   242                {ancestors.length > 0 &&
   243                  ancestors.map((ancestor) => (
   244                    <React.Fragment key={ancestor.id}>
   245                      <span
   246                        style={{
   247                          opacity: 0.5,
   248                          marginRight: 8,
   249                        }}
   250                      >
   251                        {ancestor.name}
   252                      </span>
   253                      <span
   254                        style={{
   255                          marginRight: 8,
   256                        }}
   257                      >
   258                        &rsaquo;
   259                      </span>
   260                    </React.Fragment>
   261                  ))}
   262                <span>{action.name}</span>
   263              </Box>
   264              {action.subtitle && (
   265                <span
   266                  style={{
   267                    fontSize: 12,
   268                  }}
   269                >
   270                  {action.subtitle}
   271                </span>
   272              )}
   273            </div>
   274            <Box
   275              sx={{
   276                "& .min-icon": {
   277                  width: "15px",
   278                  height: "15px",
   279                  fill: "#8f8b8b",
   280                  transform: "rotate(90deg)",
   281  
   282                  "& rect": {
   283                    fill: "#ffffff",
   284                  },
   285                },
   286              }}
   287            >
   288              <MenuExpandedIcon />
   289            </Box>
   290          </Box>
   291          {action.shortcut?.length ? (
   292            <div
   293              aria-hidden
   294              style={{ display: "grid", gridAutoFlow: "column", gap: "4px" }}
   295            >
   296              {action.shortcut.map((sc) => (
   297                <kbd
   298                  key={sc}
   299                  style={{
   300                    padding: "4px 6px",
   301                    background: "rgba(0 0 0 / .1)",
   302                    borderRadius: "4px",
   303                    fontSize: 14,
   304                  }}
   305                >
   306                  {sc}
   307                </kbd>
   308              ))}
   309            </div>
   310          ) : null}
   311        </div>
   312      );
   313    },
   314  );
   315  
   316  export default CommandBar;