Make user details a page (#726)
This commit is contained in:
38
.github/workflows/assets.yml
vendored
38
.github/workflows/assets.yml
vendored
@@ -1,38 +0,0 @@
|
||||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: CI
|
||||
|
||||
# Controls when the action will run.
|
||||
on:
|
||||
# Triggers the workflow on push or pull request events but only for the master branch
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Make assets
|
||||
run: make assets
|
||||
- name: Commit files
|
||||
run: |
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git add portal-ui/build
|
||||
git commit -m "Generated Assets for Change" -a
|
||||
- name: Push changes
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: ${{ github.ref }}
|
||||
@@ -39,7 +39,7 @@ type User struct {
|
||||
MemberOf []string `json:"memberOf"`
|
||||
|
||||
// policy
|
||||
Policy string `json:"policy,omitempty"`
|
||||
Policy []string `json:"policy"`
|
||||
|
||||
// status
|
||||
Status string `json:"status,omitempty"`
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
var (
|
||||
configuration = "/settings"
|
||||
users = "/users"
|
||||
usersDetail = "/users/:userName"
|
||||
groups = "/groups"
|
||||
iamPolicies = "/policies"
|
||||
dashboard = "/dashboard"
|
||||
@@ -268,6 +269,7 @@ var displayRules = map[string]func() bool{
|
||||
var endpointRules = map[string]ConfigurationActionSet{
|
||||
configuration: configurationActionSet,
|
||||
users: usersActionSet,
|
||||
usersDetail: usersActionSet,
|
||||
groups: groupsActionSet,
|
||||
iamPolicies: iamPoliciesActionSet,
|
||||
dashboard: dashboardActionSet,
|
||||
|
||||
@@ -72,7 +72,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
|
||||
"admin:*",
|
||||
},
|
||||
},
|
||||
want: 19,
|
||||
want: 20,
|
||||
},
|
||||
{
|
||||
name: "all s3 endpoints",
|
||||
@@ -91,7 +91,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
|
||||
"s3:*",
|
||||
},
|
||||
},
|
||||
want: 21,
|
||||
want: 22,
|
||||
},
|
||||
{
|
||||
name: "Console User - default endpoints",
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.a19f3d53.chunk.css",
|
||||
"main.js": "/static/js/main.33f485b0.chunk.js",
|
||||
"main.js.map": "/static/js/main.33f485b0.chunk.js.map",
|
||||
"main.js": "/static/js/main.7489cd3f.chunk.js",
|
||||
"main.js.map": "/static/js/main.7489cd3f.chunk.js.map",
|
||||
"runtime-main.js": "/static/js/runtime-main.f48e99e5.js",
|
||||
"runtime-main.js.map": "/static/js/runtime-main.f48e99e5.js.map",
|
||||
"static/css/2.f324abd6.chunk.css": "/static/css/2.f324abd6.chunk.css",
|
||||
"static/js/2.37779a66.chunk.js": "/static/js/2.37779a66.chunk.js",
|
||||
"static/js/2.37779a66.chunk.js.map": "/static/js/2.37779a66.chunk.js.map",
|
||||
"static/css/2.2d55190c.chunk.css": "/static/css/2.2d55190c.chunk.css",
|
||||
"static/js/2.42769b7f.chunk.js": "/static/js/2.42769b7f.chunk.js",
|
||||
"static/js/2.42769b7f.chunk.js.map": "/static/js/2.42769b7f.chunk.js.map",
|
||||
"index.html": "/index.html",
|
||||
"static/css/2.f324abd6.chunk.css.map": "/static/css/2.f324abd6.chunk.css.map",
|
||||
"static/css/2.2d55190c.chunk.css.map": "/static/css/2.2d55190c.chunk.css.map",
|
||||
"static/css/main.a19f3d53.chunk.css.map": "/static/css/main.a19f3d53.chunk.css.map",
|
||||
"static/js/2.37779a66.chunk.js.LICENSE.txt": "/static/js/2.37779a66.chunk.js.LICENSE.txt",
|
||||
"static/js/2.42769b7f.chunk.js.LICENSE.txt": "/static/js/2.42769b7f.chunk.js.LICENSE.txt",
|
||||
"static/media/minio_console_logo.0837460e.svg": "/static/media/minio_console_logo.0837460e.svg",
|
||||
"static/media/minio_operator_logo.1312b7c9.svg": "/static/media/minio_operator_logo.1312b7c9.svg"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/js/runtime-main.f48e99e5.js",
|
||||
"static/css/2.f324abd6.chunk.css",
|
||||
"static/js/2.37779a66.chunk.js",
|
||||
"static/css/2.2d55190c.chunk.css",
|
||||
"static/js/2.42769b7f.chunk.js",
|
||||
"static/css/main.a19f3d53.chunk.css",
|
||||
"static/js/main.33f485b0.chunk.js"
|
||||
"static/js/main.7489cd3f.chunk.js"
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="MinIO Console"/><link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;500;700;900&display=swap" rel="stylesheet"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png"/><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="manifest" href="/manifest.json"/><link rel="mask-icon" href="/safari-pinned-tab.svg" color="#3a4e54"/><title>MinIO Console</title><link href="/static/css/2.f324abd6.chunk.css" rel="stylesheet"><link href="/static/css/main.a19f3d53.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function r(r){for(var n,l,i=r[0],a=r[1],p=r[2],c=0,s=[];c<i.length;c++)l=i[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,p||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var a=t[i];0!==o[a]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var i=this["webpackJsonpportal-ui"]=this["webpackJsonpportal-ui"]||[],a=i.push.bind(i);i.push=r,i=i.slice();for(var p=0;p<i.length;p++)r(i[p]);var f=a;t()}([])</script><script src="/static/js/2.37779a66.chunk.js"></script><script src="/static/js/main.33f485b0.chunk.js"></script></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="MinIO Console"/><link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;500;700;900&display=swap" rel="stylesheet"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png"/><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="manifest" href="/manifest.json"/><link rel="mask-icon" href="/safari-pinned-tab.svg" color="#3a4e54"/><title>MinIO Console</title><link href="/static/css/2.2d55190c.chunk.css" rel="stylesheet"><link href="/static/css/main.a19f3d53.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function r(r){for(var n,l,i=r[0],a=r[1],p=r[2],c=0,s=[];c<i.length;c++)l=i[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,p||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var a=t[i];0!==o[a]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var i=this["webpackJsonpportal-ui"]=this["webpackJsonpportal-ui"]||[],a=i.push.bind(i);i.push=r,i=i.slice();for(var p=0;p<i.length;p++)r(i[p]);var f=a;t()}([])</script><script src="/static/js/2.42769b7f.chunk.js"></script><script src="/static/js/main.7489cd3f.chunk.js"></script></body></html>
|
||||
@@ -1,2 +1,2 @@
|
||||
.ReactVirtualized__Table__headerRow{font-weight:700;text-transform:uppercase}.ReactVirtualized__Table__headerRow,.ReactVirtualized__Table__row{display:flex;flex-direction:row;align-items:center}.ReactVirtualized__Table__headerTruncatedText{display:inline-block;max-width:100%;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.ReactVirtualized__Table__headerColumn,.ReactVirtualized__Table__rowColumn{margin-right:10px;min-width:0}.ReactVirtualized__Table__rowColumn{text-overflow:ellipsis;white-space:nowrap}.ReactVirtualized__Table__headerColumn:first-of-type,.ReactVirtualized__Table__rowColumn:first-of-type{margin-left:10px}.ReactVirtualized__Table__sortableHeaderColumn{cursor:pointer}.ReactVirtualized__Table__sortableHeaderIconContainer{display:flex;align-items:center}.ReactVirtualized__Table__sortableHeaderIcon{flex:0 0 24px;height:1em;width:1em;fill:currentColor}.react-grid-layout{position:relative;transition:height .2s ease}.react-grid-item{transition:all .2s ease;transition-property:left,top}.react-grid-item img{pointer-events:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.react-grid-item.cssTransforms{transition-property:transform}.react-grid-item.resizing{z-index:1;will-change:width,height}.react-grid-item.react-draggable-dragging{transition:none;z-index:3;will-change:transform}.react-grid-item.dropping{visibility:hidden}.react-grid-item.react-grid-placeholder{background:red;opacity:.2;transition-duration:.1s;z-index:2;-webkit-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.react-grid-item>.react-resizable-handle{position:absolute;width:20px;height:20px}.react-grid-item>.react-resizable-handle:after{content:"";position:absolute;right:3px;bottom:3px;width:5px;height:5px;border-right:2px solid rgba(0,0,0,.4);border-bottom:2px solid rgba(0,0,0,.4)}.react-resizable-hide>.react-resizable-handle{display:none}.react-grid-item>.react-resizable-handle.react-resizable-handle-sw{bottom:0;left:0;cursor:sw-resize;transform:rotate(90deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-se{bottom:0;right:0;cursor:se-resize}.react-grid-item>.react-resizable-handle.react-resizable-handle-nw{top:0;left:0;cursor:nw-resize;transform:rotate(180deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-ne{top:0;right:0;cursor:ne-resize;transform:rotate(270deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-e,.react-grid-item>.react-resizable-handle.react-resizable-handle-w{top:50%;margin-top:-10px;cursor:ew-resize}.react-grid-item>.react-resizable-handle.react-resizable-handle-w{left:0;transform:rotate(135deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-e{right:0;transform:rotate(315deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-n,.react-grid-item>.react-resizable-handle.react-resizable-handle-s{left:50%;margin-left:-10px;cursor:ns-resize}.react-grid-item>.react-resizable-handle.react-resizable-handle-n{top:0;transform:rotate(225deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-s{bottom:0;transform:rotate(45deg)}.react-resizable{position:relative}.react-resizable-handle{position:absolute;width:20px;height:20px;background-repeat:no-repeat;background-origin:content-box;box-sizing:border-box;background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgd2lkdGg9IjYiIGhlaWdodD0iNiI+PHBhdGggZD0iTTYgNkgwVjQuMmg0LjJWMEg2djZ6IiBvcGFjaXR5PSIuMzAyIi8+PC9zdmc+");background-position:100% 100%;padding:0 3px 3px 0}.react-resizable-handle-sw{bottom:0;left:0;cursor:sw-resize;transform:rotate(90deg)}.react-resizable-handle-se{bottom:0;right:0;cursor:se-resize}.react-resizable-handle-nw{top:0;left:0;cursor:nw-resize;transform:rotate(180deg)}.react-resizable-handle-ne{top:0;right:0;cursor:ne-resize;transform:rotate(270deg)}.react-resizable-handle-e,.react-resizable-handle-w{top:50%;margin-top:-10px;cursor:ew-resize}.react-resizable-handle-w{left:0;transform:rotate(135deg)}.react-resizable-handle-e{right:0;transform:rotate(315deg)}.react-resizable-handle-n,.react-resizable-handle-s{left:50%;margin-left:-10px;cursor:ns-resize}.react-resizable-handle-n{top:0;transform:rotate(225deg)}.react-resizable-handle-s{bottom:0;transform:rotate(45deg)}
|
||||
/*# sourceMappingURL=2.f324abd6.chunk.css.map */
|
||||
/*# sourceMappingURL=2.2d55190c.chunk.css.map */
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
3
portal-ui/build/static/js/2.42769b7f.chunk.js
Normal file
3
portal-ui/build/static/js/2.42769b7f.chunk.js
Normal file
File diff suppressed because one or more lines are too long
1
portal-ui/build/static/js/2.42769b7f.chunk.js.map
Normal file
1
portal-ui/build/static/js/2.42769b7f.chunk.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
portal-ui/build/static/js/main.7489cd3f.chunk.js
Normal file
2
portal-ui/build/static/js/main.7489cd3f.chunk.js
Normal file
File diff suppressed because one or more lines are too long
1
portal-ui/build/static/js/main.7489cd3f.chunk.js.map
Normal file
1
portal-ui/build/static/js/main.7489cd3f.chunk.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -66,6 +66,7 @@ import AddPolicy from "../../Policies/AddPolicy";
|
||||
import DeleteReplicationRule from "../ViewBucket/DeleteReplicationRule";
|
||||
import EditLifecycleConfiguration from "./EditLifecycleConfiguration";
|
||||
import AddLifecycleModal from "./AddLifecycleModal";
|
||||
import history from "../../../../history";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -710,6 +711,12 @@ const ViewBucket = ({
|
||||
},
|
||||
];
|
||||
|
||||
const userViewAction = (user: any) => {
|
||||
history.push(`/users/${user}`);
|
||||
};
|
||||
|
||||
const userTableActions = [{ type: "view", onClick: userViewAction }];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{addScreenOpen && (
|
||||
@@ -1061,6 +1068,7 @@ const ViewBucket = ({
|
||||
{usersEnabled && (
|
||||
<TabPanel index={3} value={curTab}>
|
||||
<TableWrapper
|
||||
itemActions={userTableActions}
|
||||
columns={[{ label: "User", elementKey: "accessKey" }]}
|
||||
isLoading={loadingUsers}
|
||||
records={bucketUsers}
|
||||
|
||||
@@ -245,6 +245,10 @@ const Console = ({
|
||||
component: Watch,
|
||||
path: "/watch",
|
||||
},
|
||||
{
|
||||
component: Users,
|
||||
path: "/users/:userName",
|
||||
},
|
||||
{
|
||||
component: Users,
|
||||
path: "/users",
|
||||
|
||||
@@ -114,9 +114,9 @@ const SetPolicy = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const userPolicy: String = get(selectedUser, "policy", "");
|
||||
setActualPolicy(userPolicy.split(","));
|
||||
setSelectedPolicy(userPolicy.split(","));
|
||||
const userPolicy: string[] = get(selectedUser, "policy", []);
|
||||
setActualPolicy(userPolicy);
|
||||
setSelectedPolicy(userPolicy);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, selectedGroup, selectedUser]);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// 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 React, { useEffect, useState } from "react";
|
||||
import React, { Fragment, useEffect, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import get from "lodash/get";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
@@ -29,16 +29,12 @@ import Tab from "@material-ui/core/Tab";
|
||||
import { CreateIcon } from "../../../../icons";
|
||||
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import {
|
||||
getTimeFromTimestamp,
|
||||
niceBytes,
|
||||
niceDays,
|
||||
} from "../../../../common/utils";
|
||||
import { niceBytes, niceDays } from "../../../../common/utils";
|
||||
import AddPoolModal from "./AddPoolModal";
|
||||
import AddBucket from "../../Buckets/ListBuckets/AddBucket";
|
||||
import ReplicationSetup from "./ReplicationSetup";
|
||||
import api from "../../../../common/api";
|
||||
import { IPool, ITenant, IPodListElement } from "../ListTenants/types";
|
||||
import { IPodListElement, IPool, ITenant } from "../ListTenants/types";
|
||||
import PageHeader from "../../Common/PageHeader/PageHeader";
|
||||
import UsageBarWrapper from "../../Common/UsageBarWrapper/UsageBarWrapper";
|
||||
import UpdateTenantModal from "./UpdateTenantModal";
|
||||
@@ -47,7 +43,6 @@ import { LicenseInfo } from "../../License/types";
|
||||
import { Link } from "react-router-dom";
|
||||
import { setErrorSnackMessage } from "../../../../actions";
|
||||
import Moment from "react-moment";
|
||||
import { Fragment } from "react";
|
||||
|
||||
interface ITenantDetailsProps {
|
||||
classes: any;
|
||||
|
||||
@@ -48,7 +48,7 @@ const styles = (theme: Theme) =>
|
||||
...modalBasic,
|
||||
});
|
||||
|
||||
const AddToGroup = ({
|
||||
const BulkAddToGroup = ({
|
||||
open,
|
||||
checkedUsers,
|
||||
closeModalAndRefresh,
|
||||
@@ -183,4 +183,4 @@ const mapDispatchToProps = {
|
||||
|
||||
const connector = connect(null, mapDispatchToProps);
|
||||
|
||||
export default withStyles(styles)(connector(AddToGroup));
|
||||
export default withStyles(styles)(connector(BulkAddToGroup));
|
||||
210
portal-ui/src/screens/Console/Users/ChangeUserGroups.tsx
Normal file
210
portal-ui/src/screens/Console/Users/ChangeUserGroups.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 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 React, { useCallback, useEffect, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import { Button, LinearProgress } from "@material-ui/core";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
|
||||
import { setModalErrorSnackMessage } from "../../../actions";
|
||||
import api from "../../../common/api";
|
||||
import GroupsSelectors from "./GroupsSelectors";
|
||||
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
strongText: {
|
||||
fontWeight: 700,
|
||||
},
|
||||
keyName: {
|
||||
marginLeft: 5,
|
||||
},
|
||||
buttonContainer: {
|
||||
textAlign: "right",
|
||||
},
|
||||
...modalBasic,
|
||||
});
|
||||
|
||||
interface IChangeUserGroupsContentProps {
|
||||
classes: any;
|
||||
closeModalAndRefresh: () => void;
|
||||
selectedUser: string;
|
||||
open: boolean;
|
||||
setModalErrorSnackMessage: typeof setModalErrorSnackMessage;
|
||||
}
|
||||
|
||||
const ChangeUserGroups = ({
|
||||
classes,
|
||||
closeModalAndRefresh,
|
||||
selectedUser,
|
||||
open,
|
||||
setModalErrorSnackMessage,
|
||||
}: IChangeUserGroupsContentProps) => {
|
||||
const [addLoading, setAddLoading] = useState<boolean>(false);
|
||||
const [accessKey, setAccessKey] = useState<string>("");
|
||||
const [secretKey, setSecretKey] = useState<string>("");
|
||||
const [enabled, setEnabled] = useState<boolean>(false);
|
||||
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
|
||||
|
||||
const getUserInformation = useCallback(() => {
|
||||
if (!selectedUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
api
|
||||
.invoke("GET", `/api/v1/users/${selectedUser}`)
|
||||
.then((res) => {
|
||||
setAddLoading(false);
|
||||
setAccessKey(res.accessKey);
|
||||
setSelectedGroups(res.memberOf || []);
|
||||
setEnabled(res.status === "enabled");
|
||||
})
|
||||
.catch((err) => {
|
||||
setAddLoading(false);
|
||||
setModalErrorSnackMessage(err);
|
||||
});
|
||||
}, [selectedUser, setModalErrorSnackMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedUser == null) {
|
||||
setAccessKey("");
|
||||
setSecretKey("");
|
||||
setSelectedGroups([]);
|
||||
} else {
|
||||
getUserInformation();
|
||||
}
|
||||
}, [selectedUser, getUserInformation]);
|
||||
|
||||
const saveRecord = (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (addLoading) {
|
||||
return;
|
||||
}
|
||||
setAddLoading(true);
|
||||
if (selectedUser !== null) {
|
||||
api
|
||||
.invoke("PUT", `/api/v1/users/${selectedUser}`, {
|
||||
status: enabled ? "enabled" : "disabled",
|
||||
groups: selectedGroups,
|
||||
})
|
||||
.then((_) => {
|
||||
setAddLoading(false);
|
||||
closeModalAndRefresh();
|
||||
})
|
||||
.catch((err) => {
|
||||
setAddLoading(false);
|
||||
setModalErrorSnackMessage(err);
|
||||
});
|
||||
} else {
|
||||
api
|
||||
.invoke("POST", "/api/v1/users", {
|
||||
accessKey,
|
||||
secretKey,
|
||||
groups: selectedGroups,
|
||||
})
|
||||
.then((_) => {
|
||||
setAddLoading(false);
|
||||
closeModalAndRefresh();
|
||||
})
|
||||
.catch((err) => {
|
||||
setAddLoading(false);
|
||||
setModalErrorSnackMessage(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
if (selectedUser !== null) {
|
||||
setSelectedGroups([]);
|
||||
return;
|
||||
}
|
||||
setAccessKey("");
|
||||
setSecretKey("");
|
||||
setSelectedGroups([]);
|
||||
};
|
||||
|
||||
const sendEnabled =
|
||||
accessKey.trim() !== "" &&
|
||||
((secretKey.trim() !== "" && selectedUser === null) ||
|
||||
selectedUser !== null);
|
||||
return (
|
||||
<ModalWrapper
|
||||
onClose={() => {
|
||||
closeModalAndRefresh();
|
||||
}}
|
||||
modalOpen={open}
|
||||
title={"Set Groups"}
|
||||
>
|
||||
<React.Fragment>
|
||||
<form
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
|
||||
saveRecord(e);
|
||||
}}
|
||||
>
|
||||
<Grid container>
|
||||
<Grid item xs={12} className={classes.formScrollable}>
|
||||
<Grid item xs={12}>
|
||||
<GroupsSelectors
|
||||
selectedGroups={selectedGroups}
|
||||
setSelectedGroups={(elements: string[]) => {
|
||||
setSelectedGroups(elements);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
<button
|
||||
type="button"
|
||||
color="primary"
|
||||
className={classes.clearButton}
|
||||
onClick={() => {
|
||||
resetForm();
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={addLoading || !sendEnabled}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Grid>
|
||||
{addLoading && (
|
||||
<Grid item xs={12}>
|
||||
<LinearProgress />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</form>
|
||||
</React.Fragment>
|
||||
</ModalWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setModalErrorSnackMessage,
|
||||
};
|
||||
|
||||
const connector = connect(null, mapDispatchToProps);
|
||||
|
||||
export default withStyles(styles)(connector(ChangeUserGroups));
|
||||
291
portal-ui/src/screens/Console/Users/ListUsers.tsx
Normal file
291
portal-ui/src/screens/Console/Users/ListUsers.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 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 React, { useCallback, useEffect, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import api from "../../../common/api";
|
||||
import { Button, Grid, InputAdornment, TextField } from "@material-ui/core";
|
||||
import SearchIcon from "@material-ui/icons/Search";
|
||||
import GroupIcon from "@material-ui/icons/Group";
|
||||
import { User, UsersList } from "./types";
|
||||
import { usersSort } from "../../../utils/sortFunctions";
|
||||
import { CreateIcon } from "../../../icons";
|
||||
import {
|
||||
actionsTray,
|
||||
containerForHeader,
|
||||
searchField,
|
||||
} from "../Common/FormComponents/common/styleLibrary";
|
||||
import { setErrorSnackMessage } from "../../../actions";
|
||||
import AddUser from "./AddUser";
|
||||
import DeleteUser from "./DeleteUser";
|
||||
import AddToGroup from "./BulkAddToGroup";
|
||||
import TableWrapper from "../Common/TableWrapper/TableWrapper";
|
||||
import SetPolicy from "../Policies/SetPolicy";
|
||||
import PageHeader from "../Common/PageHeader/PageHeader";
|
||||
import history from "../../../history";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
seeMore: {
|
||||
marginTop: theme.spacing(3),
|
||||
},
|
||||
paper: {
|
||||
// padding: theme.spacing(2),
|
||||
display: "flex",
|
||||
overflow: "auto",
|
||||
flexDirection: "column",
|
||||
},
|
||||
addSideBar: {
|
||||
width: "320px",
|
||||
padding: "20px",
|
||||
},
|
||||
tableToolbar: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(0),
|
||||
},
|
||||
wrapCell: {
|
||||
maxWidth: "200px",
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word",
|
||||
},
|
||||
minTableHeader: {
|
||||
color: "#393939",
|
||||
"& tr": {
|
||||
"& th": {
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
...actionsTray,
|
||||
...searchField,
|
||||
...containerForHeader(theme.spacing(4)),
|
||||
});
|
||||
|
||||
interface IUsersProps {
|
||||
classes: any;
|
||||
setErrorSnackMessage: typeof setErrorSnackMessage;
|
||||
}
|
||||
|
||||
const ListUsers = ({ classes, setErrorSnackMessage }: IUsersProps) => {
|
||||
const [records, setRecords] = useState<User[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
|
||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||
const [addGroupOpen, setAddGroupOpen] = useState<boolean>(false);
|
||||
const [filter, setFilter] = useState<string>("");
|
||||
const [checkedUsers, setCheckedUsers] = useState<string[]>([]);
|
||||
const [policyOpen, setPolicyOpen] = useState<boolean>(false);
|
||||
|
||||
const fetchRecords = useCallback(() => {
|
||||
setLoading(true);
|
||||
api
|
||||
.invoke("GET", `/api/v1/users`)
|
||||
.then((res: UsersList) => {
|
||||
const users = res.users === null ? [] : res.users;
|
||||
|
||||
setLoading(false);
|
||||
setRecords(users.sort(usersSort));
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
setErrorSnackMessage(err);
|
||||
});
|
||||
}, [setLoading, setRecords, setErrorSnackMessage]);
|
||||
|
||||
const closeAddModalAndRefresh = () => {
|
||||
setAddScreenOpen(false);
|
||||
fetchRecords();
|
||||
};
|
||||
|
||||
const closeDeleteModalAndRefresh = (refresh: boolean) => {
|
||||
setDeleteOpen(false);
|
||||
if (refresh) {
|
||||
fetchRecords();
|
||||
}
|
||||
};
|
||||
|
||||
const closeAddGroupBulk = (unCheckAll: boolean = false) => {
|
||||
setAddGroupOpen(false);
|
||||
if (unCheckAll) {
|
||||
setCheckedUsers([]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchRecords();
|
||||
}, [fetchRecords]);
|
||||
|
||||
const filteredRecords = records.filter((elementItem) =>
|
||||
elementItem.accessKey.includes(filter)
|
||||
);
|
||||
|
||||
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const targetD = e.target;
|
||||
const value = targetD.value;
|
||||
const checked = targetD.checked;
|
||||
|
||||
let elements: string[] = [...checkedUsers]; // We clone the checkedUsers array
|
||||
|
||||
if (checked) {
|
||||
// If the user has checked this field we need to push this to checkedUsersList
|
||||
elements.push(value);
|
||||
} else {
|
||||
// User has unchecked this field, we need to remove it from the list
|
||||
elements = elements.filter((element) => element !== value);
|
||||
}
|
||||
|
||||
setCheckedUsers(elements);
|
||||
|
||||
return elements;
|
||||
};
|
||||
|
||||
const viewAction = (selectionElement: any): void => {
|
||||
history.push(`/users/${selectionElement.accessKey}`);
|
||||
};
|
||||
|
||||
const deleteAction = (selectionElement: any): void => {
|
||||
setDeleteOpen(true);
|
||||
setSelectedUser(selectionElement);
|
||||
};
|
||||
|
||||
const userLoggedIn = atob(localStorage.getItem("userLoggedIn") || "");
|
||||
|
||||
const tableActions = [
|
||||
{ type: "view", onClick: viewAction },
|
||||
{
|
||||
type: "delete",
|
||||
onClick: deleteAction,
|
||||
disableButtonFunction: (topValue: any) => topValue === userLoggedIn,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{addScreenOpen && (
|
||||
<AddUser
|
||||
open={addScreenOpen}
|
||||
selectedUser={selectedUser}
|
||||
closeModalAndRefresh={() => {
|
||||
closeAddModalAndRefresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{policyOpen && (
|
||||
<SetPolicy
|
||||
open={policyOpen}
|
||||
selectedUser={selectedUser}
|
||||
selectedGroup={null}
|
||||
closeModalAndRefresh={() => {
|
||||
setPolicyOpen(false);
|
||||
fetchRecords();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{deleteOpen && (
|
||||
<DeleteUser
|
||||
deleteOpen={deleteOpen}
|
||||
selectedUser={selectedUser}
|
||||
closeDeleteModalAndRefresh={(refresh: boolean) => {
|
||||
closeDeleteModalAndRefresh(refresh);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{addGroupOpen && (
|
||||
<AddToGroup
|
||||
open={addGroupOpen}
|
||||
checkedUsers={checkedUsers}
|
||||
closeModalAndRefresh={(close: boolean) => {
|
||||
closeAddGroupBulk(close);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<PageHeader label={"Users"} />
|
||||
<Grid container>
|
||||
<Grid item xs={12} className={classes.container}>
|
||||
<Grid item xs={12} className={classes.actionsTray}>
|
||||
<TextField
|
||||
placeholder="Search Users"
|
||||
className={classes.searchField}
|
||||
id="search-resource"
|
||||
label=""
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setFilter(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<GroupIcon />}
|
||||
disabled={checkedUsers.length <= 0}
|
||||
onClick={() => {
|
||||
if (checkedUsers.length > 0) {
|
||||
setAddGroupOpen(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add to Group
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<CreateIcon />}
|
||||
onClick={() => {
|
||||
setAddScreenOpen(true);
|
||||
setSelectedUser(null);
|
||||
}}
|
||||
>
|
||||
Create User
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TableWrapper
|
||||
itemActions={tableActions}
|
||||
columns={[{ label: "Access Key", elementKey: "accessKey" }]}
|
||||
onSelect={selectionChanged}
|
||||
selectedItems={checkedUsers}
|
||||
isLoading={loading}
|
||||
records={filteredRecords}
|
||||
entityName="Users"
|
||||
idField="accessKey"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setErrorSnackMessage,
|
||||
};
|
||||
|
||||
const connector = connect(null, mapDispatchToProps);
|
||||
|
||||
export default withStyles(styles)(connector(ListUsers));
|
||||
160
portal-ui/src/screens/Console/Users/SetUserPolicies.tsx
Normal file
160
portal-ui/src/screens/Console/Users/SetUserPolicies.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 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/>.
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 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 React, { useEffect, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { Button, LinearProgress } from "@material-ui/core";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
|
||||
import { IPolicyItem } from "../Users/types";
|
||||
import { setModalErrorSnackMessage } from "../../../actions";
|
||||
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
|
||||
import api from "../../../common/api";
|
||||
import PolicySelectors from "../Policies/PolicySelectors";
|
||||
|
||||
interface ISetUserPoliciesProps {
|
||||
classes: any;
|
||||
closeModalAndRefresh: () => void;
|
||||
selectedUser: string;
|
||||
currentPolicies: IPolicyItem[];
|
||||
open: boolean;
|
||||
setModalErrorSnackMessage: typeof setModalErrorSnackMessage;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
...modalBasic,
|
||||
buttonContainer: {
|
||||
textAlign: "right",
|
||||
},
|
||||
});
|
||||
|
||||
const SetUserPolicies = ({
|
||||
classes,
|
||||
closeModalAndRefresh,
|
||||
selectedUser,
|
||||
currentPolicies,
|
||||
setModalErrorSnackMessage,
|
||||
open,
|
||||
}: ISetUserPoliciesProps) => {
|
||||
//Local States
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [actualPolicy, setActualPolicy] = useState<string[]>([]);
|
||||
const [selectedPolicy, setSelectedPolicy] = useState<string[]>([]);
|
||||
|
||||
const SetUserPoliciesAction = () => {
|
||||
let entity = "user";
|
||||
let value = selectedUser;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
api
|
||||
.invoke("PUT", `/api/v1/set-policy/${selectedPolicy}`, {
|
||||
entityName: value,
|
||||
entityType: entity,
|
||||
})
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
closeModalAndRefresh();
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
setModalErrorSnackMessage(err);
|
||||
});
|
||||
};
|
||||
|
||||
const resetSelection = () => {
|
||||
setSelectedPolicy(actualPolicy);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const userPolicy: string[] = [];
|
||||
for (let pol of currentPolicies) {
|
||||
userPolicy.push(pol.policy);
|
||||
}
|
||||
setActualPolicy(userPolicy);
|
||||
setSelectedPolicy(userPolicy);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, selectedUser]);
|
||||
|
||||
return (
|
||||
<ModalWrapper
|
||||
onClose={() => {
|
||||
closeModalAndRefresh();
|
||||
}}
|
||||
modalOpen={open}
|
||||
title="Set Policies"
|
||||
>
|
||||
<PolicySelectors
|
||||
selectedPolicy={selectedPolicy}
|
||||
setSelectedPolicy={setSelectedPolicy}
|
||||
/>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
<button
|
||||
type="button"
|
||||
color="primary"
|
||||
className={classes.clearButton}
|
||||
onClick={resetSelection}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={loading}
|
||||
onClick={SetUserPoliciesAction}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Grid>
|
||||
{loading && (
|
||||
<Grid item xs={12}>
|
||||
<LinearProgress />
|
||||
</Grid>
|
||||
)}
|
||||
</ModalWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setModalErrorSnackMessage,
|
||||
};
|
||||
|
||||
const connector = connect(null, mapDispatchToProps);
|
||||
|
||||
export default withStyles(styles)(connector(SetUserPolicies));
|
||||
@@ -14,284 +14,33 @@
|
||||
// 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 React, { useCallback, useEffect, useState } from "react";
|
||||
import React from "react";
|
||||
import history from "../../../history";
|
||||
import { Route, Router, Switch, withRouter } from "react-router-dom";
|
||||
import { connect } from "react-redux";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import api from "../../../common/api";
|
||||
import { Button, Grid, InputAdornment, TextField } from "@material-ui/core";
|
||||
import SearchIcon from "@material-ui/icons/Search";
|
||||
import GroupIcon from "@material-ui/icons/Group";
|
||||
import { User, UsersList } from "./types";
|
||||
import { usersSort } from "../../../utils/sortFunctions";
|
||||
import { CreateIcon } from "../../../icons";
|
||||
import {
|
||||
actionsTray,
|
||||
containerForHeader,
|
||||
searchField,
|
||||
} from "../Common/FormComponents/common/styleLibrary";
|
||||
import { setErrorSnackMessage } from "../../../actions";
|
||||
import AddUser from "./AddUser";
|
||||
import DeleteUser from "./DeleteUser";
|
||||
import AddToGroup from "./AddToGroup";
|
||||
import TableWrapper from "../Common/TableWrapper/TableWrapper";
|
||||
import SetPolicy from "../Policies/SetPolicy";
|
||||
import PageHeader from "../Common/PageHeader/PageHeader";
|
||||
import { AppState } from "../../../store";
|
||||
import { setMenuOpen } from "../../../actions";
|
||||
import NotFoundPage from "../../NotFoundPage";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
seeMore: {
|
||||
marginTop: theme.spacing(3),
|
||||
},
|
||||
paper: {
|
||||
// padding: theme.spacing(2),
|
||||
display: "flex",
|
||||
overflow: "auto",
|
||||
flexDirection: "column",
|
||||
},
|
||||
addSideBar: {
|
||||
width: "320px",
|
||||
padding: "20px",
|
||||
},
|
||||
tableToolbar: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(0),
|
||||
},
|
||||
wrapCell: {
|
||||
maxWidth: "200px",
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word",
|
||||
},
|
||||
minTableHeader: {
|
||||
color: "#393939",
|
||||
"& tr": {
|
||||
"& th": {
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
...actionsTray,
|
||||
...searchField,
|
||||
...containerForHeader(theme.spacing(4)),
|
||||
});
|
||||
import ListUsers from "./ListUsers";
|
||||
import ViewUser from "./ViewUser";
|
||||
|
||||
interface IUsersProps {
|
||||
classes: any;
|
||||
setErrorSnackMessage: typeof setErrorSnackMessage;
|
||||
}
|
||||
const mapState = (state: AppState) => ({
|
||||
open: state.system.sidebarOpen,
|
||||
});
|
||||
|
||||
const Users = ({ classes, setErrorSnackMessage }: IUsersProps) => {
|
||||
const [records, setRecords] = useState<User[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
|
||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||
const [addGroupOpen, setAddGroupOpen] = useState<boolean>(false);
|
||||
const [filter, setFilter] = useState<string>("");
|
||||
const [checkedUsers, setCheckedUsers] = useState<string[]>([]);
|
||||
const [policyOpen, setPolicyOpen] = useState<boolean>(false);
|
||||
|
||||
const fetchRecords = useCallback(() => {
|
||||
setLoading(true);
|
||||
api
|
||||
.invoke("GET", `/api/v1/users`)
|
||||
.then((res: UsersList) => {
|
||||
const users = res.users === null ? [] : res.users;
|
||||
|
||||
setLoading(false);
|
||||
setRecords(users.sort(usersSort));
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
setErrorSnackMessage(err);
|
||||
});
|
||||
}, [setLoading, setRecords, setErrorSnackMessage]);
|
||||
|
||||
const closeAddModalAndRefresh = () => {
|
||||
setAddScreenOpen(false);
|
||||
fetchRecords();
|
||||
};
|
||||
|
||||
const closeDeleteModalAndRefresh = (refresh: boolean) => {
|
||||
setDeleteOpen(false);
|
||||
if (refresh) {
|
||||
fetchRecords();
|
||||
}
|
||||
};
|
||||
|
||||
const closeAddGroupBulk = (unCheckAll: boolean = false) => {
|
||||
setAddGroupOpen(false);
|
||||
if (unCheckAll) {
|
||||
setCheckedUsers([]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchRecords();
|
||||
}, [fetchRecords]);
|
||||
|
||||
const filteredRecords = records.filter((elementItem) =>
|
||||
elementItem.accessKey.includes(filter)
|
||||
);
|
||||
|
||||
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const targetD = e.target;
|
||||
const value = targetD.value;
|
||||
const checked = targetD.checked;
|
||||
|
||||
let elements: string[] = [...checkedUsers]; // We clone the checkedUsers array
|
||||
|
||||
if (checked) {
|
||||
// If the user has checked this field we need to push this to checkedUsersList
|
||||
elements.push(value);
|
||||
} else {
|
||||
// User has unchecked this field, we need to remove it from the list
|
||||
elements = elements.filter((element) => element !== value);
|
||||
}
|
||||
|
||||
setCheckedUsers(elements);
|
||||
|
||||
return elements;
|
||||
};
|
||||
|
||||
const viewAction = (selectionElement: any): void => {
|
||||
setAddScreenOpen(true);
|
||||
setSelectedUser(selectionElement);
|
||||
};
|
||||
|
||||
const setPolicyAction = (selectionElement: any): void => {
|
||||
setPolicyOpen(true);
|
||||
setSelectedUser(selectionElement);
|
||||
};
|
||||
|
||||
const deleteAction = (selectionElement: any): void => {
|
||||
setDeleteOpen(true);
|
||||
setSelectedUser(selectionElement);
|
||||
};
|
||||
|
||||
const userLoggedIn = atob(localStorage.getItem("userLoggedIn") || "");
|
||||
|
||||
const tableActions = [
|
||||
{ type: "view", onClick: viewAction },
|
||||
{ type: "description", onClick: setPolicyAction },
|
||||
{
|
||||
type: "delete",
|
||||
onClick: deleteAction,
|
||||
disableButtonFunction: (topValue: any) => topValue === userLoggedIn,
|
||||
},
|
||||
];
|
||||
const connector = connect(mapState, { setMenuOpen });
|
||||
|
||||
const Users = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{addScreenOpen && (
|
||||
<AddUser
|
||||
open={addScreenOpen}
|
||||
selectedUser={selectedUser}
|
||||
closeModalAndRefresh={() => {
|
||||
closeAddModalAndRefresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{policyOpen && (
|
||||
<SetPolicy
|
||||
open={policyOpen}
|
||||
selectedUser={selectedUser}
|
||||
selectedGroup={null}
|
||||
closeModalAndRefresh={() => {
|
||||
setPolicyOpen(false);
|
||||
fetchRecords();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{deleteOpen && (
|
||||
<DeleteUser
|
||||
deleteOpen={deleteOpen}
|
||||
selectedUser={selectedUser}
|
||||
closeDeleteModalAndRefresh={(refresh: boolean) => {
|
||||
closeDeleteModalAndRefresh(refresh);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{addGroupOpen && (
|
||||
<AddToGroup
|
||||
open={addGroupOpen}
|
||||
checkedUsers={checkedUsers}
|
||||
closeModalAndRefresh={(close: boolean) => {
|
||||
closeAddGroupBulk(close);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<PageHeader label={"Users"} />
|
||||
<Grid container>
|
||||
<Grid item xs={12} className={classes.container}>
|
||||
<Grid item xs={12} className={classes.actionsTray}>
|
||||
<TextField
|
||||
placeholder="Search Users"
|
||||
className={classes.searchField}
|
||||
id="search-resource"
|
||||
label=""
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setFilter(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<GroupIcon />}
|
||||
disabled={checkedUsers.length <= 0}
|
||||
onClick={() => {
|
||||
if (checkedUsers.length > 0) {
|
||||
setAddGroupOpen(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add to Group
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<CreateIcon />}
|
||||
onClick={() => {
|
||||
setAddScreenOpen(true);
|
||||
setSelectedUser(null);
|
||||
}}
|
||||
>
|
||||
Create User
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TableWrapper
|
||||
itemActions={tableActions}
|
||||
columns={[{ label: "Access Key", elementKey: "accessKey" }]}
|
||||
onSelect={selectionChanged}
|
||||
selectedItems={checkedUsers}
|
||||
isLoading={loading}
|
||||
records={filteredRecords}
|
||||
entityName="Users"
|
||||
idField="accessKey"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Route path="/users/:userName" component={ViewUser} />
|
||||
<Route path="/" component={ListUsers} />
|
||||
<Route component={NotFoundPage} />
|
||||
</Switch>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setErrorSnackMessage,
|
||||
};
|
||||
|
||||
const connector = connect(null, mapDispatchToProps);
|
||||
|
||||
export default withStyles(styles)(connector(Users));
|
||||
export default withRouter(connector(Users));
|
||||
|
||||
340
portal-ui/src/screens/Console/Users/ViewUser.tsx
Normal file
340
portal-ui/src/screens/Console/Users/ViewUser.tsx
Normal file
@@ -0,0 +1,340 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 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 React, { Fragment, useCallback, useEffect, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { Button, Grid } from "@material-ui/core";
|
||||
import PageHeader from "../Common/PageHeader/PageHeader";
|
||||
import { CreateIcon } from "../../../icons";
|
||||
import {
|
||||
setErrorSnackMessage,
|
||||
setModalErrorSnackMessage,
|
||||
} from "../../../actions";
|
||||
import {
|
||||
actionsTray,
|
||||
containerForHeader,
|
||||
searchField,
|
||||
} from "../Common/FormComponents/common/styleLibrary";
|
||||
import { IPolicyItem } from "./types";
|
||||
import Tabs from "@material-ui/core/Tabs";
|
||||
import Tab from "@material-ui/core/Tab";
|
||||
import { TabPanel } from "../../shared/tabs";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import api from "../../../common/api";
|
||||
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
|
||||
import TableWrapper from "../Common/TableWrapper/TableWrapper";
|
||||
import ChangeUserGroups from "./ChangeUserGroups";
|
||||
import SetUserPolicies from "./SetUserPolicies";
|
||||
import { Bookmark } from "@material-ui/icons";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
seeMore: {
|
||||
marginTop: theme.spacing(3),
|
||||
},
|
||||
paper: {
|
||||
// padding: theme.spacing(2),
|
||||
display: "flex",
|
||||
overflow: "auto",
|
||||
flexDirection: "column",
|
||||
},
|
||||
addSideBar: {
|
||||
width: "320px",
|
||||
padding: "20px",
|
||||
},
|
||||
tableToolbar: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(0),
|
||||
},
|
||||
wrapCell: {
|
||||
maxWidth: "200px",
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word",
|
||||
},
|
||||
minTableHeader: {
|
||||
color: "#393939",
|
||||
"& tr": {
|
||||
"& th": {
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
fixedHeight: {
|
||||
height: 165,
|
||||
minWidth: 247,
|
||||
padding: "25px 28px",
|
||||
"& svg": {
|
||||
maxHeight: 18,
|
||||
},
|
||||
},
|
||||
paperContainer: {
|
||||
padding: 15,
|
||||
paddingLeft: 50,
|
||||
display: "flex",
|
||||
},
|
||||
gridContainer: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "auto auto",
|
||||
gridGap: 8,
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
"& div:not(.MuiCircularProgress-root)": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
"& div:nth-child(odd)": {
|
||||
justifyContent: "flex-end",
|
||||
fontWeight: 700,
|
||||
},
|
||||
"& div:nth-child(2n)": {
|
||||
minWidth: 150,
|
||||
},
|
||||
},
|
||||
...actionsTray,
|
||||
...searchField,
|
||||
...containerForHeader(theme.spacing(4)),
|
||||
});
|
||||
|
||||
interface IViewUserProps {
|
||||
classes: any;
|
||||
match: any;
|
||||
setErrorSnackMessage: typeof setErrorSnackMessage;
|
||||
}
|
||||
|
||||
function a11yProps(index: any) {
|
||||
return {
|
||||
id: `simple-tab-${index}`,
|
||||
"aria-controls": `simple-tabpanel-${index}`,
|
||||
};
|
||||
}
|
||||
|
||||
interface IGroupItem {
|
||||
group: string;
|
||||
}
|
||||
|
||||
const ViewUser = ({ classes, match }: IViewUserProps) => {
|
||||
const [curTab, setCurTab] = useState<number>(0);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [addGroupOpen, setAddGroupOpen] = useState<boolean>(false);
|
||||
const [policyOpen, setPolicyOpen] = useState<boolean>(false);
|
||||
const [addLoading, setAddLoading] = useState<boolean>(false);
|
||||
|
||||
const [enabled, setEnabled] = useState<boolean>(false);
|
||||
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
|
||||
const [currentGroups, setCurrentGroups] = useState<IGroupItem[]>([]);
|
||||
const [currentPolicies, setCurrentPolicies] = useState<IPolicyItem[]>([]);
|
||||
|
||||
const userName = match.params["userName"];
|
||||
|
||||
const getUserInformation = useCallback(() => {
|
||||
if (userName === "") {
|
||||
return null;
|
||||
}
|
||||
setLoading(true);
|
||||
api
|
||||
.invoke("GET", `/api/v1/users/${userName}`)
|
||||
.then((res) => {
|
||||
setAddLoading(false);
|
||||
const memberOf = res.memberOf || [];
|
||||
setSelectedGroups(memberOf);
|
||||
let currentGroups: IGroupItem[] = [];
|
||||
for (let group of memberOf) {
|
||||
currentGroups.push({
|
||||
group: group,
|
||||
});
|
||||
}
|
||||
setCurrentGroups(currentGroups);
|
||||
let currentPolicies: IPolicyItem[] = [];
|
||||
for (let policy of res.policy) {
|
||||
currentPolicies.push({
|
||||
policy: policy,
|
||||
});
|
||||
}
|
||||
setCurrentPolicies(currentPolicies);
|
||||
setEnabled(res.status === "enabled");
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setAddLoading(false);
|
||||
setLoading(false);
|
||||
setModalErrorSnackMessage(err);
|
||||
});
|
||||
}, [userName]);
|
||||
|
||||
const saveRecord = (isEnabled: boolean) => {
|
||||
if (addLoading) {
|
||||
return;
|
||||
}
|
||||
setAddLoading(true);
|
||||
api
|
||||
.invoke("PUT", `/api/v1/users/${userName}`, {
|
||||
status: isEnabled ? "enabled" : "disabled",
|
||||
groups: selectedGroups,
|
||||
})
|
||||
.then((_) => {
|
||||
setAddLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setAddLoading(false);
|
||||
setModalErrorSnackMessage(err);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getUserInformation();
|
||||
}, [getUserInformation]);
|
||||
|
||||
const userLoggedIn = atob(localStorage.getItem("userLoggedIn") || "");
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<PageHeader label={`User: ${userName}`} />
|
||||
{addGroupOpen && (
|
||||
<ChangeUserGroups
|
||||
open={addGroupOpen}
|
||||
selectedUser={userName}
|
||||
closeModalAndRefresh={() => {
|
||||
setAddGroupOpen(false);
|
||||
getUserInformation();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{policyOpen && (
|
||||
<SetUserPolicies
|
||||
open={policyOpen}
|
||||
selectedUser={userName}
|
||||
currentPolicies={currentPolicies}
|
||||
closeModalAndRefresh={() => {
|
||||
setPolicyOpen(false);
|
||||
getUserInformation();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Grid container>
|
||||
<Grid item xs={12} className={classes.container}>
|
||||
<Grid item xs={12}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item>
|
||||
<Paper className={classes.paperContainer}>
|
||||
<div className={classes.gridContainer}>
|
||||
<div>Enabled:</div>
|
||||
<div className={classes.capitalizeFirst}>
|
||||
<FormSwitchWrapper
|
||||
checked={enabled}
|
||||
value={"user_enabled"}
|
||||
id="user-status"
|
||||
name="user-status"
|
||||
disabled={userLoggedIn === userName}
|
||||
onChange={(e) => {
|
||||
setEnabled(e.target.checked);
|
||||
saveRecord(e.target.checked);
|
||||
}}
|
||||
switchOnly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid container item xs={12}>
|
||||
<Grid item xs={9}>
|
||||
<Tabs
|
||||
value={curTab}
|
||||
onChange={(e: React.ChangeEvent<{}>, newValue: number) => {
|
||||
setCurTab(newValue);
|
||||
}}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
aria-label="cluster-tabs"
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
>
|
||||
<Tab label="Groups" {...a11yProps(0)} />
|
||||
<Tab label="Policies" {...a11yProps(0)} />
|
||||
</Tabs>
|
||||
</Grid>
|
||||
<Grid item xs={3} className={classes.actionsTray}>
|
||||
{curTab === 0 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<CreateIcon />}
|
||||
size="medium"
|
||||
onClick={() => {
|
||||
setAddGroupOpen(true);
|
||||
}}
|
||||
>
|
||||
Add to Groups
|
||||
</Button>
|
||||
)}
|
||||
{curTab === 1 && (
|
||||
<Fragment>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<Bookmark />}
|
||||
size="medium"
|
||||
onClick={() => {
|
||||
setPolicyOpen(true);
|
||||
}}
|
||||
>
|
||||
Assign Policies
|
||||
</Button>
|
||||
</Fragment>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TabPanel index={0} value={curTab}>
|
||||
<TableWrapper
|
||||
// itemActions={userTableActions}
|
||||
columns={[{ label: "Name", elementKey: "group" }]}
|
||||
isLoading={loading}
|
||||
records={currentGroups}
|
||||
entityName="Groups"
|
||||
idField="group"
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel index={1} value={curTab}>
|
||||
<TableWrapper
|
||||
// itemActions={userTableActions}
|
||||
columns={[{ label: "Name", elementKey: "policy" }]}
|
||||
isLoading={loading}
|
||||
records={currentPolicies}
|
||||
entityName="Policies"
|
||||
idField="policy"
|
||||
/>
|
||||
</TabPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setErrorSnackMessage,
|
||||
};
|
||||
|
||||
const connector = connect(null, mapDispatchToProps);
|
||||
|
||||
export default withStyles(styles)(connector(ViewUser));
|
||||
@@ -22,10 +22,14 @@ export interface User {
|
||||
enabled: boolean;
|
||||
accessKey: string;
|
||||
secretKey: string;
|
||||
policy?: string;
|
||||
policy?: string[];
|
||||
}
|
||||
|
||||
export interface UsersList {
|
||||
users: User[];
|
||||
total_users: number;
|
||||
}
|
||||
|
||||
export interface IPolicyItem {
|
||||
policy: string;
|
||||
}
|
||||
|
||||
40
portal-ui/src/screens/shared/tabs.tsx
Normal file
40
portal-ui/src/screens/shared/tabs.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 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 React, { Fragment } from "react";
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: any;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export const TabPanel = (props: TabPanelProps) => {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`simple-tabpanel-${index}`}
|
||||
aria-labelledby={`simple-tab-${index}`}
|
||||
style={{ marginTop: "5px" }}
|
||||
{...other}
|
||||
>
|
||||
{value === index && <Fragment>{children}</Fragment>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -115,7 +115,7 @@ func listUsers(ctx context.Context, client MinioAdmin) ([]*models.User, error) {
|
||||
userElem := &models.User{
|
||||
AccessKey: accessKey,
|
||||
Status: string(user.Status),
|
||||
Policy: user.PolicyName,
|
||||
Policy: strings.Split(user.PolicyName, ","),
|
||||
MemberOf: user.MemberOf,
|
||||
}
|
||||
users = append(users, userElem)
|
||||
@@ -165,7 +165,7 @@ func addUser(ctx context.Context, client MinioAdmin, accessKey, secretKey *strin
|
||||
userRet := &models.User{
|
||||
AccessKey: *accessKey,
|
||||
MemberOf: nil,
|
||||
Policy: "",
|
||||
Policy: []string{},
|
||||
Status: "",
|
||||
}
|
||||
return userRet, nil
|
||||
@@ -250,7 +250,7 @@ func getUserInfoResponse(session *models.Principal, params admin_api.GetUserInfo
|
||||
userInformation := &models.User{
|
||||
AccessKey: params.Name,
|
||||
MemberOf: user.MemberOf,
|
||||
Policy: user.PolicyName,
|
||||
Policy: strings.Split(user.PolicyName, ","),
|
||||
Status: string(user.Status),
|
||||
}
|
||||
|
||||
@@ -333,10 +333,12 @@ func updateUserGroups(ctx context.Context, client MinioAdmin, user string, group
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policies := strings.Split(userInfo.PolicyName, ",")
|
||||
|
||||
userReturn := &models.User{
|
||||
AccessKey: user,
|
||||
MemberOf: userInfo.MemberOf,
|
||||
Policy: userInfo.PolicyName,
|
||||
Policy: policies,
|
||||
Status: string(userInfo.Status),
|
||||
}
|
||||
|
||||
@@ -492,18 +494,20 @@ func getListUsersWithAccessToBucketResponse(session *models.Principal, bucket st
|
||||
var retval []string
|
||||
seen := make(map[string]bool)
|
||||
for i := 0; i < len(users); i++ {
|
||||
policy, err := adminClient.getPolicy(ctx, users[i].Policy)
|
||||
if err == nil {
|
||||
parsedPolicy, err2 := parsePolicy(users[i].Policy, policy)
|
||||
if err2 == nil && policyMatchesBucket(parsedPolicy, bucket) {
|
||||
retval = append(retval, users[i].AccessKey)
|
||||
seen[users[i].AccessKey] = true
|
||||
for _, policyName := range users[i].Policy {
|
||||
policy, err := adminClient.getPolicy(ctx, policyName)
|
||||
if err == nil {
|
||||
parsedPolicy, err2 := parsePolicy(policyName, policy)
|
||||
if err2 == nil && policyMatchesBucket(parsedPolicy, bucket) {
|
||||
retval = append(retval, users[i].AccessKey)
|
||||
seen[users[i].AccessKey] = true
|
||||
}
|
||||
if err2 != nil {
|
||||
log.Println(err2)
|
||||
}
|
||||
} else {
|
||||
log.Println(err)
|
||||
}
|
||||
if err2 != nil {
|
||||
log.Println(err2)
|
||||
}
|
||||
} else {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package restapi
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
@@ -99,7 +100,7 @@ func TestListUsers(t *testing.T) {
|
||||
for _, b := range userMap {
|
||||
assert.Contains(mockUserMap, b.AccessKey)
|
||||
assert.Equal(string(mockUserMap[b.AccessKey].Status), b.Status)
|
||||
assert.Equal(mockUserMap[b.AccessKey].PolicyName, b.Policy)
|
||||
assert.Equal(mockUserMap[b.AccessKey].PolicyName, strings.Join(b.Policy, ","))
|
||||
assert.ElementsMatch(mockUserMap[b.AccessKey].MemberOf, []string{"group1", "group2"})
|
||||
}
|
||||
|
||||
|
||||
@@ -6684,7 +6684,10 @@ func init() {
|
||||
}
|
||||
},
|
||||
"policy": {
|
||||
"type": "string"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
@@ -13936,7 +13939,10 @@ func init() {
|
||||
}
|
||||
},
|
||||
"policy": {
|
||||
"type": "string"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
|
||||
@@ -2665,7 +2665,9 @@ definitions:
|
||||
accessKey:
|
||||
type: string
|
||||
policy:
|
||||
type: string
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
memberOf:
|
||||
type: array
|
||||
items:
|
||||
|
||||
Reference in New Issue
Block a user