Fix Kbar in operator mode (#1638)

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
Daniel Valdivia
2022-03-01 07:50:46 -08:00
committed by GitHub
parent d82bd31bf1
commit 19dd7aad89
4 changed files with 295 additions and 248 deletions

View File

@@ -0,0 +1,218 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import * as React from "react";
import {
ActionId,
ActionImpl,
KBarAnimator,
KBarPortal,
KBarPositioner,
KBarResults,
KBarSearch,
useMatches,
useRegisterActions,
} from "kbar";
import { Action } from "kbar/lib/types";
import { Theme } from "@mui/material/styles";
import makeStyles from "@mui/styles/makeStyles";
import { routesAsKbarActions } from "./kbar-actions";
const useStyles = makeStyles((theme: Theme) => ({
resultItem: {
display: "flex",
gap: "8px",
alignItems: "center",
fontSize: 14,
"& .min-icon": {
color: theme.palette.primary.main,
width: "18px",
height: "18px",
},
},
}));
const searchStyle = {
padding: "12px 16px",
fontSize: "16px",
width: "100%",
boxSizing: "border-box" as React.CSSProperties["boxSizing"],
outline: "none",
border: "none",
background: "transparent",
color: "#111111",
};
const animatorStyle = {
maxWidth: "600px",
width: "100%",
background: "white",
color: "black",
borderRadius: "8px",
overflow: "hidden",
boxShadow: "rgba(0, 0, 0, 0.2) 0px 6px 20px 0px",
};
const groupNameStyle = {
padding: "8px 16px",
fontSize: "10px",
textTransform: "uppercase" as const,
opacity: 0.5,
};
const CommandBar = ({
features,
operatorMode,
}: {
operatorMode: boolean;
features: string[] | null;
}) => {
const initialActions: Action[] = routesAsKbarActions(features, operatorMode);
useRegisterActions(initialActions, [operatorMode]);
return (
<KBarPortal>
<KBarPositioner
style={{ zIndex: 9999, backgroundColor: "rgb(33,33,33,0.5)" }}
>
<KBarAnimator style={animatorStyle}>
<KBarSearch style={searchStyle} />
<RenderResults />
</KBarAnimator>
</KBarPositioner>
</KBarPortal>
);
};
function RenderResults() {
const { results, rootActionId } = useMatches();
return (
<KBarResults
items={results}
onRender={({ item, active }) =>
typeof item === "string" ? (
<div style={groupNameStyle}>{item}</div>
) : (
<ResultItem
action={item}
active={active}
currentRootActionId={`${rootActionId}`}
/>
)
}
/>
);
}
const ResultItem = React.forwardRef(
(
{
action,
active,
currentRootActionId,
}: {
action: ActionImpl;
active: boolean;
currentRootActionId: ActionId;
},
ref: React.Ref<HTMLDivElement>
) => {
const classes = useStyles();
const ancestors = React.useMemo(() => {
if (!currentRootActionId) return action.ancestors;
const index = action.ancestors.findIndex(
(ancestor) => ancestor.id === currentRootActionId
);
// +1 removes the currentRootAction; e.g.
// if we are on the "Set theme" parent action,
// the UI should not display "Set theme… > Dark"
// but rather just "Dark"
return action.ancestors.slice(index + 1);
}, [action.ancestors, currentRootActionId]);
return (
<div
ref={ref}
style={{
padding: "12px 16px",
background: active ? "#dddddd" : "transparent",
borderLeft: `2px solid ${
active ? "var(--foreground)" : "transparent"
}`,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
cursor: "pointer",
}}
>
<div className={classes.resultItem}>
{action.icon && action.icon}
<div style={{ display: "flex", flexDirection: "column" }}>
<div>
{ancestors.length > 0 &&
ancestors.map((ancestor) => (
<React.Fragment key={ancestor.id}>
<span
style={{
opacity: 0.5,
marginRight: 8,
}}
>
{ancestor.name}
</span>
<span
style={{
marginRight: 8,
}}
>
&rsaquo;
</span>
</React.Fragment>
))}
<span>{action.name}</span>
</div>
{action.subtitle && (
<span style={{ fontSize: 12 }}>{action.subtitle}</span>
)}
</div>
</div>
{action.shortcut?.length ? (
<div
aria-hidden
style={{ display: "grid", gridAutoFlow: "column", gap: "4px" }}
>
{action.shortcut.map((sc) => (
<kbd
key={sc}
style={{
padding: "4px 6px",
background: "rgba(0 0 0 / .1)",
borderRadius: "4px",
fontSize: 14,
}}
>
{sc}
</kbd>
))}
</div>
) : null}
</div>
);
}
);
export default CommandBar;

View File

@@ -14,68 +14,11 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import * as React from "react";
import history from "../../history";
import {
ActionId,
ActionImpl,
KBarAnimator,
KBarPortal,
KBarPositioner,
KBarProvider,
KBarResults,
KBarSearch,
useMatches,
} from "kbar";
import { KBarProvider } from "kbar";
import Console from "./Console";
import { validRoutes } from "./valid-routes";
import { AppState } from "../../store";
import { connect } from "react-redux";
import { Action } from "kbar/lib/types";
import { Theme } from "@mui/material/styles";
import makeStyles from "@mui/styles/makeStyles";
import { BucketsIcon } from "../../icons";
const useStyles = makeStyles((theme: Theme) => ({
resultItem: {
display: "flex",
gap: "8px",
alignItems: "center",
fontSize: 14,
"& .min-icon": {
color: theme.palette.primary.main,
width: "18px",
height: "18px",
},
},
}));
const searchStyle = {
padding: "12px 16px",
fontSize: "16px",
width: "100%",
boxSizing: "border-box" as React.CSSProperties["boxSizing"],
outline: "none",
border: "none",
background: "transparent",
color: "#111111",
};
const animatorStyle = {
maxWidth: "600px",
width: "100%",
background: "white",
color: "black",
borderRadius: "8px",
overflow: "hidden",
boxShadow: "rgba(0, 0, 0, 0.2) 0px 6px 20px 0px",
};
const groupNameStyle = {
padding: "8px 16px",
fontSize: "10px",
textTransform: "uppercase" as const,
opacity: 0.5,
};
import CommandBar from "./CommandBar";
const ConsoleKBar = ({
features,
@@ -89,190 +32,18 @@ const ConsoleKBar = ({
return <Console />;
}
const allowedMenuItems = validRoutes(features, operatorMode);
const initialActions = [];
for (const i of allowedMenuItems) {
if (i.children && i.children.length > 0) {
for (const childI of i.children) {
const a: Action = {
id: `${childI.id}`,
name: childI.name,
section: i.name,
perform: () => history.push(`${childI.to}`),
icon: <childI.icon />,
};
initialActions.push(a);
}
} else {
const a: Action = {
id: `${i.id}`,
name: i.name,
section: "Navigation",
perform: () => history.push(`${i.to}`),
icon: <i.icon />,
};
initialActions.push(a);
}
}
if (!operatorMode) {
// Add additional actions
const a: Action = {
id: `create-bucket`,
name: "Create Bucket",
section: "Buckets",
perform: () => history.push(`/add-bucket`),
icon: <BucketsIcon />,
};
initialActions.push(a);
}
return (
<KBarProvider
options={{
enableHistory: true,
}}
actions={initialActions}
>
<CommandBar />
<CommandBar operatorMode={operatorMode} features={features} />
<Console />
</KBarProvider>
);
};
function CommandBar() {
return (
<KBarPortal>
<KBarPositioner
style={{ zIndex: 9999, backgroundColor: "rgb(33,33,33,0.5)" }}
>
<KBarAnimator style={animatorStyle}>
<KBarSearch style={searchStyle} />
<RenderResults />
</KBarAnimator>
</KBarPositioner>
</KBarPortal>
);
}
function RenderResults() {
const { results, rootActionId } = useMatches();
return (
<KBarResults
items={results}
onRender={({ item, active }) =>
typeof item === "string" ? (
<div style={groupNameStyle}>{item}</div>
) : (
<ResultItem
action={item}
active={active}
currentRootActionId={`${rootActionId}`}
/>
)
}
/>
);
}
const ResultItem = React.forwardRef(
(
{
action,
active,
currentRootActionId,
}: {
action: ActionImpl;
active: boolean;
currentRootActionId: ActionId;
},
ref: React.Ref<HTMLDivElement>
) => {
const classes = useStyles();
const ancestors = React.useMemo(() => {
if (!currentRootActionId) return action.ancestors;
const index = action.ancestors.findIndex(
(ancestor) => ancestor.id === currentRootActionId
);
// +1 removes the currentRootAction; e.g.
// if we are on the "Set theme" parent action,
// the UI should not display "Set theme… > Dark"
// but rather just "Dark"
return action.ancestors.slice(index + 1);
}, [action.ancestors, currentRootActionId]);
return (
<div
ref={ref}
style={{
padding: "12px 16px",
background: active ? "#dddddd" : "transparent",
borderLeft: `2px solid ${
active ? "var(--foreground)" : "transparent"
}`,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
cursor: "pointer",
}}
>
<div className={classes.resultItem}>
{action.icon && action.icon}
<div style={{ display: "flex", flexDirection: "column" }}>
<div>
{ancestors.length > 0 &&
ancestors.map((ancestor) => (
<React.Fragment key={ancestor.id}>
<span
style={{
opacity: 0.5,
marginRight: 8,
}}
>
{ancestor.name}
</span>
<span
style={{
marginRight: 8,
}}
>
&rsaquo;
</span>
</React.Fragment>
))}
<span>{action.name}</span>
</div>
{action.subtitle && (
<span style={{ fontSize: 12 }}>{action.subtitle}</span>
)}
</div>
</div>
{action.shortcut?.length ? (
<div
aria-hidden
style={{ display: "grid", gridAutoFlow: "column", gap: "4px" }}
>
{action.shortcut.map((sc) => (
<kbd
key={sc}
style={{
padding: "4px 6px",
background: "rgba(0 0 0 / .1)",
borderRadius: "4px",
fontSize: 14,
}}
>
{sc}
</kbd>
))}
</div>
) : null}
</div>
);
}
);
const mapState = (state: AppState) => ({
operatorMode: state.system.operatorMode,
features: state.console.session.features,

View File

@@ -0,0 +1,63 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Action } from "kbar/lib/types";
import history from "../../history";
import { BucketsIcon } from "../../icons";
import { validRoutes } from "./valid-routes";
export const routesAsKbarActions = (
features: string[] | null,
operatorMode: boolean
) => {
const initialActions: Action[] = [];
const allowedMenuItems = validRoutes(features, operatorMode);
for (const i of allowedMenuItems) {
if (i.children && i.children.length > 0) {
for (const childI of i.children) {
const a: Action = {
id: `${childI.id}`,
name: childI.name,
section: i.name,
perform: () => history.push(`${childI.to}`),
icon: <childI.icon />,
};
initialActions.push(a);
}
} else {
const a: Action = {
id: `${i.id}`,
name: i.name,
section: "Navigation",
perform: () => history.push(`${i.to}`),
icon: <i.icon />,
};
initialActions.push(a);
}
}
if (!operatorMode) {
// Add additional actions
const a: Action = {
id: `create-bucket`,
name: "Create Bucket",
section: "Buckets",
perform: () => history.push(`/add-bucket`),
icon: <BucketsIcon />,
};
initialActions.push(a);
}
return initialActions;
};

View File

@@ -32,6 +32,7 @@ import {
GroupsMenuIcon,
HealthMenuIcon,
IdentityMenuIcon,
InspectMenuIcon,
LogsMenuIcon,
MetricsMenuIcon,
MonitoringMenuIcon,
@@ -39,7 +40,6 @@ import {
SupportMenuIcon,
TraceMenuIcon,
UsersMenuIcon,
InspectMenuIcon,
} from "../../icons/SidebarMenus";
import { hasPermission } from "../../common/SecureComponent";
import WatchIcon from "../../icons/WatchIcon";
@@ -59,7 +59,6 @@ export const validRoutes = (
operatorMode: boolean
) => {
const ldapIsEnabled = (features && features.includes("ldap-idp")) || false;
let consoleMenus: IMenuItem[] = [
{
name: "Buckets",
@@ -159,13 +158,6 @@ export const validRoutes = (
icon: DrivesMenuIcon,
component: NavLink,
},
{
name: "Inspect",
id: "inspectObjects",
to: IAM_PAGES.TOOLS_INSPECT,
icon: InspectMenuIcon,
component: NavLink,
},
],
},
{
@@ -202,13 +194,13 @@ export const validRoutes = (
// icon: CallHomeMenuIcon,
// to: IAM_PAGES.CALL_HOME,
// },
// {
// name: "Inspect",
// id: "inspect",
// component: NavLink,
// icon: InspectMenuIcon,
// to: IAM_PAGES.TOOLS_WATCH,
// },
{
name: "Inspect",
id: "inspectObjects",
to: IAM_PAGES.TOOLS_INSPECT,
icon: InspectMenuIcon,
component: NavLink,
},
// {
// name: "Profile",
// id: "profile",
@@ -277,6 +269,7 @@ export const validRoutes = (
{
group: "Operator",
type: "item",
id: "Tenants",
component: NavLink,
to: IAM_PAGES.TENANTS,
name: "Tenants",
@@ -286,6 +279,7 @@ export const validRoutes = (
{
group: "Operator",
type: "item",
id: "License",
component: NavLink,
to: IAM_PAGES.LICENSE,
name: "License",
@@ -295,6 +289,7 @@ export const validRoutes = (
{
group: "Operator",
type: "item",
id: "Documentation",
component: NavLink,
to: IAM_PAGES.DOCUMENTATION,
name: "Documentation",