Compare commits

...

12 Commits

Author SHA1 Message Date
Minio Trusted
2c14142e19 update to v0.4.1 2020-10-12 20:37:21 -07:00
Kaan Kabalak
1caa3f2ce8 Implement License page (#324)
* Implement License page

Fixes #320

* License Assets

* Fix endpoint tests

Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-10-12 11:56:15 -07:00
Alex
6501a4b13f First set of Modal style changes (#322)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-10-09 12:01:24 -07:00
Daniel Valdivia
2f51621e69 Get Tenant Secret From Tenant CR (#323)
We were assuming the Tenant Credentials Secret instead of reading it from it's .spec.credsSecret this commit addresses that
2020-10-09 11:51:02 -07:00
Cesar N
7e6e64c729 Add download objects api and integrate it with UI (#321) 2020-10-09 11:43:15 -07:00
Kaan Kabalak
9007c7dd14 Consolidate Remote Buckets and Replication modals (#317)
* Consolidate Remote Buckets and Replication modals

This commit consolidates Remote Buckets and Replication modals into a
single modal in the Add Replication Rule modal located in the Buckets
section

Fixes #301

* Remove Remote Buckets section

* Properly align tabs and button on Buckets page
2020-10-08 09:55:31 -07:00
Alex
850fd3e371 Changed buttons & search boxes styles (#318)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-10-06 21:37:13 -07:00
Daniel Valdivia
6d8f1c439e Handle Invalid User error coming from madmin (#314)
Right now we display `Internal Server Error` when invalid credentials are presneted, this makes it so we only present `Unauthorized`
2020-10-06 16:45:26 -07:00
Alex
7166717688 Changed styles for Login page (#316)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-10-06 16:37:25 -07:00
Cesar N
f91346dc5b Add retention mode and legal hold mode on list objects api (#312)
Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
2020-10-06 16:07:33 -07:00
Alex
dccdfb5533 Customization of Dashboard page & improved some styles (#315)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-10-05 21:30:52 -07:00
Daniel Valdivia
4f065bdedf Change Menu Order. (#313) 2020-10-05 20:48:10 -07:00
70 changed files with 3306 additions and 1453 deletions

View File

@@ -15,7 +15,7 @@ spec:
serviceAccountName: console-sa
containers:
- name: console
image: minio/console:v0.4.0
image: minio/console:v0.4.1
imagePullPolicy: "IfNotPresent"
args:
- server

View File

@@ -15,7 +15,7 @@ spec:
serviceAccountName: console-sa
containers:
- name: console
image: minio/console:v0.4.0
image: minio/console:v0.4.1
imagePullPolicy: "IfNotPresent"
env:
- name: CONSOLE_OPERATOR_MODE

View File

@@ -35,14 +35,41 @@ type BucketObject struct {
// content type
ContentType string `json:"content_type,omitempty"`
// expiration
Expiration string `json:"expiration,omitempty"`
// expiration rule id
ExpirationRuleID string `json:"expiration_rule_id,omitempty"`
// is delete marker
IsDeleteMarker bool `json:"is_delete_marker,omitempty"`
// is latest
IsLatest bool `json:"is_latest,omitempty"`
// last modified
LastModified string `json:"last_modified,omitempty"`
// legal hold status
LegalHoldStatus string `json:"legal_hold_status,omitempty"`
// name
Name string `json:"name,omitempty"`
// retention mode
RetentionMode string `json:"retention_mode,omitempty"`
// retention until date
RetentionUntilDate string `json:"retention_until_date,omitempty"`
// size
Size int64 `json:"size,omitempty"`
// user tags
UserTags map[string]string `json:"user_tags,omitempty"`
// version id
VersionID string `json:"version_id,omitempty"`
}
// Validate validates this bucket object

View File

@@ -42,6 +42,7 @@ var (
replication = "/replication"
objectBrowser = "/object-browser/:bucket?"
mainObjectBrowser = "/object-browser"
license = "/license"
)
type ConfigurationActionSet struct {
@@ -236,6 +237,12 @@ var objectBrowserActionSet = ConfigurationActionSet{
actions: iampolicy.NewActionSet(),
}
// licenseActionSet no actions needed for this module to work
var licenseActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(),
actions: iampolicy.NewActionSet(),
}
// endpointRules contains the mapping between endpoints and ActionSets, additional rules can be added here
var endpointRules = map[string]ConfigurationActionSet{
configuration: configurationActionSet,
@@ -256,6 +263,7 @@ var endpointRules = map[string]ConfigurationActionSet{
replication: replicationActionSet,
objectBrowser: objectBrowserActionSet,
mainObjectBrowser: objectBrowserActionSet,
license: licenseActionSet,
}
// operatorRules contains the mapping between endpoints and ActionSets for operator only mode

View File

@@ -50,7 +50,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
args: args{
[]string{"admin:ServerInfo"},
},
want: 4,
want: 5,
},
{
name: "policies endpoint",
@@ -63,7 +63,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:ListUserPolicies",
},
},
want: 4,
want: 5,
},
{
name: "all admin endpoints",
@@ -72,7 +72,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:*",
},
},
want: 15,
want: 16,
},
{
name: "all s3 endpoints",
@@ -81,7 +81,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 6,
want: 7,
},
{
name: "all admin and s3 endpoints",
@@ -91,7 +91,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 18,
want: 19,
},
{
name: "no endpoints",

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,67 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 368.999 192.934">
<defs>
<style>
.cls-1{opacity:0.35;}.cls-12,.cls-15,.cls-16,.cls-17,.cls-2,.cls-5,.cls-6,.cls-7,.cls-8{opacity:0.5;}.cls-10,.cls-11,.cls-12,.cls-13,.cls-14,.cls-15,.cls-16,.cls-17,.cls-3,.cls-4,.cls-5,.cls-6,.cls-7,.cls-9{fill:none;stroke:#707070;stroke-miterlimit:10;}.cls-4{stroke-width:1px;}.cls-10,.cls-11,.cls-5,.cls-9{stroke-width:1.2px;}.cls-5{stroke-dasharray:2.619
2.182;}.cls-12,.cls-15,.cls-16,.cls-17,.cls-5,.cls-6,.cls-7,.cls-8{isolation:isolate;}.cls-6{stroke-width:1.6px;stroke-dasharray:2.144
1.786;}.cls-7{stroke-width:1.6px;stroke-dasharray:2.23 1.858;}.cls-10{stroke-dasharray:2.646
2.204;}.cls-11{stroke-dasharray:2.585 2.154;}.cls-12{stroke-width:1.8px;stroke-dasharray:2.484
2.07;}.cls-13{stroke-dasharray:2.984 2.487;}.cls-14{stroke-dasharray:2.773
2.311;}.cls-16{stroke-width:1.8px;}.cls-17{stroke-width:1.8px;}
</style>
</defs>
<title>BG_Illustration</title>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<g id="BG_Illustration" data-name="BG Illustration" class="cls-1">
<g id="Group_118" data-name="Group 118" class="cls-2">
<path id="Path_56" data-name="Path 56" class="cls-3"
d="M211.5,140.678l-52.726,29.078L79.687,126.139V29.652L132.411.571,211.5,44.188Z"/>
<path id="Path_58" data-name="Path 58" class="cls-3"
d="M158.776,169.756V73.271L211.5,44.193,158.776,73.271,79.688,29.654"/>
<path id="Path_59" data-name="Path 59" class="cls-4" d="M84.681,41l69.1,38.11v79.3l-69.1-38.11Z"/>
<line id="Line_37" data-name="Line 37" class="cls-4" x1="106.25" y1="52.782" x2="106.25"
y2="132.086"/>
<line id="Line_38" data-name="Line 38" class="cls-4" x1="153.783" y1="92.327" x2="106.25"
y2="65.999"/>
<line id="Line_39" data-name="Line 39" class="cls-4" x1="153.783" y1="105.545" x2="106.25"
y2="79.217"/>
<line id="Line_40" data-name="Line 40" class="cls-4" x1="153.783" y1="118.762" x2="106.25"
y2="92.434"/>
<line id="Line_41" data-name="Line 41" class="cls-4" x1="153.783" y1="131.979" x2="106.25"
y2="105.651"/>
<line id="Line_42" data-name="Line 42" class="cls-4" x1="153.783" y1="145.197" x2="106.25"
y2="118.869"/>
<path id="Path_60" data-name="Path 60" class="cls-4"
d="M166.723,151.031l38.8-22.487V62.916L166.723,85.4Z"/>
</g>
<path id="Path_62" data-name="Path 62" class="cls-5" d="M117.106,148.062l-76.18,43.33"/>
<path id="Path_63" data-name="Path 63" class="cls-6" d="M271.394,167.271l-44.483,25.3"/>
<path id="Path_64" data-name="Path 64" class="cls-7" d="M190.722,155.708l61.951,36.031"/>
<path id="Path_65" data-name="Path 65" class="cls-5" d="M237.7,36.385l28.182,17.229"/>
<g id="Path_66" data-name="Path 66" class="cls-8">
<line class="cls-9" x1="362.563" y1="69.327" x2="361.42" y2="68.688"/>
<line class="cls-10" x1="359.496" y1="67.613" x2="305.418" y2="37.39"/>
<polyline class="cls-9" points="304.456 36.852 303.313 36.213 302.158 36.83"/>
<line class="cls-11" x1="300.258" y1="37.844" x2="213.418" y2="84.213"/>
<line class="cls-9" x1="212.468" y1="84.72" x2="211.313" y2="85.337"/>
</g>
<path id="Path_67" data-name="Path 67" class="cls-12"
d="M79.648,192.571,31.786,166.344h-.868l-23.579,14.2"/>
<g id="Path_68" data-name="Path 68" class="cls-8">
<line class="cls-3" x1="22.871" y1="84.641" x2="24.156" y2="83.867"/>
<line class="cls-13" x1="26.286" y1="82.584" x2="48.654" y2="69.113"/>
<polyline class="cls-3" points="49.719 68.471 51.004 67.698 52.307 68.441"/>
<line class="cls-14" x1="54.315" y1="69.585" x2="75.395" y2="81.606"/>
<line class="cls-3" x1="76.399" y1="82.178" x2="77.702" y2="82.921"/>
</g>
<circle id="Ellipse_11" data-name="Ellipse 11" class="cls-15" cx="4.092" cy="183.59" r="3.592"/>
<circle id="Ellipse_12" data-name="Ellipse 12" class="cls-15" cx="274.986" cy="165.477" r="3.592"/>
<ellipse id="Ellipse_13" data-name="Ellipse 13" class="cls-16" cx="364.957" cy="71.922" rx="3.592"
ry="2.904"/>
<circle id="Ellipse_14" data-name="Ellipse 14" class="cls-15" cx="19.279" cy="87.681" r="3.592"/>
<ellipse id="Ellipse_15" data-name="Ellipse 15" class="cls-17" cx="234.106" cy="32.58" rx="3.592"
ry="2.649"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -0,0 +1,75 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 from "react";
import { SvgIcon } from "@material-ui/core";
class AllBucketsIcon extends React.Component {
render() {
return (
<SvgIcon viewBox="0 0 15.834 17.375">
<defs>
<linearGradient
id="a"
y1="0.5"
x2="1"
y2="0.5"
gradientUnits="objectBoundingBox"
>
<stop offset="0.044" stopColor="#362585" />
<stop offset="0.301" stopColor="#281b6f" />
<stop offset="1" stopColor="#1e1560" />
</linearGradient>
</defs>
<g transform="translate(0 0.375)">
<circle
style={{ opacity: 0.1, fill: "url(#a)" }}
cx="6.625"
cy="6.625"
r="6.625"
transform="translate(0 3.75)"
/>
<g transform="translate(3.092)">
<ellipse
style={{
fill: "none",
stroke: "#707070",
strokeMiterlimit: 10,
strokeWidth: "0.75px",
}}
cx="6.183"
cy="1.244"
rx="6.183"
ry="1.244"
transform="translate(0)"
/>
<path
style={{
fill: "none",
stroke: "#707070",
strokeMiterlimit: 10,
strokeWidth: "0.75px",
}}
d="M-3722.174,1225.225l-1.687,10.292a.858.858,0,0,1-.578.669,12.182,12.182,0,0,1-3.918.647,12.187,12.187,0,0,1-3.894-.639.878.878,0,0,1-.6-.678q-.843-5.145-1.687-10.291"
transform="translate(3734.541 -1223.981)"
/>
</g>
</g>
</SvgIcon>
);
}
}
export default AllBucketsIcon;

View File

@@ -14,19 +14,24 @@
// 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/>.
export interface IRemoteBucketsResponse {
buckets: IRemoteBucket[];
total: number;
import React from "react";
import { SvgIcon } from "@material-ui/core";
class ConsoleIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
<g transform="translate(-518 -361)">
<path
d="M-126,0V10h10V0Zm1.5,8.5V2.95h7V8.5Z"
transform="translate(644 361)"
/>
<rect width="2" height="1" transform="translate(520.272 364.772)" />
</g>
</svg>
</SvgIcon>
);
}
}
export interface IRemoteBucket {
name: string;
accessKey: string;
secretKey: string;
sourceBucket: string;
targetURL: string;
targetBucket: string;
remoteARN: string;
status: string;
service: string;
}
export default ConsoleIcon;

View File

@@ -0,0 +1,73 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 from "react";
import { SvgIcon } from "@material-ui/core";
class EgressIcon extends React.Component {
render() {
return (
<SvgIcon viewBox="0 0 18.344 17.009">
<defs>
<linearGradient
id="a"
y1="0.5"
x2="1"
y2="0.5"
gradientUnits="objectBoundingBox"
>
<stop offset="0.044" stopColor="#362585" />
<stop offset="0.301" stopColor="#281b6f" />
<stop offset="1" stopColor="#1e1560" />
</linearGradient>
</defs>
<g transform="translate(0 0.25)">
<ellipse
style={{ opacity: 0.1, fill: "url(#a)" }}
cx="7.462"
cy="7.462"
rx="7.462"
ry="7.462"
transform="translate(0 1.835)"
/>
<rect
style={{
fill: "none",
stroke: "#707070",
strokeMiterlimit: 10,
strokeWidth: "0.5px",
}}
width="9.323"
height="9.323"
transform="translate(4.083)"
/>
<rect
style={{
fill: "none",
stroke: "#707070",
strokeMiterlimit: 10,
strokeWidth: "0.5px",
}}
width="8.223"
height="8.223"
transform="translate(9.871 5.307)"
/>
</g>
</SvgIcon>
);
}
}
export default EgressIcon;

View File

@@ -0,0 +1,53 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 from "react";
import { SvgIcon } from "@material-ui/core";
class HealIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10.014 9.993">
<path
className="a"
d="M9.162,5.971h0L8.192,5,9.346,3.846a2.257,2.257,0,0,0,0-3.192,2.311,2.311,0,0,0-3.192,0L5,1.808,4.029.837,3.846.654a2.311,2.311,0,0,0-3.192,0,2.257,2.257,0,0,0,0,3.192l.184.183h0L1.808,5,.654,6.154A2.257,2.257,0,0,0,3.846,9.346L5,8.192l.971.971.183.183A2.257,2.257,0,0,0,9.346,6.154Zm-2.29-4.6a1.27,1.27,0,0,1,1.757,0,1.242,1.242,0,0,1,0,1.757L7.475,4.283,5.717,2.525Zm-5.5,1.757A1.243,1.243,0,0,1,3.129,1.371l.183.183L1.555,3.312Zm1.757,5.5a1.27,1.27,0,0,1-1.757,0,1.242,1.242,0,0,1,0-1.757L2.525,5.717,4.283,7.475Zm2.843-.9-.254-.253L2.525,4.283l-.253-.254L4.029,2.272l.254.253L7.475,5.717l.253.254Zm2.657.9a1.271,1.271,0,0,1-1.757,0l-.183-.183L8.446,6.688l.183.183h0a1.241,1.241,0,0,1,0,1.757Z"
transform="translate(0.007 -0.014)"
/>
<circle
cx="0.5"
cy="0.5"
r="0.5"
transform="translate(4.507 4.486)"
/>
<circle
cx="0.5"
cy="0.5"
r="0.5"
transform="translate(3.507 3.486)"
/>
<circle
cx="0.5"
cy="0.5"
r="0.5"
transform="translate(5.507 5.486)"
/>
</svg>
</SvgIcon>
);
}
}
export default HealIcon;

View File

@@ -0,0 +1,47 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 from "react";
import { SvgIcon } from "@material-ui/core";
class LicenseIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 11">
<path fill="#fff" d="M11 11H0V2h11v9zM2 8v1h7V8zm0-3v1h5V5z"></path>
<g
fill="#07274a"
stroke="#fdfdfd"
strokeWidth="0.5"
transform="translate(7)"
>
<circle cx="3" cy="3" r="3" stroke="none"></circle>
<circle cx="3" cy="3" r="2.75" fill="none"></circle>
</g>
<path
fill="none"
stroke="#fff"
strokeWidth="0.5"
d="M8.73 2.794l.954.953 1.471-1.471"
></path>
</svg>
</SvgIcon>
);
}
}
export default LicenseIcon;

View File

@@ -0,0 +1,47 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 from "react";
import { SvgIcon } from "@material-ui/core";
class LogoutIcon extends React.Component {
render() {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12.122 10.571">
<g transform="translate(0 0.5)">
<path
style={{ fill: "none", stroke: "rgba(255,255,255,0.8)" }}
d="M4816.27,3755.205v-2.939h8.539v9.571h-8.539v-2.932"
transform="translate(-4813.187 -3752.266)"
/>
<path
style={{ fill: "none", stroke: "rgba(255,255,255,0.8)" }}
d="M4813.187,3757.052h8.081"
transform="translate(-4813.187 -3752.266)"
/>
<path
style={{ fill: "none", stroke: "rgba(255,255,255,0.8)" }}
d="M4806.5,3756.511l2.265,2.063-2.265,2.063"
transform="translate(-4800.808 -3753.863)"
/>
</g>
</svg>
</SvgIcon>
);
}
}
export default LogoutIcon;

View File

@@ -0,0 +1,64 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 from "react";
import { SvgIcon } from "@material-ui/core";
class UsageIcon extends React.Component {
render() {
return (
<SvgIcon viewBox="0 0 16.172 17.187">
<defs>
<linearGradient
id="a"
y1="0.5"
x2="1"
y2="0.5"
gradientUnits="objectBoundingBox"
>
<stop offset="0.044" stopColor="#362585" />
<stop offset="0.301" stopColor="#281b6f" />
<stop offset="1" stopColor="#1e1560" />
</linearGradient>
</defs>
<path
style={{
fill: "none",
stroke: "#707070",
strokeMiterlimit: 10,
strokeWidth: "0.5px",
}}
d="M-4778.1,2239.582v6.425h6.425"
transform="translate(4787.594 -2239.582)"
/>
<path
fill={"#707070"}
d="M-4784.238,2247.532v-.581c0-.027.009-.054.012-.081.039-.313.055-.632.121-.939a6.744,6.744,0,0,1,3.064-4.441,6.514,6.514,0,0,1,3.293-1.032,6.923,6.923,0,0,1,2.667.423,6.793,6.793,0,0,1,4.119,4.333,6.053,6.053,0,0,1,.279,1.337c.006.083.014.164.021.247v.86c-.011.131-.018.261-.032.392a6.494,6.494,0,0,1-.626,2.147,6.807,6.807,0,0,1-4.044,3.528,6.052,6.052,0,0,1-1.663.3,6.576,6.576,0,0,1-2.565-.325,6.73,6.73,0,0,1-3.947-3.451,6.627,6.627,0,0,1-.658-2.288C-4784.212,2247.816-4784.225,2247.674-4784.238,2247.532Zm13.025-.306c-.024-.309-.021-.661-.082-1a6.206,6.206,0,0,0-1.658-3.293,6.153,6.153,0,0,0-4.1-1.9,5.984,5.984,0,0,0-2.476.355,6.188,6.188,0,0,0-4.134,5.708,6.453,6.453,0,0,0,.228,1.881,6.127,6.127,0,0,0,1.984,3.052,6.046,6.046,0,0,0,3.806,1.445,6.043,6.043,0,0,0,1.235-.065,6.249,6.249,0,0,0,3.783-2.2,6.2,6.2,0,0,0,1.352-3.048C-4771.228,2247.863-4771.233,2247.563-4771.212,2247.226Z"
transform="translate(4786.834 -2240.452)"
/>
<ellipse
style={{ opacity: 0.1, fill: "url(#a)" }}
cx="6.151"
cy="6.151"
rx="6.151"
ry="6.151"
transform="translate(0 4.886)"
/>
</SvgIcon>
);
}
}
export default UsageIcon;

View File

@@ -38,6 +38,7 @@ import {
} from "../actions";
import { useDebounce } from "use-debounce";
import { MakeBucketRequest } from "../types";
import FormSwitchWrapper from "../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
const styles = (theme: Theme) =>
createStyles({
@@ -52,7 +53,12 @@ const styles = (theme: Theme) =>
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
quotaSizeContainer: {
flexGrow: 1,
},
sizeFactorContainer: {
flexGrow: 0,
maxWidth: 80,
marginLeft: 8,
alignSelf: "flex-start" as const,
},
@@ -182,7 +188,7 @@ const AddBucket = ({
/>
</Grid>
<Grid item xs={12}>
<CheckboxWrapper
<FormSwitchWrapper
value="versioned"
id="versioned"
name="versioned"
@@ -194,7 +200,7 @@ const AddBucket = ({
/>
</Grid>
<Grid item xs={12}>
<CheckboxWrapper
<FormSwitchWrapper
value="bucket_quota"
id="bucket_quota"
name="bucket_quota"
@@ -224,7 +230,7 @@ const AddBucket = ({
</Grid>
<Grid item xs={12}>
<div className={classes.multiContainer}>
<div>
<div className={classes.quotaSizeContainer}>
<InputBoxWrapper
type="number"
id="quota_size"
@@ -232,7 +238,7 @@ const AddBucket = ({
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
addBucketQuotaSize(e.target.value);
}}
label="Size"
label="Quota"
value={quotaSize}
required
min="1"
@@ -240,7 +246,7 @@ const AddBucket = ({
</div>
<div className={classes.sizeFactorContainer}>
<SelectWrapper
label=""
label="&nbsp;"
id="quota_unit"
name="quota_unit"
value={quotaUnit}

View File

@@ -33,9 +33,12 @@ import { CreateIcon } from "../../../../icons";
import { niceBytes } from "../../../../common/utils";
import { AppState } from "../../../../store";
import { connect } from "react-redux";
import { logMessageReceived, logResetMessages } from "../../Logs/actions";
import { addBucketOpen, addBucketReset } from "../actions";
import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary";
import {
actionsTray,
containerForHeader,
searchField,
} from "../../Common/FormComponents/common/styleLibrary";
import PageHeader from "../../Common/PageHeader/PageHeader";
const styles = (theme: Theme) =>
@@ -68,18 +71,8 @@ const styles = (theme: Theme) =>
},
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
});

View File

@@ -26,7 +26,6 @@ import {
LinearProgress,
} from "@material-ui/core";
import api from "../../../../../../common/api";
import { BucketObjectsList } from "../ListObjects/types";
import Typography from "@material-ui/core/Typography";
const styles = (theme: Theme) =>
@@ -68,13 +67,14 @@ class DeleteObject extends React.Component<
if (selectedObject.endsWith("/")) {
recursive = true;
}
this.setState({ deleteLoading: true }, () => {
api
.invoke(
"DELETE",
`/api/v1/buckets/${selectedBucket}/objects?path=${selectedObject}&recursive=${recursive}`
)
.then((res: BucketObjectsList) => {
.then((res: any) => {
this.setState(
{
deleteLoading: false,
@@ -142,10 +142,12 @@ class DeleteObject extends React.Component<
</Button>
<Button
onClick={() => {
this.removeRecord();
this.setState({ deleteError: "" }, () => {
this.removeRecord();
});
}}
color="secondary"
autoFocus
disabled={deleteLoading}
>
Delete
</Button>

View File

@@ -30,6 +30,13 @@ import { CreateIcon } from "../../.././../../../icons";
import { niceBytes } from "../../../../../../common/utils";
import Moment from "react-moment";
import DeleteObject from "./DeleteObject";
import {
actionsTray,
containerForHeader,
searchField,
} from "../../../../Common/FormComponents/common/styleLibrary";
import PageHeader from "../../../../Common/PageHeader/PageHeader";
import storage from "local-storage-fallback";
const styles = (theme: Theme) =>
createStyles({
@@ -61,18 +68,9 @@ const styles = (theme: Theme) =>
},
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
});
interface IListObjectsProps {
@@ -143,6 +141,38 @@ class ListObjects extends React.Component<
});
}
download(bucketName: string, objectName: string) {
var anchor = document.createElement("a");
document.body.appendChild(anchor);
const token: string = storage.getItem("token")!;
var xhr = new XMLHttpRequest();
xhr.open(
"GET",
`/api/v1/buckets/${bucketName}/objects/download?prefix=${objectName}`,
true
);
xhr.setRequestHeader("Authorization", `Bearer ${token}`);
xhr.responseType = "blob";
xhr.onload = function(e) {
if (this.status == 200) {
var blob = new Blob([this.response], {
type: "octet/stream",
});
var blobUrl = window.URL.createObjectURL(blob);
anchor.href = blobUrl;
anchor.download = objectName;
anchor.click();
window.URL.revokeObjectURL(blobUrl);
anchor.remove();
}
};
xhr.send();
}
bucketFilter(): void {}
render() {
@@ -163,7 +193,12 @@ class ListObjects extends React.Component<
this.setState({ deleteOpen: true, selectedObject: object });
};
const downloadObject = (object: string) => {
this.download(selectedBucket, object);
};
const tableActions = [
{ type: "download", onClick: downloadObject, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteObject, sendOnlyId: true },
];
@@ -191,58 +226,55 @@ class ListObjects extends React.Component<
}}
/>
)}
<PageHeader label="Objects" />
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Objects</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Objects"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
this.setState({
filterObjects: val.target.value,
});
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Name", elementKey: "name" },
{
label: "Last Modified",
elementKey: "last_modified",
renderFunction: displayParsedDate,
},
{
label: "Size",
elementKey: "size",
renderFunction: niceBytes,
},
]}
isLoading={loading}
entityName="Objects"
idField="name"
records={filteredRecords}
/>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Objects"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
this.setState({
filterObjects: val.target.value,
});
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Name", elementKey: "name" },
{
label: "Last Modified",
elementKey: "last_modified",
renderFunction: displayParsedDate,
},
{
label: "Size",
elementKey: "size",
renderFunction: niceBytes,
},
]}
isLoading={loading}
entityName="Objects"
idField="name"
records={filteredRecords}
/>
</Grid>
</Grid>
</Grid>
</React.Fragment>

View File

@@ -25,11 +25,7 @@ import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBo
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { Button, LinearProgress } from "@material-ui/core";
import api from "../../../../common/api";
import {
IRemoteBucket,
IRemoteBucketsResponse,
} from "../../RemoteBuckets/types";
import RemoteBucketsList from "../../RemoteBuckets/RemoteBuckets";
import { IRemoteBucket } from "../types";
interface IReplicationModal {
open: boolean;
@@ -64,42 +60,61 @@ const AddReplicationModal = ({
bucketName,
}: IReplicationModal) => {
const [addError, setAddError] = useState("");
const [loadingForm, setLoadingForm] = useState(true);
const [addLoading, setAddLoading] = useState(false);
const [remoteURL, setRemoteURL] = useState("");
const [source, setSource] = useState("");
const [target, setTarget] = useState("");
const [ARN, setARN] = useState("");
const [arnValues, setARNValues] = useState([]);
useEffect(() => {
if (addLoading) {
addRecord();
}
}, [addLoading]);
useEffect(() => {
if (loadingForm) {
getARNValues();
}
});
const [accessKey, setAccessKey] = useState("");
const [secretKey, setSecretKey] = useState("");
const [targetURL, setTargetURL] = useState("");
const [targetBucket, setTargetBucket] = useState("");
const [region, setRegion] = useState("");
const addRecord = () => {
const replicationInfo = {
destination_bucket: target,
arn: ARN,
const remoteBucketInfo = {
accessKey: accessKey,
secretKey: secretKey,
sourceBucket: bucketName,
targetURL: targetURL,
targetBucket: targetBucket,
region: region,
};
api
.invoke(
"POST",
`/api/v1/buckets/${bucketName}/replication`,
replicationInfo
)
.then((res) => {
setAddLoading(false);
setAddError("");
closeModalAndRefresh();
.invoke("POST", "/api/v1/remote-buckets", remoteBucketInfo)
.then(() => {
api
.invoke("GET", "/api/v1/remote-buckets")
.then((res: any) => {
const remoteBuckets = get(res, "buckets", []);
const remoteBucket = remoteBuckets.find(
(itemRemote: IRemoteBucket) => {
return itemRemote.sourceBucket === bucketName;
}
);
if (remoteBucket && remoteBucket.remoteARN) {
const remoteARN = remoteBucket.remoteARN;
const replicationInfo = {
destination_bucket: targetBucket,
arn: remoteARN,
};
api
.invoke(
"POST",
`/api/v1/buckets/${bucketName}/replication`,
replicationInfo
)
.then(() => {
setAddLoading(false);
setAddError("");
closeModalAndRefresh();
})
.catch((err) => {
setAddLoading(false);
setAddError(err);
});
}
})
.catch((err) => {
setAddError(err);
});
})
.catch((err) => {
setAddLoading(false);
@@ -107,23 +122,6 @@ const AddReplicationModal = ({
});
};
const getARNValues = () => {
api
.invoke("GET", "/api/v1/remote-buckets")
.then((res: any) => {
const remoteBuckets = get(res, "buckets", []);
const remoteARNS = remoteBuckets.map((itemRemote: IRemoteBucket) => {
return { label: itemRemote.remoteARN, value: itemRemote.remoteARN };
});
setLoadingForm(false);
setARNValues(remoteARNS);
})
.catch((err) => {
setLoadingForm(false);
});
};
return (
<ModalWrapper
modalOpen={open}
@@ -139,71 +137,96 @@ const AddReplicationModal = ({
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setAddLoading(true);
addRecord();
}}
>
{loadingForm && (
<Grid container>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="target"
name="target"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAccessKey(e.target.value);
}}
label="Access Key"
value={accessKey}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="target"
name="target"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSecretKey(e.target.value);
}}
label="Secret Key"
value={secretKey}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="target"
name="target"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTargetURL(e.target.value);
}}
placeholder="https://play.min.io:9000"
label="Target URL"
value={targetURL}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="target"
name="target"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTargetBucket(e.target.value);
}}
label="Target Bucket"
value={targetBucket}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="target"
name="target"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setRegion(e.target.value);
}}
label="Region"
value={region}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading}
>
Save
</Button>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
</Grid>
)}
{!loadingForm && (
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="target"
name="target"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTarget(e.target.value);
}}
label="Destination Bucket"
value={target}
/>
</Grid>
<Grid item xs={12}>
<SelectWrapper
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setARN(e.target.value as string);
}}
id="arn"
name="arn"
label={"ARN"}
value={ARN}
options={arnValues}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading}
>
Save
</Button>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
)}
)}
</Grid>
</form>
</ModalWrapper>
);

View File

@@ -515,51 +515,53 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={6}>
<Tabs
value={curTab}
onChange={(e: React.ChangeEvent<{}>, newValue: number) => {
this.setState({ curTab: newValue });
}}
indicatorColor="primary"
textColor="primary"
aria-label="cluster-tabs"
>
<Tab label="Events" {...a11yProps(0)} />
<Tab label="Replication" {...a11yProps(1)} />
</Tabs>
</Grid>
<Grid item xs={6} className={classes.actionsTray}>
{curTab === 0 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
size="medium"
onClick={() => {
this.setState({
addScreenOpen: true,
});
<Grid container item xs={12}>
<Grid item xs={6}>
<Tabs
value={curTab}
onChange={(e: React.ChangeEvent<{}>, newValue: number) => {
this.setState({ curTab: newValue });
}}
indicatorColor="primary"
textColor="primary"
aria-label="cluster-tabs"
>
Subscribe to Event
</Button>
)}
{curTab === 1 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
size="medium"
onClick={() => {
this.setState({
openSetReplication: true,
});
}}
>
Add Replication Rule
</Button>
)}
<Tab label="Events" {...a11yProps(0)} />
<Tab label="Replication" {...a11yProps(1)} />
</Tabs>
</Grid>
<Grid item xs={6} className={classes.actionsTray}>
{curTab === 0 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
size="medium"
onClick={() => {
this.setState({
addScreenOpen: true,
});
}}
>
Subscribe to Event
</Button>
)}
{curTab === 1 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
size="medium"
onClick={() => {
this.setState({
openSetReplication: true,
});
}}
>
Add Replication Rule
</Button>
)}
</Grid>
</Grid>
<Grid item xs={12}>
<TabPanel index={0} value={curTab}>

View File

@@ -79,3 +79,15 @@ export interface MakeBucketRequest {
versioning: boolean;
quota?: QuotaRequest;
}
export interface IRemoteBucket {
name: string;
accessKey: string;
secretKey: string;
sourceBucket: string;
targetURL: string;
targetBucket: string;
remoteARN: string;
status: string;
service: string;
}

View File

@@ -0,0 +1,200 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { InputLabel, Switch, Tooltip } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import { actionsTray, fieldBasic } from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
interface IFormSwitch {
label: string;
classes: any;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
value: string | boolean;
id: string;
name: string;
disabled?: boolean;
tooltip?: string;
index?: number;
checked: boolean;
}
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3),
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column",
paddingTop: 15,
boxShadow: "none",
},
addSideBar: {
width: "320px",
padding: "20px",
},
errorBlock: {
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold",
},
},
},
noFound: {
textAlign: "center",
padding: "10px 0",
},
tableContainer: {
maxHeight: 200,
},
stickyHeader: {
backgroundColor: "#fff",
},
actionsTitle: {
fontWeight: 600,
color: "#000",
fontSize: 16,
alignSelf: "center",
},
tableBlock: {
marginTop: 15,
},
filterField: {
width: 375,
fontWeight: 600,
"& .input": {
"&::placeholder": {
fontWeight: 600,
color: "#000",
},
},
},
wrapperContainer: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
borderBottom: "#9c9c9c 1px solid",
paddingBottom: 14,
marginBottom: 20,
},
...actionsTray,
...fieldBasic,
});
const StyledSwitch = withStyles({
root: {
alignItems: "flex-start",
height: 18,
padding: "0 12px",
display: "flex",
position: "relative",
},
switchBase: {
color: "#fff",
padding: 0,
top: "initial",
"&$checked": {
color: "#fff",
},
"&$checked + $track": {
backgroundColor: "#000",
opacity: 1,
height: 15,
},
},
checked: {},
track: {
height: 15,
backgroundColor: "#000",
opacity: 1,
padding: 0,
marginTop: 1.5,
},
thumb: {
backgroundColor: "#fff",
border: "#000 1px solid",
boxShadow: "none",
width: 18,
height: 18,
padding: 0,
marginLeft: 10,
},
})(Switch);
const FormSwitchWrapper = ({
label,
onChange,
value,
id,
name,
checked = false,
disabled = false,
tooltip = "",
classes,
}: IFormSwitch) => {
return (
<React.Fragment>
<Grid item xs={12} className={`${classes.wrapperContainer}`}>
{label !== "" && (
<InputLabel htmlFor={id} className={classes.inputLabel}>
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
)}
<div className={classes.textBoxContainer}>
<StyledSwitch
checked={checked}
onChange={onChange}
color="primary"
name={name}
inputProps={{ "aria-label": "primary checkbox" }}
disabled={disabled}
disableRipple
disableFocusRipple
disableTouchRipple
value={value}
/>
</div>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(FormSwitchWrapper);

View File

@@ -71,13 +71,16 @@ const styles = (theme: Theme) =>
const inputStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
borderColor: "#393939",
borderRadius: 0,
"&::before": {
borderColor: "#9c9c9c",
},
},
input: {
padding: "11px 20px",
padding: "15px 5px 10px",
color: "#393939",
fontSize: 14,
fontSize: 13,
fontWeight: 600,
},
error: {
color: "#b53b4b",
@@ -160,7 +163,6 @@ const InputBoxWrapper = ({
<InputField
id={id}
name={name}
variant="outlined"
fullWidth
value={value}
disabled={disabled}
@@ -172,6 +174,10 @@ const InputBoxWrapper = ({
error={error !== ""}
helperText={error}
placeholder={placeholder}
InputLabelProps={{
shrink: true,
}}
className={classes.inputRebase}
/>
</div>
</Grid>

View File

@@ -50,26 +50,25 @@ const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
inputLabel: {
...fieldBasic.inputLabel,
width: 215,
},
});
const SelectStyled = withStyles((theme: Theme) =>
createStyles({
root: {
lineHeight: 1,
"label + &": {
marginTop: theme.spacing(3),
},
},
input: {
borderRadius: 0,
position: "relative",
color: "#393939",
fontSize: 14,
padding: "11px 20px",
border: "1px solid #c4c4c4",
fontSize: 13,
fontWeight: 600,
padding: "15px 20px 10px 10px",
borderBottom: "1px solid #9c9c9c",
display: "flex",
alignItems: "center",
"&:hover": {
borderColor: "#393939",
},
@@ -106,7 +105,7 @@ const SelectWrapper = ({
)}
</InputLabel>
)}
<FormControl variant="outlined" fullWidth>
<FormControl fullWidth>
<Select
id={id}
name={name}

View File

@@ -18,28 +18,24 @@
export const fieldBasic = {
inputLabel: {
fontWeight: 500,
fontWeight: 600,
marginRight: 10,
width: 160,
fontSize: 14,
color: "#393939",
textAlign: "right" as const,
display: "flex",
textOverflow: "ellipsis",
fontSize: 15,
color: "#000",
textAlign: "left" as const,
overflow: "hidden",
justifyContent: "flex-end",
"& span": {
display: "flex",
alignItems: "center",
},
display: "flex",
},
fieldLabelError: {
paddingBottom: 22,
},
fieldContainer: {
display: "flex",
alignItems: "center",
marginBottom: 10,
marginBottom: 20,
},
tooltipContainer: {
marginLeft: 5,
@@ -66,17 +62,17 @@ export const tooltipHelper = {
};
const checkBoxBasic = {
width: 16,
height: 16,
borderRadius: 3,
width: 14,
height: 14,
borderRadius: 2,
};
export const checkboxIcons = {
unCheckedIcon: { ...checkBoxBasic, border: "1px solid #d0d0d0" },
unCheckedIcon: { ...checkBoxBasic, border: "1px solid #c3c3c3" },
checkedIcon: {
...checkBoxBasic,
border: "1px solid #201763",
backgroundColor: "#201763",
border: "1px solid #081C42",
backgroundColor: "#081C42",
},
};
@@ -89,9 +85,57 @@ export const containerForHeader = (bottomSpacing: any) => ({
fontSize: 14,
},
"& p": {
"& span": {
"& span:not(*[class*='smallUnit'])": {
fontSize: 16,
},
},
},
});
export const actionsTray = {
actionsTray: {
display: "flex",
justifyContent: "space-between",
"& button": {
flexGrow: 0,
marginLeft: 15,
},
},
};
export const searchField = {
searchField: {
flexGrow: 1,
marginRight: 30,
background: "#FFFFFF",
borderRadius: 5,
border: "#EAEDEE 1px solid",
display: "flex",
justifyContent: "center",
padding: "0 16px",
"& input": {
fontSize: 14,
color: "#000",
"&::placeholder": {
color: "#393939",
opacity: 1,
},
},
},
};
export const predefinedList = {
predefinedTitle: {
fontSize: 16,
fontWeight: 600,
color: "#000",
margin: "10px 0",
},
predefinedList: {
backgroundColor: "#eaeaea",
padding: "12px 10px",
color: "#393939",
fontSize: 12,
fontWeight: 600,
},
};

View File

@@ -16,7 +16,12 @@
import React from "react";
import { Dialog, DialogContent, DialogTitle } from "@material-ui/core";
import IconButton from "@material-ui/core/IconButton";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
createStyles,
makeStyles,
Theme,
withStyles,
} from "@material-ui/core/styles";
interface IModalProps {
classes: any;
@@ -31,16 +36,16 @@ const baseCloseLine = {
borderLeft: "2px solid #707070",
height: 33,
width: 1,
position: "absolute"
position: "absolute",
};
const styles = (theme: Theme) =>
createStyles({
dialogContainer: {
padding: "12px 26px 22px"
padding: "8px 15px 22px",
},
closeContainer: {
textAlign: "right"
textAlign: "right",
},
closeButton: {
width: 45,
@@ -48,40 +53,48 @@ const styles = (theme: Theme) =>
padding: 0,
backgroundColor: "initial",
"&:hover": {
backgroundColor: "initial"
backgroundColor: "initial",
},
"&:active": {
backgroundColor: "initial"
}
backgroundColor: "initial",
},
},
modalCloseIcon: {
fontSize: 35,
color: "#707070",
fontWeight: 300,
"&:hover": {
color: "#000"
}
color: "#000",
},
},
closeIcon: {
"&::before": {
...baseCloseLine,
transform: "rotate(45deg)"
transform: "rotate(45deg)",
},
"&::after": {
...baseCloseLine,
transform: "rotate(-45deg)"
transform: "rotate(-45deg)",
},
"&:hover::before, &:hover::after": {
borderColor: "#000"
borderColor: "#000",
},
width: 24,
height: 24,
display: "block",
position: "relative"
position: "relative",
},
titleClass: {
padding: "0px 24px 12px"
}
padding: "0px 50px 12px",
"& h2": {
fontWeight: 600,
color: "#000",
fontSize: 22,
},
},
modalContent: {
padding: "0 50px",
},
});
const ModalWrapper = ({
@@ -89,7 +102,7 @@ const ModalWrapper = ({
modalOpen,
title,
children,
classes
classes,
}: IModalProps) => {
return (
<Dialog
@@ -114,7 +127,9 @@ const ModalWrapper = ({
<DialogTitle id="alert-dialog-title" className={classes.titleClass}>
{title}
</DialogTitle>
<DialogContent>{children}</DialogContent>
<DialogContent className={classes.modalContent}>
{children}
</DialogContent>
</div>
</Dialog>
);

View File

@@ -22,6 +22,8 @@ import DeleteIcon from "./TableActionIcons/DeleteIcon";
import DescriptionIcon from "./TableActionIcons/DescriptionIcon";
import CloudIcon from "./TableActionIcons/CloudIcon";
import ConsoleIcon from "./TableActionIcons/ConsoleIcon";
import GetAppIcon from "@material-ui/icons/GetApp";
import SvgIcon from "@material-ui/core/SvgIcon";
import { Link } from "react-router-dom";
interface IActionButton {
@@ -48,6 +50,10 @@ const defineIcon = (type: string, selected: boolean) => {
return <CloudIcon active={selected} />;
case "console":
return <ConsoleIcon active={selected} />;
case "download":
return (
<SvgIcon component={GetAppIcon} fontSize="small" color="primary" />
);
}
return null;

View File

@@ -7,12 +7,13 @@ const PencilIcon = ({ active = false }: IIcon) => {
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
viewBox="0 0 10 11.429"
>
<path
fill={active ? selected : unSelected}
d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"
></path>
d="M-43.375,11.429-48.35,8.664l-5.025,2.764V0h10Z"
transform="translate(53.375)"
/>
</svg>
);
};

View File

@@ -2,5 +2,5 @@ export interface IIcon {
active: boolean;
}
export const unSelected = "#adadad";
export const selected = "#201763";
export const unSelected = "#081C42";
export const selected = "#081C42";

View File

@@ -102,6 +102,8 @@ const styles = (theme: Theme) =>
flexDirection: "column",
padding: "19px 38px",
minHeight: "200px",
boxShadow: "none",
border: "#e7e7e7 1px solid",
},
minTableHeader: {
color: "#393939",
@@ -119,7 +121,8 @@ const styles = (theme: Theme) =>
},
rowSelected: {
...rowText,
color: "#201763",
color: "#081C42",
fontWeight: 600,
},
paginatorContainer: {
display: "flex",

View File

@@ -27,7 +27,11 @@ import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import { configurationElements } from "../utils";
import { IConfigurationElement } from "../types";
import EditConfiguration from "../CustomForms/EditConfiguration";
import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary";
import {
actionsTray,
containerForHeader,
searchField,
} from "../../Common/FormComponents/common/styleLibrary";
import PageHeader from "../../Common/PageHeader/PageHeader";
interface IListConfiguration {
@@ -45,21 +49,11 @@ const styles = (theme: Theme) =>
keyName: {
marginLeft: 5,
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
iconText: {
lineHeight: "24px",
},
...searchField,
...actionsTray,
...containerForHeader(theme.spacing(4)),
});

View File

@@ -67,9 +67,9 @@ import { ISessionResponse } from "./types";
import { saveSessionResponse } from "./actions";
import TenantDetails from "./Tenants/TenantDetails/TenantDetails";
import { clearSession } from "../../common/utils";
import RemoteBuckets from "./RemoteBuckets/RemoteBuckets";
import ObjectBrowser from "./ObjectBrowser/ObjectBrowser";
import ListObjects from "./Buckets/ListBuckets/Objects/ListObjects/ListObjects";
import License from "./License/License";
function Copyright() {
return (
@@ -279,10 +279,6 @@ const Console = ({
component: Policies,
path: "/policies",
},
{
component: RemoteBuckets,
path: "/remote-buckets",
},
{
component: Trace,
path: "/trace",
@@ -327,6 +323,10 @@ const Console = ({
component: TenantDetails,
path: "/namespaces/:tenantNamespace/tenants/:tenantName",
},
{
component: License,
path: "/license",
},
];
const allowedRoutes = routes.filter((route: any) => allowedPages[route.path]);

View File

@@ -29,6 +29,9 @@ import { niceBytes } from "../../../common/utils";
import { LinearProgress } from "@material-ui/core";
import PageHeader from "../Common/PageHeader/PageHeader";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import AllBucketsIcon from "../../../icons/AllBucketsIcon";
import UsageIcon from "../../../icons/UsageIcon";
import EgressIcon from "../../../icons/EgressIcon";
const styles = (theme: Theme) =>
createStyles({
@@ -90,9 +93,13 @@ const styles = (theme: Theme) =>
boxShadow: "none",
},
fixedHeight: {
minHeight: 165,
height: 165,
minWidth: 247,
marginRight: 20,
padding: "25px 28px",
"& svg": {
maxHeight: 18,
},
},
consumptionValue: {
color: "#000000",
@@ -106,6 +113,32 @@ const styles = (theme: Theme) =>
notationContainer: {
display: "flex",
},
dashboardBG: {
width: 390,
height: 255,
zIndex: 500,
position: "absolute",
backgroundSize: "fill",
backgroundImage: "url(/images/BG_IllustrationDarker.svg)",
backgroundPosition: "right bottom",
right: 0,
bottom: 0,
backgroundRepeat: "no-repeat",
},
dashboardContainer: {
zIndex: 600,
position: "absolute",
},
elementTitle: {
fontWeight: 500,
color: "#777777",
fontSize: 14,
marginTop: -9,
},
smallUnit: {
fontSize: 20,
},
});
interface IDashboardProps {
@@ -141,7 +174,19 @@ const Dashboard = ({ classes }: IDashboardProps) => {
if (usage === undefined) {
return "0";
}
return niceBytes(usage);
const niceBytesUsage = niceBytes(usage).split(" ");
if (niceBytesUsage.length !== 2) {
return niceBytesUsage.join(" ");
}
return (
<React.Fragment>
{niceBytesUsage[0]}
<span className={classes.smallUnit}>{niceBytesUsage[1]}</span>
</React.Fragment>
);
};
const prettyNumber = (usage: number | undefined) => {
@@ -155,7 +200,8 @@ const Dashboard = ({ classes }: IDashboardProps) => {
return (
<React.Fragment>
<PageHeader label="Dashboard" />
<Grid container>
<div className={classes.dashboardBG} />
<Grid container className={classes.dashboardContainer}>
<Grid container spacing={3} className={classes.container}>
{error !== "" && <Grid container>{error}</Grid>}
{loading ? (
@@ -168,10 +214,12 @@ const Dashboard = ({ classes }: IDashboardProps) => {
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<ViewHeadlineIcon />
<AllBucketsIcon />
</Grid>
<Grid item>
<Typography variant="h6">All Buckets</Typography>
<Typography className={classes.elementTitle}>
All buckets
</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
@@ -181,10 +229,12 @@ const Dashboard = ({ classes }: IDashboardProps) => {
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<PieChartIcon />
<UsageIcon />
</Grid>
<Grid item>
<Typography variant="h6">Usage</Typography>
<Typography className={classes.elementTitle}>
Usage
</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
@@ -194,10 +244,13 @@ const Dashboard = ({ classes }: IDashboardProps) => {
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<NetworkCheckIcon />
<EgressIcon />
</Grid>
<Grid item>
<Typography variant="h6"> Total Objects</Typography>
<Typography className={classes.elementTitle}>
{" "}
Total Objects
</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>

View File

@@ -31,7 +31,11 @@ import AddGroup from "../Groups/AddGroup";
import DeleteGroup from "./DeleteGroup";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import SetPolicy from "../Policies/SetPolicy";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import {
actionsTray,
containerForHeader,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import PageHeader from "../Common/PageHeader/PageHeader";
interface IGroupsProps {
@@ -74,18 +78,8 @@ const styles = (theme: Theme) =>
},
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
});

View File

@@ -166,7 +166,7 @@ const UsersSelectors = ({
return (
<React.Fragment>
<Title>Members</Title>
<Title>Assign Users</Title>
{error !== "" ? <div>{error}</div> : <React.Fragment />}
<Grid item xs={12}>
<Paper className={classes.paper}>

View File

@@ -0,0 +1,335 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 from "react";
import clsx from "clsx";
import Grid from "@material-ui/core/Grid";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import CheckCircleIcon from "@material-ui/icons/CheckCircle";
import PageHeader from "../Common/PageHeader/PageHeader";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import { planDetails, planItems, planButtons } from "./utils";
const styles = (theme: Theme) =>
createStyles({
pageTitle: {
fontSize: 18,
marginBottom: 20,
},
paper: {
padding: "20px 52px 20px 28px",
},
tableContainer: {
marginLeft: 28,
},
detailsContainer: {
textAlign: "center",
paddingTop: 18,
paddingBottom: 12,
borderRadius: "3px 3px 0 0",
marginLeft: 8,
maxWidth: "calc(25% - 8px)",
},
detailsContainerBorder: {
border: "1px solid #e2e2e2",
borderBottom: 0,
},
detailsContainerBorderHighlighted: {
border: "1px solid #9a93ad",
borderBottom: 0,
},
detailsTitle: {
fontSize: 17,
fontWeight: 700,
marginBottom: 26,
},
detailsPrice: {
fontSize: 12,
fontWeight: 700,
marginBottom: 8,
},
detailsCapacityMax: {
minHeight: 28,
fontSize: 10,
fontWeight: 700,
marginBottom: 12,
padding: "0% 15%",
color: "#474747",
},
detailsCapacityMin: {
fontSize: 10,
},
itemContainer: {
height: 36,
borderTop: "1px solid #e5e5e5",
},
itemContainerDetail: {
height: 48,
borderTop: "1px solid #e5e5e5",
},
item: {
height: "100%",
borderLeft: "1px solid #e2e2e2",
borderRight: "1px solid #e2e2e2",
textAlign: "center",
fontSize: 10,
fontWeight: 700,
display: "flex",
alignItems: "center",
alignContent: "center",
marginLeft: 8,
maxWidth: "calc(25% - 8px)",
},
itemFirst: {
borderLeft: 0,
borderRight: 0,
},
itemHighlighted: {
borderLeft: "1px solid #9a93ad",
borderRight: "1px solid #9a93ad",
},
field: {
textAlign: "left",
fontWeight: 400,
},
checkIcon: {
height: 12,
color:
"transparent linear-gradient(90deg, #073052 0%, #081c42 100%) 0% 0% no-repeat padding-box",
},
buttonContainer: {
paddingTop: 8,
paddingBottom: 24,
height: "100%",
display: "flex",
justifyContent: "center",
borderRadius: "0 0 3px 3px",
border: "1px solid #e2e2e2",
borderTop: 0,
marginLeft: 8,
maxWidth: "calc(25% - 8px)",
},
buttonContainerBlank: {
border: 0,
},
buttonContainerHighlighted: {
border: "1px solid #9a93ad",
borderTop: 0,
},
button: {
textTransform: "none",
fontSize: 15,
fontWeight: 700,
},
...containerForHeader(theme.spacing(4)),
});
interface ILicense {
classes: any;
}
const License = ({ classes }: ILicense) => {
return (
<React.Fragment>
<PageHeader label="License" />
<Grid container>
<Grid item xs={12} className={classes.container}>
<Paper className={classes.paper}>
<Grid container>
<Grid item xs={12}>
<Typography
component="h2"
variant="h6"
className={classes.pageTitle}
>
Upgrade to commercial license
</Typography>
</Grid>
<Grid container item xs={12} className={classes.tableContainer}>
<Grid container item xs={12}>
<Grid item xs={3} className={classes.detailsContainer} />
{planDetails.map((details: any) => {
return (
<Grid
container
item
xs={3}
className={clsx(
classes.detailsContainer,
classes.detailsContainerBorder,
{
[classes.detailsContainerBorderHighlighted]:
details.title !== "Community",
}
)}
>
<Grid item xs={12} className={classes.detailsTitle}>
{details.title}
</Grid>
<Grid item xs={12} className={classes.detailsPrice}>
{details.price}
</Grid>
<Grid
item
xs={12}
className={classes.detailsCapacityMax}
>
{details.capacityMax || ""}
</Grid>
<Grid
item
xs={12}
className={classes.detailsCapacityMin}
>
{details.capacityMin}
</Grid>
</Grid>
);
})}
</Grid>
{planItems.map((item: any) => {
return (
<Grid
container
item
xs={12}
className={clsx(
classes.itemContainer,
item.communityDetail && classes.itemContainerDetail
)}
>
<Grid
item
xs={3}
className={clsx(
classes.item,
classes.field,
classes.itemFirst
)}
>
{item.field}
</Grid>
<Grid container item xs={3} className={classes.item}>
<Grid item xs={12}>
{item.community === "N/A" ? (
""
) : item.community === "Yes" ? (
<CheckCircleIcon className={classes.checkIcon} />
) : (
item.community
)}
</Grid>
{item.communityDetail !== undefined && (
<Grid item xs={12}>
{item.communityDetail}
</Grid>
)}
</Grid>
<Grid
container
item
xs={3}
className={clsx(classes.item, classes.itemHighlighted)}
>
<Grid item xs={12}>
{item.standard === "N/A" ? (
""
) : item.standard === "Yes" ? (
<CheckCircleIcon className={classes.checkIcon} />
) : (
item.standard
)}
</Grid>
{item.standardDetail !== undefined && (
<Grid item xs={12}>
{item.standardDetail}
</Grid>
)}
</Grid>
<Grid
container
item
xs={3}
className={clsx(classes.item, classes.itemHighlighted)}
>
<Grid item xs={12}>
{item.enterprise === "N/A" ? (
""
) : item.enterprise === "Yes" ? (
<CheckCircleIcon className={classes.checkIcon} />
) : (
item.enterprise
)}
</Grid>
{item.enterpriseDetail !== undefined && (
<Grid item xs={12}>
{item.enterpriseDetail}
</Grid>
)}
</Grid>
</Grid>
);
})}
<Grid container item xs={12}>
<Grid
item
xs={3}
className={clsx(
classes.buttonContainer,
classes.buttonContainerBlank
)}
/>
{planButtons.map((button: any) => {
return (
<Grid
container
item
xs={3}
className={clsx(classes.buttonContainer, {
[classes.buttonContainerHighlighted]:
button.text === "Subscribe",
})}
>
<Button
variant={
button.text === "Subscribe"
? "contained"
: "outlined"
}
color="primary"
className={classes.button}
target="_blank"
href={button.link}
>
{button.text}
</Button>
</Grid>
);
})}
</Grid>
</Grid>
</Grid>
</Paper>
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(License);

View File

@@ -0,0 +1,119 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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/>.
export const planDetails = [
{
title: "Community",
price: "Free",
capacityMin: "(No minimum)",
},
{
title: "Standard",
price: "$10/TB/month",
capacityMax: "Up to 10PB. No additional charges for capacity over 10PB",
capacityMin: "(25TB minimum)",
},
{
title: "Enterprise",
price: "$20/TB/month",
capacityMax: "Up to 5PB. No additional charges for capacity over 5PB",
capacityMin: "(100TB minimum)",
},
];
export const planItems = [
{
field: "License",
community: "100% Open Source",
communityDetail: "Apache License v2, GNU AGPL v3",
standard: "Dual License",
standardDetail: "Commercial + Open Source",
enterprise: "Dual License",
enterpriseDetail: "Commercial + Open Source",
},
{
field: "Software Release",
community: "Update to latest",
standard: "1 Year Long Term Support",
enterprise: "5 Years Long Term Support",
},
{
field: "SLA",
community: "No SLA",
standard: "<24 hours",
enterprise: "<1 hour",
},
{
field: "Support",
community: "Community:",
communityDetail: "Public Slack Channel + Github Issues",
standard: "24x7 L4 direct engineering",
standardDetail: "Support via SUBNET",
enterprise: "24x7 L4 direct engineering",
enterpriseDetail: "Support via SUBNET",
},
{
field: "Security Updates & Critical Bugs",
community: "Self Update",
standard: "Guided Update",
enterprise: "Guided Update",
},
{
field: "Panic Button",
community: "N/A",
standard: "1 per year",
enterprise: "Unlimited",
},
{
field: "Annual Architecture Review",
community: "N/A",
standard: "Yes",
enterprise: "Yes",
},
{
field: "Annual Performance Review",
community: "N/A",
standard: "Yes",
enterprise: "Yes",
},
{
field: "Indemnification",
community: "N/A",
standard: "N/A",
enterprise: "Yes",
},
{
field: "Security + Policy Review",
community: "N/A",
standard: "N/A",
enterprise: "Yes",
},
];
export const planButtons = [
{
text: "Slack Community",
link: "https://slack.min.io",
},
{
text: "Subscribe",
link: "https://min.io/pricing",
},
{
text: "Subscribe",
link: "https://min.io/pricing",
},
];

View File

@@ -21,7 +21,6 @@ import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import WebAssetIcon from "@material-ui/icons/WebAsset";
import HealingIcon from "@material-ui/icons/Healing";
import CloudUploadIcon from "@material-ui/icons/CloudUpload";
import DescriptionIcon from "@material-ui/icons/Description";
import FileCopyIcon from "@material-ui/icons/FileCopy";
import Collapse from "@material-ui/core/Collapse";
@@ -54,6 +53,10 @@ import {
WarpIcon,
} from "../../../icons";
import { clearSession } from "../../../common/utils";
import HealIcon from "../../../icons/HealIcon";
import ConsoleIcon from "../../../icons/ConsoleIcon";
import LicenseIcon from "../../../icons/LicenseIcon";
import LogoutIcon from "../../../icons/LogoutIcon";
const styles = (theme: Theme) =>
createStyles({
@@ -200,17 +203,25 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
group: "User",
type: "item",
component: NavLink,
to: "/service-accounts",
name: "Service Accounts",
icon: <ServiceAccountsIcon />,
to: "/object-browser",
name: "Object Browser",
icon: <DescriptionIcon />,
},
{
group: "User",
type: "item",
component: NavLink,
to: "/object-browser",
name: "Object Browser",
icon: <DescriptionIcon />,
to: "/service-accounts",
name: "Service Accounts",
icon: <ServiceAccountsIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/buckets",
name: "Buckets",
icon: <BucketsIcon />,
},
{
group: "Admin",
@@ -228,14 +239,6 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
name: "Groups",
icon: <GroupsIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/buckets",
name: "Buckets",
icon: <BucketsIcon />,
},
{
group: "Admin",
type: "item",
@@ -244,21 +247,13 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
name: "IAM Policies",
icon: <IAMPoliciesIcon />,
},
{
group: "Admin",
type: "item",
component: NavLink,
to: "/remote-buckets",
name: "Remote Buckets",
icon: <CloudUploadIcon />,
},
{
group: "Tools",
type: "item",
component: NavLink,
to: "/logs",
name: "Console Logs",
icon: <WebAssetIcon />,
name: "Logs",
icon: <ConsoleIcon />,
},
{
group: "Tools",
@@ -282,7 +277,7 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
component: NavLink,
to: "/heal",
name: "Heal",
icon: <HealingIcon />,
icon: <HealIcon />,
},
{
group: "Admin",
@@ -324,6 +319,14 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
name: "Warp",
icon: <WarpIcon />,
},
{
group: "License",
type: "item",
component: NavLink,
to: "/license",
name: "License",
icon: <LicenseIcon />,
},
];
const allowedPages = pages.reduce((result: any, item: any, index: any) => {
@@ -436,7 +439,7 @@ const Menu = ({ userLoggedIn, classes, pages }: IMenuProps) => {
<ListItem button onClick={logout}>
<ListItemIcon>
<ExitToApp />
<LogoutIcon />
</ListItemIcon>
<ListItemText primary="Logout" />
</ListItem>

View File

@@ -20,4 +20,5 @@ export const menuGroups = [
{ label: "Admin", group: "Admin", collapsible: true },
{ label: "Tools", group: "Tools", collapsible: true },
{ label: "Operator", group: "Operator", collapsible: true },
{ label: "", group: "License", collapsible: false },
];

View File

@@ -35,7 +35,11 @@ import api from "../../../common/api";
import FiberManualRecordIcon from "@material-ui/icons/FiberManualRecord";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import AddNotificationEndpoint from "./AddNotificationEndpoint";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import {
actionsTray,
containerForHeader,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import PageHeader from "../Common/PageHeader/PageHeader";
interface IListNotificationEndpoints {
@@ -53,21 +57,11 @@ const styles = (theme: Theme) =>
keyName: {
marginLeft: 5,
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
iconText: {
lineHeight: "24px",
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
});

View File

@@ -31,6 +31,10 @@ import { Bucket, BucketList } from "../Buckets/types";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import api from "../../../common/api";
import history from "../../../history";
import {
actionsTray,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
const styles = (theme: Theme) =>
createStyles({
@@ -62,18 +66,6 @@ const styles = (theme: Theme) =>
},
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
usedSpaceCol: {
width: 150,
},
@@ -81,6 +73,8 @@ const styles = (theme: Theme) =>
alignItems: "center",
display: "flex",
},
...actionsTray,
...searchField,
});
interface IBrowseBucketsProps {
@@ -162,10 +156,10 @@ const BrowseBuckets = ({ classes }: IBrowseBucketsProps) => {
/>
)}
<Grid container>
<Grid item xs={6} className={classes.subTitleLabel}>
<Grid item xs={2} className={classes.subTitleLabel}>
<Typography variant="h6">Buckets</Typography>
</Grid>
<Grid item xs={6} className={classes.actionsTray}>
<Grid item xs={10} className={classes.actionsTray}>
<TextField
placeholder="Search Buckets"
className={classes.searchField}

View File

@@ -41,6 +41,19 @@ const styles = (theme: Theme) =>
},
codeMirror: {
fontSize: 14,
"& .CodeMirror": {
color: "#fff",
backgroundColor: "#081C42",
},
"& .CodeMirror-gutter": {
backgroundColor: "#081C4280",
},
"& .CodeMirror-linenumber": {
color: "#000",
fontSize: 10,
height: 20,
lineHeight: "20px",
},
},
buttonContainer: {
textAlign: "right",

View File

@@ -30,7 +30,11 @@ import AddPolicy from "./AddPolicy";
import DeletePolicy from "./DeletePolicy";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import api from "../../../common/api";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import {
actionsTray,
containerForHeader,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import PageHeader from "../Common/PageHeader/PageHeader";
const styles = (theme: Theme) =>
@@ -63,18 +67,8 @@ const styles = (theme: Theme) =>
},
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
});

View File

@@ -1,209 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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, { useState, useEffect } from "react";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import api from "../../../common/api";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../Common/FormComponents/SelectWrapper/SelectWrapper";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
buttonContainer: {
textAlign: "right",
},
...modalBasic,
});
interface IAddBucketProps {
classes: any;
open: boolean;
closeModalAndRefresh: () => void;
}
const AddRemoteBucket = ({
classes,
open,
closeModalAndRefresh,
}: IAddBucketProps) => {
const [addLoading, setAddLoading] = useState(false);
const [addError, setAddError] = useState("");
const [accessKey, setAccessKey] = useState("");
const [secretKey, setSecretKey] = useState("");
const [sourceBucket, setSourceBucket] = useState("");
const [targetURL, setTargetURL] = useState("");
const [targetBucket, setTargetBucket] = useState("");
const [region, setRegion] = useState("");
useEffect(() => {
if (addLoading) {
addRecord();
}
}, [addLoading]);
const addRecord = () => {
const remoteBucketInfo = {
accessKey: accessKey,
secretKey: secretKey,
sourceBucket: sourceBucket,
targetURL: targetURL,
targetBucket: targetBucket,
region: region,
};
api
.invoke("POST", "/api/v1/remote-buckets", remoteBucketInfo)
.then((res) => {
setAddLoading(false);
setAddError("");
closeModalAndRefresh();
})
.catch((err) => {
setAddLoading(false);
setAddError(err);
});
};
return (
<ModalWrapper
title="Create Remote Bucket"
modalOpen={open}
onClose={() => {
setAddError("");
closeModalAndRefresh();
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
addRecord();
}}
>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="accessKey"
name="accessKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAccessKey(e.target.value);
}}
label="Access Key"
value={accessKey}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="secretKey"
name="secretKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSecretKey(e.target.value);
}}
label="Secret Key"
value={secretKey}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="sourceBucket"
name="sourceBucket"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSourceBucket(e.target.value);
}}
label="Source Bucket"
value={sourceBucket}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="targetURL"
name="targetURL"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTargetURL(e.target.value);
}}
placeholder="https://play.min.io:9000"
label="Target URL"
value={targetURL}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="targetBucket"
name="targetBucket"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTargetBucket(e.target.value);
}}
label="Target Bucket"
value={targetBucket}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="region"
name="region"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setRegion(e.target.value);
}}
label="Region"
value={region}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading}
>
Save
</Button>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
};
export default withStyles(styles)(AddRemoteBucket);

View File

@@ -1,137 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import get from "lodash/get";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@material-ui/core";
import api from "../../../common/api";
import Typography from "@material-ui/core/Typography";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
});
interface IDeleteEventProps {
classes: any;
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
bucketName: any;
sourceBucket: string;
}
interface IDeleteEventState {
deleteLoading: boolean;
deleteError: string;
}
const DeleteRemoteBucket = ({
deleteOpen,
closeDeleteModalAndRefresh,
classes,
bucketName,
sourceBucket,
}: IDeleteEventProps) => {
const [deleteError, setDeleteError] = useState("");
const [deleteLoading, setDeleteLoading] = useState(false);
useEffect(() => {
if (deleteLoading) {
removeRecord();
}
}, [deleteLoading]);
const removeRecord = () => {
api
.invoke("DELETE", `/api/v1/remote-buckets/${sourceBucket}/${bucketName}`)
.then(() => {
setDeleteLoading(false);
setDeleteError("");
closeDeleteModalAndRefresh(true);
})
.catch((err) => {
setDeleteLoading(false);
setDeleteError(err);
});
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
setDeleteError("");
closeDeleteModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete Remote Bucket</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete <strong>'{bucketName}'</strong> Remote
Bucket?
{deleteError !== "" && (
<React.Fragment>
<br />
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{deleteError}
</Typography>
</React.Fragment>
)}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
setDeleteError("");
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
onClick={() => {
setDeleteLoading(true);
}}
color="secondary"
autoFocus
>
Delete
</Button>
</DialogActions>
</Dialog>
);
};
export default withStyles(styles)(DeleteRemoteBucket);

View File

@@ -1,279 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import Moment from "react-moment";
import api from "../../../common/api";
import { Bucket } from "../Buckets/types";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import AddRemoteBucket from "./AddRemoteBucket";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import { CreateIcon } from "../../../icons";
import { IRemoteBucket, IRemoteBucketsResponse } from "./types";
import DeleteRemoteBucket from "./DeleteRemoteBucket";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import PageHeader from "../Common/PageHeader/PageHeader";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3),
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px",
},
errorBlock: {
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0),
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold",
},
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
...containerForHeader(theme.spacing(4)),
});
interface IRemoteListBucketsProps {
classes: any;
}
const RemoteBucketsList = ({ classes }: IRemoteListBucketsProps) => {
const [records, setRecords] = useState<IRemoteBucket[]>([]);
const [totalRecords, setTotalRecords] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
const [deleteScreenOpen, setDeleteOpen] = useState<boolean>(false);
const [page, setPage] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [selectedBucket, setSelectedBucket] = useState<IRemoteBucket>({
remoteARN: "",
accessKey: "",
name: "",
secretKey: "",
service: "",
sourceBucket: "",
status: "",
targetBucket: "",
targetURL: "",
});
const [filterBuckets, setFilterBuckets] = useState<string>("");
useEffect(() => {
if (loading) {
fetchRecords();
}
}, [loading]);
const closeAddModalAndRefresh = () => {
setAddScreenOpen(false);
setLoading(true);
};
const closeDeleteModalAndRefresh = (reload: boolean) => {
setDeleteOpen(false);
if (reload) {
setLoading(true);
}
};
const fetchRecords = () => {
const offset = page * rowsPerPage;
api
.invoke("GET", `/api/v1/remote-buckets`)
.then((res: IRemoteBucketsResponse) => {
setLoading(false);
setRecords(res.buckets || []);
setTotalRecords(!res.buckets ? 0 : res.total);
setError("");
// if we get 0 results, and page > 0 , go down 1 page
if (
(res.buckets === undefined ||
res.buckets == null ||
res.buckets.length === 0) &&
page > 0
) {
const newPage = page - 1;
setPage(newPage);
setLoading(true);
}
})
.catch((err: any) => {
setLoading(false);
setError(err);
});
};
const offset = page * rowsPerPage;
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
setPage(0);
setRowsPerPage(rPP);
};
const confirmDeleteRemoteBucket = (arnRemoteBucket: IRemoteBucket) => {
setSelectedBucket(arnRemoteBucket);
setDeleteOpen(true);
};
const tableActions = [{ type: "delete", onClick: confirmDeleteRemoteBucket }];
const filteredRecords = records
.slice(offset, offset + rowsPerPage)
.filter((b: IRemoteBucket) => {
if (filterBuckets === "") {
return true;
} else {
if (b.name.indexOf(filterBuckets) >= 0) {
return true;
} else {
return false;
}
}
});
return (
<React.Fragment>
{addScreenOpen && (
<AddRemoteBucket
open={addScreenOpen}
closeModalAndRefresh={() => {
closeAddModalAndRefresh();
}}
/>
)}
{deleteScreenOpen && (
<DeleteRemoteBucket
bucketName={selectedBucket.remoteARN}
sourceBucket={selectedBucket.sourceBucket}
closeDeleteModalAndRefresh={(reload) => {
closeDeleteModalAndRefresh(reload);
}}
deleteOpen={deleteScreenOpen}
/>
)}
<PageHeader label="Remote Buckets" />
<Grid container>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Remote Buckets"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterBuckets(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
}}
>
Create Remote Bucket
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Remote ARN", elementKey: "remoteARN" },
{ label: "Source Bucket", elementKey: "sourceBucket" },
{ label: "Target Bucket", elementKey: "targetBucket" },
{ label: "Status", elementKey: "status" },
]}
isLoading={loading}
records={filteredRecords}
entityName="Remote Buckets"
idField="remoteARN"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: totalRecords,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(RemoteBucketsList);

View File

@@ -32,7 +32,11 @@ import SearchIcon from "@material-ui/icons/Search";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import { stringSort } from "../../../utils/sortFunctions";
import PageHeader from "../Common/PageHeader/PageHeader";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import {
actionsTray,
containerForHeader,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
const styles = (theme: Theme) =>
createStyles({
@@ -75,18 +79,8 @@ const styles = (theme: Theme) =>
iconRoot: {
textAlign: "center",
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
});

View File

@@ -29,7 +29,7 @@ import TableRow from "@material-ui/core/TableRow";
import api from "../../../../common/api";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import CheckboxWrapper from "../../Common/FormComponents/CheckboxWrapper/CheckboxWrapper";
import FormSwitchWrapper from "../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import {
calculateDistribution,
@@ -1001,7 +1001,7 @@ const AddTenant = ({
</span>
<br />
<br />
<CheckboxWrapper
<FormSwitchWrapper
value="adv_mode"
id="adv_mode"
name="adv_mode"
@@ -1033,7 +1033,7 @@ const AddTenant = ({
</div>
<Grid item xs={12}>
<CheckboxWrapper
<FormSwitchWrapper
value="custom_dockerhub"
id="custom_dockerhub"
name="custom_dockerhub"
@@ -1083,7 +1083,7 @@ const AddTenant = ({
</React.Fragment>
)}
<Grid item xs={12}>
<CheckboxWrapper
<FormSwitchWrapper
value="enable_prometheus"
id="enable_prometheus"
name="enable_prometheus"
@@ -1198,7 +1198,7 @@ const AddTenant = ({
/>
</Grid>
<Grid item xs={12}>
<CheckboxWrapper
<FormSwitchWrapper
value="ad_skipTLS"
id="ad_skipTLS"
name="ad_skipTLS"
@@ -1213,7 +1213,7 @@ const AddTenant = ({
/>
</Grid>
<Grid item xs={12}>
<CheckboxWrapper
<FormSwitchWrapper
value="ad_serverInsecure"
id="ad_serverInsecure"
name="ad_serverInsecure"
@@ -1302,7 +1302,7 @@ const AddTenant = ({
<h3>Security</h3>
</div>
<Grid item xs={12}>
<CheckboxWrapper
<FormSwitchWrapper
value="enableTLS"
id="enableTLS"
name="enableTLS"
@@ -1425,7 +1425,7 @@ const AddTenant = ({
<span>How would you like to encrypt the information at rest.</span>
</div>
<Grid item xs={12}>
<CheckboxWrapper
<FormSwitchWrapper
value="enableEncryption"
id="enableEncryption"
name="enableEncryption"

View File

@@ -34,6 +34,8 @@ import { NewServiceAccount } from "../../Common/CredentialsPrompt/types";
import CredentialsPrompt from "../../Common/CredentialsPrompt/CredentialsPrompt";
import history from "../../../../history";
import RefreshIcon from "@material-ui/icons/Refresh";
import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary";
import PageHeader from "../../Common/PageHeader/PageHeader";
interface ITenantsList {
classes: any;
@@ -81,6 +83,7 @@ const styles = (theme: Theme) =>
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
...containerForHeader(theme.spacing(4)),
});
const ListTenants = ({ classes }: ITenantsList) => {
@@ -245,84 +248,81 @@ const ListTenants = ({ classes }: ITenantsList) => {
entity="Tenant"
/>
)}
<PageHeader label={"Users"} />
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Tenants</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<IconButton
color="primary"
aria-label="Refresh Tenant List"
component="span"
onClick={() => {
setIsLoading(true);
}}
>
<RefreshIcon />
</IconButton>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.actionsTray}>
<IconButton
color="primary"
aria-label="Refresh Tenant List"
component="span"
onClick={() => {
setIsLoading(true);
}}
>
<RefreshIcon />
</IconButton>
<TextField
placeholder="Search Tenants"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterTenants(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setCreateTenantOpen(true);
}}
>
Create Tenant
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Zones", elementKey: "zone_count" },
{ label: "State", elementKey: "currentState" },
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Tenants"
idField="name"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: filteredRecords.length,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
<TextField
placeholder="Search Tenants"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterTenants(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setCreateTenantOpen(true);
}}
>
Create Tenant
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Zones", elementKey: "zone_count" },
{ label: "State", elementKey: "currentState" },
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Tenants"
idField="name"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: filteredRecords.length,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>
</Grid>
</Grid>
</React.Fragment>

View File

@@ -18,7 +18,10 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, LinearProgress } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import {
modalBasic,
predefinedList,
} from "../Common/FormComponents/common/styleLibrary";
import api from "../../../common/api";
import GroupsSelectors from "./GroupsSelectors";
import Title from "../../../common/Title";
@@ -46,6 +49,7 @@ const styles = (theme: Theme) =>
textAlign: "right",
},
...modalBasic,
...predefinedList,
});
const AddToGroup = ({
@@ -121,10 +125,10 @@ const AddToGroup = ({
</Grid>
)}
<Grid item xs={12}>
<Title>Users to be altered</Title>
<Grid item xs={12} className={classes.predefinedTitle}>
Selected Users
</Grid>
<Grid item xs={12}>
<Grid item xs={12} className={classes.predefinedList}>
{checkedUsers.join(", ")}
</Grid>
<Grid item xs={12}>

View File

@@ -195,7 +195,7 @@ class AddUserContent extends React.Component<
this.props.closeModalAndRefresh();
}}
modalOpen={this.props.open}
title={selectedUser !== null ? "Edit User" : "Add User"}
title={selectedUser !== null ? "Edit User" : "Create User"}
>
<React.Fragment>
<form

View File

@@ -28,6 +28,7 @@ import { stringSort } from "../../../utils/sortFunctions";
import { GroupsList } from "../Groups/types";
import get from "lodash/get";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import { actionsTray } from "../Common/FormComponents/common/styleLibrary";
interface IGroupsProps {
classes: any;
@@ -41,10 +42,11 @@ const styles = (theme: Theme) =>
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column",
paddingTop: 15,
boxShadow: "none",
},
addSideBar: {
width: "320px",
@@ -70,20 +72,6 @@ const styles = (theme: Theme) =>
},
},
},
actionsTray: {
textAlign: "left",
"& button": {
marginLeft: 10,
},
},
filterField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
width: "100%",
zIndex: 500,
},
noFound: {
textAlign: "center",
padding: "10px 0",
@@ -94,6 +82,26 @@ const styles = (theme: Theme) =>
stickyHeader: {
backgroundColor: "#fff",
},
actionsTitle: {
fontWeight: 600,
color: "#000",
fontSize: 16,
alignSelf: "center",
},
tableBlock: {
marginTop: 15,
},
filterField: {
width: 375,
fontWeight: 600,
"& .input": {
"&::placeholder": {
fontWeight: 600,
color: "#000",
},
},
},
...actionsTray,
});
const GroupsSelectors = ({
@@ -164,7 +172,6 @@ const GroupsSelectors = ({
return (
<React.Fragment>
<Title>Groups</Title>
<Grid item xs={12}>
<Paper className={classes.paper}>
{loading && <LinearProgress />}
@@ -172,13 +179,13 @@ const GroupsSelectors = ({
{records != null && records.length > 0 ? (
<React.Fragment>
<Grid item xs={12} className={classes.actionsTray}>
<span className={classes.actionsTitle}>Assign Groups</span>
<TextField
placeholder="Filter Groups"
placeholder="Filter by Group"
className={classes.filterField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
@@ -190,7 +197,7 @@ const GroupsSelectors = ({
}}
/>
</Grid>
<Grid item xs={12}>
<Grid item xs={12} className={classes.tableBlock}>
<TableWrapper
columns={[{ label: "Group", elementKey: "" }]}
onSelect={selectionChanged}

View File

@@ -36,7 +36,11 @@ import AddToGroup from "./AddToGroup";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import DescriptionIcon from "@material-ui/icons/Description";
import SetPolicy from "../Policies/SetPolicy";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import {
actionsTray,
containerForHeader,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import PageHeader from "../Common/PageHeader/PageHeader";
const styles = (theme: Theme) =>
@@ -74,18 +78,8 @@ const styles = (theme: Theme) =>
},
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
});

View File

@@ -18,6 +18,7 @@ import React, { useEffect, useState } from "react";
import request from "superagent";
import storage from "local-storage-fallback";
import { connect, ConnectedProps } from "react-redux";
import ErrorIcon from "@material-ui/icons/Error";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Grid from "@material-ui/core/Grid";
@@ -30,22 +31,31 @@ import api from "../../common/api";
import { ILoginDetails, loginStrategyType } from "./types";
import { setSession } from "../../common/utils";
import history from "../../history";
import { Error } from "@material-ui/icons";
const styles = (theme: Theme) =>
createStyles({
"@global": {
body: {
backgroundColor: "#F4F4F4",
backgroundColor: "#FAFAFA",
},
},
paper: {
marginTop: theme.spacing(16),
borderRadius: "3px",
borderRadius: 8,
display: "flex",
flexDirection: "column",
alignItems: "center",
width: "800px",
width: 800,
height: 424,
margin: "auto",
position: "absolute",
top: "50%",
left: "50%",
marginLeft: -400,
marginTop: -212,
"&.MuiPaper-root": {
borderRadius: 8,
},
},
avatar: {
margin: theme.spacing(1),
@@ -53,36 +63,66 @@ const styles = (theme: Theme) =>
},
form: {
width: "100%", // Fix IE 11 issue.
marginTop: theme.spacing(3),
},
submit: {
margin: theme.spacing(3, 0, 2),
margin: "30px 0px 16px",
height: 40,
boxShadow: "none",
padding: "16px 30px",
},
errorBlock: {
color: "red",
backgroundColor: "#C72C48",
width: 800,
height: 64,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "absolute",
left: "50%",
top: "50%",
marginLeft: -400,
marginTop: -290,
color: "#fff",
fontWeight: 700,
fontSize: 14,
borderRadius: 8,
},
mainContainer: {
borderRadius: "3px",
position: "relative",
height: 424,
},
theOcean: {
borderTopLeftRadius: "3px",
borderBottomLeftRadius: "3px",
borderTopLeftRadius: 8,
borderBottomLeftRadius: 8,
background:
"transparent linear-gradient(333deg, #281B6F 1%, #271260 13%, #120D53 83%) 0% 0% no-repeat padding-box;",
"transparent linear-gradient(to bottom, #073052 0%,#05122b 100%); 0% 0% no-repeat padding-box;",
},
oceanBg: {
backgroundImage: "url(/images/BG_Illustration.svg)",
backgroundRepeat: "no-repeat",
backgroundPosition: "bottom left",
height: "100%",
width: "100%",
width: 324,
},
theLogin: {
padding: "76px 62px 20px 62px",
padding: "40px 45px 20px 45px",
},
loadingLoginStrategy: {
textAlign: "center",
},
headerTitle: {
marginBottom: 10,
},
submitContainer: {
textAlign: "right",
},
disclaimer: {
fontSize: 12,
marginTop: 30,
},
jwtInput: {
marginTop: 45,
},
});
const mapState = (state: SystemState) => ({
@@ -182,61 +222,60 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
case loginStrategyType.form: {
loginComponent = (
<React.Fragment>
<Typography component="h1" variant="h6">
Login
<Typography
component="h1"
variant="h6"
className={classes.headerTitle}
>
Console Login
</Typography>
<form className={classes.form} noValidate onSubmit={formSubmit}>
<Grid container spacing={2}>
{error !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{error}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<TextField
required
fullWidth
id="accessKey"
value={accessKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setAccessKey(e.target.value)
}
label="Access Key"
label="Enter Access Key"
name="accessKey"
autoComplete="username"
/>
</Grid>
<Grid item xs={12}>
<TextField
required
fullWidth
value={secretKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setSecretKey(e.target.value)
}
name="secretKey"
label="Secret Key"
label="Enter Secret Key"
type="password"
id="secretKey"
autoComplete="current-password"
/>
</Grid>
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
Login
</Button>
<Grid item xs={12} className={classes.submitContainer}>
<Button
type="submit"
variant="contained"
color="primary"
className={classes.submit}
disabled={secretKey === "" || accessKey === ""}
>
Login
</Button>
</Grid>
<Grid item xs={12} className={classes.disclaimer}>
<strong>Don't have an access key?</strong>
<br />
<br />
Contact your administrator to have one made
</Grid>
</form>
</React.Fragment>
);
@@ -245,7 +284,11 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
case loginStrategyType.redirect: {
loginComponent = (
<React.Fragment>
<Typography component="h1" variant="h6">
<Typography
component="h1"
variant="h6"
className={classes.headerTitle}
>
Login
</Typography>
<Button
@@ -255,7 +298,6 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
window.location.hostname
)}
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
@@ -269,23 +311,16 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
case loginStrategyType.serviceAccount: {
loginComponent = (
<React.Fragment>
<Typography component="h1" variant="h6">
Login
<Typography
component="h1"
variant="h6"
className={classes.headerTitle}
>
Operator Login
</Typography>
<form className={classes.form} noValidate onSubmit={formSubmit}>
<Grid container spacing={2}>
{error !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{error}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<Grid item xs={12} className={classes.jwtInput}>
<TextField
required
fullWidth
@@ -300,15 +335,22 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
/>
</Grid>
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
Login
</Button>
<Grid item xs={12} className={classes.submitContainer}>
<Button
type="submit"
variant="contained"
color="primary"
className={classes.submit}
disabled={jwt === ""}
>
Login
</Button>
</Grid>
<Grid item xs={12} className={classes.disclaimer}>
<strong>Don't have an access key?</strong>
<br />
Contact your administrator to have one made
</Grid>
</form>
</React.Fragment>
);
@@ -321,16 +363,24 @@ const Login = ({ classes, userLoggedIn }: ILoginProps) => {
}
return (
<Paper className={classes.paper}>
<Grid container className={classes.mainContainer}>
<Grid item xs={7} className={classes.theOcean}>
<div className={classes.oceanBg} />
<React.Fragment>
{error !== "" && (
<div className={classes.errorBlock}>
<ErrorIcon fontSize="small" />
&nbsp;{error}
</div>
)}
<Paper className={classes.paper}>
<Grid container className={classes.mainContainer}>
<Grid item xs={7} className={classes.theOcean}>
<div className={classes.oceanBg} />
</Grid>
<Grid item xs={5} className={classes.theLogin}>
{loginComponent}
</Grid>
</Grid>
<Grid item xs={5} className={classes.theLogin}>
{loginComponent}
</Grid>
</Grid>
</Paper>
</Paper>
</React.Fragment>
);
};

View File

@@ -3,9 +3,9 @@ import { createMuiTheme } from "@material-ui/core";
const theme = createMuiTheme({
palette: {
primary: {
light: "#757ce8",
main: "#201763",
dark: "#362585",
light: "#073052",
main: "#081C42",
dark: "#05122B",
contrastText: "#fff",
},
secondary: {
@@ -38,29 +38,50 @@ const theme = createMuiTheme({
fontFamily: ["Lato", "sans-serif"].join(","),
h1: {
fontWeight: "bold",
color: "#201763",
color: "#081C42",
},
h2: {
fontWeight: "bold",
color: "#201763",
color: "#081C42",
},
h3: {
fontWeight: "bold",
color: "#201763",
color: "#081C42",
},
h4: {
fontWeight: "bold",
color: "#201763",
color: "#081C42",
},
h5: {
fontWeight: "bold",
color: "#201763",
color: "#081C42",
},
h6: {
fontWeight: "bold",
color: "#000000",
},
},
overrides: {
MuiButton: {
root: {
borderRadius: 3,
color: "white",
height: 40,
padding: "0 20px",
fontSize: 14,
fontWeight: 600,
boxShadow: "none",
"& .MuiSvgIcon-root": {
maxHeight: 18,
},
"&.MuiButton-contained.Mui-disabled": {
backgroundColor: "#EAEDEE",
fontWeight: 600,
color: "#767676",
},
},
},
},
});
export default theme;

View File

@@ -230,9 +230,12 @@ func GetTenantServiceURL(mi *operator.Tenant) (svcURL string) {
return fmt.Sprintf("%s://%s", scheme, net.JoinHostPort(svc, strconv.Itoa(port)))
}
func getTenantAdminClient(ctx context.Context, client K8sClientI, namespace, tenantName, svcURL string, insecure bool) (*madmin.AdminClient, error) {
func getTenantAdminClient(ctx context.Context, client K8sClientI, tenant *operator.Tenant, svcURL string, insecure bool) (*madmin.AdminClient, error) {
if tenant == nil || tenant.Spec.CredsSecret == nil {
return nil, errors.New("invalid arguments")
}
// get admin credentials from secret
creds, err := client.getSecret(ctx, namespace, fmt.Sprintf("%s-secret", tenantName), metav1.GetOptions{})
creds, err := client.getSecret(ctx, tenant.Namespace, tenant.Spec.CredsSecret.Name, metav1.GetOptions{})
if err != nil {
return nil, err
}
@@ -677,7 +680,7 @@ func getTenantCreatedResponse(session *models.Principal, params admin_api.Create
return nil, prepareError(errorGeneric)
}
const consoleVersion = "minio/console:v0.4.0"
const consoleVersion = "minio/console:v0.4.1"
minInst.Spec.Console = &operator.ConsoleConfiguration{
Replicas: 1,
Image: consoleVersion,
@@ -1047,8 +1050,7 @@ func getTenantUsageResponse(session *models.Principal, params admin_api.GetTenan
mAdmin, err := getTenantAdminClient(
ctx,
k8sClient,
params.Namespace,
params.Tenant,
minTenant,
svcURL,
true)
if err != nil {

View File

@@ -89,8 +89,7 @@ func Test_TenantInfoTenantAdminClient(t *testing.T) {
type args struct {
ctx context.Context
client K8sClientI
namespace string
tenantName string
tenant v1.Tenant
serviceURL string
insecure bool
}
@@ -104,10 +103,15 @@ func Test_TenantInfoTenantAdminClient(t *testing.T) {
{
name: "Return Tenant Admin, no errors",
args: args{
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
ctx: ctx,
client: kClient,
tenant: v1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "tenant-1",
},
Spec: v1.TenantSpec{CredsSecret: &corev1.LocalObjectReference{Name: "secret-name"}},
},
serviceURL: "http://service-1.default.svc.cluster.local:80",
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
@@ -132,10 +136,14 @@ func Test_TenantInfoTenantAdminClient(t *testing.T) {
{
name: "Access key not stored on secrets",
args: args{
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
ctx: ctx,
client: kClient,
tenant: v1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "tenant-1",
},
},
serviceURL: "http://service-1.default.svc.cluster.local:80",
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
@@ -159,10 +167,14 @@ func Test_TenantInfoTenantAdminClient(t *testing.T) {
{
name: "Secret key not stored on secrets",
args: args{
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
ctx: ctx,
client: kClient,
tenant: v1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "tenant-1",
},
},
serviceURL: "http://service-1.default.svc.cluster.local:80",
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
@@ -186,10 +198,14 @@ func Test_TenantInfoTenantAdminClient(t *testing.T) {
{
name: "Handle error on getService",
args: args{
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
ctx: ctx,
client: kClient,
tenant: v1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "tenant-1",
},
},
serviceURL: "http://service-1.default.svc.cluster.local:80",
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
@@ -209,10 +225,14 @@ func Test_TenantInfoTenantAdminClient(t *testing.T) {
{
name: "Handle error on getSecret",
args: args{
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
ctx: ctx,
client: kClient,
tenant: v1.Tenant{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "tenant-1",
},
},
serviceURL: "http://service-1.default.svc.cluster.local:80",
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
@@ -233,7 +253,7 @@ func Test_TenantInfoTenantAdminClient(t *testing.T) {
k8sclientGetSecretMock = tt.mockGetSecret
k8sclientGetServiceMock = tt.mockGetService
t.Run(tt.name, func(t *testing.T) {
got, err := getTenantAdminClient(tt.args.ctx, tt.args.client, tt.args.namespace, tt.args.tenantName, tt.args.serviceURL, tt.args.insecure)
got, err := getTenantAdminClient(tt.args.ctx, tt.args.client, &tt.args.tenant, tt.args.serviceURL, tt.args.insecure)
if err != nil {
if tt.wantErr {
return
@@ -996,7 +1016,7 @@ func Test_UpdateTenantAction(t *testing.T) {
},
params: admin_api.UpdateTenantParams{
Body: &models.UpdateTenantRequest{
ConsoleImage: "minio/console:v0.4.0",
ConsoleImage: "minio/console:v0.4.1",
},
},
},

View File

@@ -19,7 +19,9 @@ package restapi
import (
"context"
"fmt"
"io"
"strings"
"time"
"github.com/minio/minio-go/v7/pkg/replication"
@@ -54,6 +56,8 @@ type MinioClient interface {
getBucketNotification(ctx context.Context, bucketName string) (config notification.Configuration, err error)
getBucketPolicy(ctx context.Context, bucketName string) (string, error)
listObjects(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo
getObjectRetention(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error)
getObjectLegalHold(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error)
}
// Interface implementation
@@ -116,6 +120,14 @@ func (c minioClient) listObjects(ctx context.Context, bucket string, opts minio.
return c.client.ListObjects(ctx, bucket, opts)
}
func (c minioClient) getObjectRetention(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error) {
return c.client.GetObjectRetention(ctx, bucketName, objectName, versionID)
}
func (c minioClient) getObjectLegalHold(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error) {
return c.client.GetObjectLegalHold(ctx, bucketName, objectName, opts)
}
// MCClient interface with all functions to be implemented
// by mock when testing, it should include all mc/S3Client respective api calls
// that are used within this project.
@@ -125,6 +137,7 @@ type MCClient interface {
watch(ctx context.Context, options mc.WatchOptions) (*mc.WatchObject, *probe.Error)
remove(ctx context.Context, isIncomplete, isRemoveBucket, isBypass bool, contentCh <-chan *mc.ClientContent) <-chan *probe.Error
list(ctx context.Context, opts mc.ListOptions) <-chan *mc.ClientContent
get(ctx context.Context, opts mc.GetOptions) (io.ReadCloser, *probe.Error)
}
// Interface implementation
@@ -161,6 +174,10 @@ func (c mcClient) list(ctx context.Context, opts mc.ListOptions) <-chan *mc.Clie
return c.client.List(ctx, opts)
}
func (c mcClient) get(ctx context.Context, opts mc.GetOptions) (io.ReadCloser, *probe.Error) {
return c.client.Get(ctx, opts)
}
// ConsoleCredentials interface with all functions to be implemented
// by mock when testing, it should include all needed consoleCredentials.Login api calls
// that are used within this project.

View File

@@ -364,6 +364,11 @@ func init() {
"type": "boolean",
"name": "recursive",
"in": "query"
},
{
"type": "boolean",
"name": "with_versions",
"in": "query"
}
],
"responses": {
@@ -424,6 +429,46 @@ func init() {
}
}
},
"/buckets/{bucket_name}/objects/download": {
"get": {
"produces": [
"application/octet-stream"
],
"tags": [
"UserAPI"
],
"summary": "Download Object",
"operationId": "Download Object",
"parameters": [
{
"type": "string",
"name": "bucket_name",
"in": "path",
"required": true
},
{
"type": "string",
"name": "prefix",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"type": "file"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/buckets/{bucket_name}/replication": {
"get": {
"tags": [
@@ -2570,15 +2615,45 @@ func init() {
"content_type": {
"type": "string"
},
"expiration": {
"type": "string"
},
"expiration_rule_id": {
"type": "string"
},
"is_delete_marker": {
"type": "boolean"
},
"is_latest": {
"type": "boolean"
},
"last_modified": {
"type": "string"
},
"legal_hold_status": {
"type": "string"
},
"name": {
"type": "string"
},
"retention_mode": {
"type": "string"
},
"retention_until_date": {
"type": "string"
},
"size": {
"type": "integer",
"format": "int64"
},
"user_tags": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"version_id": {
"type": "string"
}
}
},
@@ -4740,6 +4815,11 @@ func init() {
"type": "boolean",
"name": "recursive",
"in": "query"
},
{
"type": "boolean",
"name": "with_versions",
"in": "query"
}
],
"responses": {
@@ -4800,6 +4880,46 @@ func init() {
}
}
},
"/buckets/{bucket_name}/objects/download": {
"get": {
"produces": [
"application/octet-stream"
],
"tags": [
"UserAPI"
],
"summary": "Download Object",
"operationId": "Download Object",
"parameters": [
{
"type": "string",
"name": "bucket_name",
"in": "path",
"required": true
},
{
"type": "string",
"name": "prefix",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"type": "file"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/buckets/{bucket_name}/replication": {
"get": {
"tags": [
@@ -7469,15 +7589,45 @@ func init() {
"content_type": {
"type": "string"
},
"expiration": {
"type": "string"
},
"expiration_rule_id": {
"type": "string"
},
"is_delete_marker": {
"type": "boolean"
},
"is_latest": {
"type": "boolean"
},
"last_modified": {
"type": "string"
},
"legal_hold_status": {
"type": "string"
},
"name": {
"type": "string"
},
"retention_mode": {
"type": "string"
},
"retention_until_date": {
"type": "string"
},
"size": {
"type": "integer",
"format": "int64"
},
"user_tags": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"version_id": {
"type": "string"
}
}
},

View File

@@ -6,6 +6,7 @@ import (
"github.com/go-openapi/swag"
"github.com/minio/console/models"
"github.com/minio/minio/pkg/madmin"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
)
@@ -86,6 +87,11 @@ func prepareError(err ...error) *models.Error {
errorCode = 401
errorMessage = errorGenericInvalidSession.Error()
}
// console invalid session error
if madmin.ToErrorResponse(err[0]).Code == "XMinioAdminNoSuchUser" {
errorCode = 401
errorMessage = errorGenericInvalidSession.Error()
}
// if we received a second error take that as friendly message but dont override the code
if len(err) > 1 && err[1] != nil {
log.Print("friendly error: ", err[1].Error())

View File

@@ -126,6 +126,9 @@ func NewConsoleAPI(spec *loads.Document) *ConsoleAPI {
AdminAPIDeleteTenantHandler: admin_api.DeleteTenantHandlerFunc(func(params admin_api.DeleteTenantParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.DeleteTenant has not yet been implemented")
}),
UserAPIDownloadObjectHandler: user_api.DownloadObjectHandlerFunc(func(params user_api.DownloadObjectParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation user_api.DownloadObject has not yet been implemented")
}),
UserAPIGetBucketQuotaHandler: user_api.GetBucketQuotaHandlerFunc(func(params user_api.GetBucketQuotaParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation user_api.GetBucketQuota has not yet been implemented")
}),
@@ -364,6 +367,8 @@ type ConsoleAPI struct {
UserAPIDeleteServiceAccountHandler user_api.DeleteServiceAccountHandler
// AdminAPIDeleteTenantHandler sets the operation handler for the delete tenant operation
AdminAPIDeleteTenantHandler admin_api.DeleteTenantHandler
// UserAPIDownloadObjectHandler sets the operation handler for the download object operation
UserAPIDownloadObjectHandler user_api.DownloadObjectHandler
// UserAPIGetBucketQuotaHandler sets the operation handler for the get bucket quota operation
UserAPIGetBucketQuotaHandler user_api.GetBucketQuotaHandler
// UserAPIGetBucketReplicationHandler sets the operation handler for the get bucket replication operation
@@ -598,6 +603,9 @@ func (o *ConsoleAPI) Validate() error {
if o.AdminAPIDeleteTenantHandler == nil {
unregistered = append(unregistered, "admin_api.DeleteTenantHandler")
}
if o.UserAPIDownloadObjectHandler == nil {
unregistered = append(unregistered, "user_api.DownloadObjectHandler")
}
if o.UserAPIGetBucketQuotaHandler == nil {
unregistered = append(unregistered, "user_api.GetBucketQuotaHandler")
}
@@ -932,6 +940,10 @@ func (o *ConsoleAPI) initHandlerCache() {
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/buckets/{bucket_name}/objects/download"] = user_api.NewDownloadObject(o.context, o.UserAPIDownloadObjectHandler)
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/buckets/{name}/quota"] = user_api.NewGetBucketQuota(o.context, o.UserAPIGetBucketQuotaHandler)
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)

View File

@@ -0,0 +1,90 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 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/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
)
// DownloadObjectHandlerFunc turns a function with the right signature into a download object handler
type DownloadObjectHandlerFunc func(DownloadObjectParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn DownloadObjectHandlerFunc) Handle(params DownloadObjectParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// DownloadObjectHandler interface for that can handle valid download object params
type DownloadObjectHandler interface {
Handle(DownloadObjectParams, *models.Principal) middleware.Responder
}
// NewDownloadObject creates a new http.Handler for the download object operation
func NewDownloadObject(ctx *middleware.Context, handler DownloadObjectHandler) *DownloadObject {
return &DownloadObject{Context: ctx, Handler: handler}
}
/*DownloadObject swagger:route GET /buckets/{bucket_name}/objects/download UserAPI downloadObject
Download Object
*/
type DownloadObject struct {
Context *middleware.Context
Handler DownloadObjectHandler
}
func (o *DownloadObject) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
r = rCtx
}
var Params = NewDownloadObjectParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
r = aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -0,0 +1,124 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 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/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
)
// NewDownloadObjectParams creates a new DownloadObjectParams object
// no default values defined in spec.
func NewDownloadObjectParams() DownloadObjectParams {
return DownloadObjectParams{}
}
// DownloadObjectParams contains all the bound params for the download object operation
// typically these are obtained from a http.Request
//
// swagger:parameters Download Object
type DownloadObjectParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: path
*/
BucketName string
/*
Required: true
In: query
*/
Prefix string
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewDownloadObjectParams() beforehand.
func (o *DownloadObjectParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
qs := runtime.Values(r.URL.Query())
rBucketName, rhkBucketName, _ := route.Params.GetOK("bucket_name")
if err := o.bindBucketName(rBucketName, rhkBucketName, route.Formats); err != nil {
res = append(res, err)
}
qPrefix, qhkPrefix, _ := qs.GetOK("prefix")
if err := o.bindPrefix(qPrefix, qhkPrefix, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// bindBucketName binds and validates parameter BucketName from path.
func (o *DownloadObjectParams) bindBucketName(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// Parameter is provided by construction from the route
o.BucketName = raw
return nil
}
// bindPrefix binds and validates parameter Prefix from query.
func (o *DownloadObjectParams) bindPrefix(rawData []string, hasKey bool, formats strfmt.Registry) error {
if !hasKey {
return errors.Required("prefix", "query", rawData)
}
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// AllowEmptyValue: false
if err := validate.RequiredString("prefix", "query", raw); err != nil {
return err
}
o.Prefix = raw
return nil
}

View File

@@ -0,0 +1,132 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 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/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"io"
"net/http"
"github.com/go-openapi/runtime"
"github.com/minio/console/models"
)
// DownloadObjectOKCode is the HTTP code returned for type DownloadObjectOK
const DownloadObjectOKCode int = 200
/*DownloadObjectOK A successful response.
swagger:response downloadObjectOK
*/
type DownloadObjectOK struct {
/*
In: Body
*/
Payload io.ReadCloser `json:"body,omitempty"`
}
// NewDownloadObjectOK creates DownloadObjectOK with default headers values
func NewDownloadObjectOK() *DownloadObjectOK {
return &DownloadObjectOK{}
}
// WithPayload adds the payload to the download object o k response
func (o *DownloadObjectOK) WithPayload(payload io.ReadCloser) *DownloadObjectOK {
o.Payload = payload
return o
}
// SetPayload sets the payload to the download object o k response
func (o *DownloadObjectOK) SetPayload(payload io.ReadCloser) {
o.Payload = payload
}
// WriteResponse to the client
func (o *DownloadObjectOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(200)
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
/*DownloadObjectDefault Generic error response.
swagger:response downloadObjectDefault
*/
type DownloadObjectDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.Error `json:"body,omitempty"`
}
// NewDownloadObjectDefault creates DownloadObjectDefault with default headers values
func NewDownloadObjectDefault(code int) *DownloadObjectDefault {
if code <= 0 {
code = 500
}
return &DownloadObjectDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the download object default response
func (o *DownloadObjectDefault) WithStatusCode(code int) *DownloadObjectDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the download object default response
func (o *DownloadObjectDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the download object default response
func (o *DownloadObjectDefault) WithPayload(payload *models.Error) *DownloadObjectDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the download object default response
func (o *DownloadObjectDefault) SetPayload(payload *models.Error) {
o.Payload = payload
}
// WriteResponse to the client
func (o *DownloadObjectDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(o._statusCode)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}

View File

@@ -0,0 +1,127 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2020 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/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
"strings"
)
// DownloadObjectURL generates an URL for the download object operation
type DownloadObjectURL struct {
BucketName string
Prefix string
_basePath string
// avoid unkeyed usage
_ struct{}
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *DownloadObjectURL) WithBasePath(bp string) *DownloadObjectURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *DownloadObjectURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *DownloadObjectURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/buckets/{bucket_name}/objects/download"
bucketName := o.BucketName
if bucketName != "" {
_path = strings.Replace(_path, "{bucket_name}", bucketName, -1)
} else {
return nil, errors.New("bucketName is required on DownloadObjectURL")
}
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
qs := make(url.Values)
prefixQ := o.Prefix
if prefixQ != "" {
qs.Set("prefix", prefixQ)
}
_result.RawQuery = qs.Encode()
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *DownloadObjectURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *DownloadObjectURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *DownloadObjectURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on DownloadObjectURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on DownloadObjectURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *DownloadObjectURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -61,6 +61,10 @@ type ListObjectsParams struct {
In: query
*/
Recursive *bool
/*
In: query
*/
WithVersions *bool
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
@@ -89,6 +93,11 @@ func (o *ListObjectsParams) BindRequest(r *http.Request, route *middleware.Match
res = append(res, err)
}
qWithVersions, qhkWithVersions, _ := qs.GetOK("with_versions")
if err := o.bindWithVersions(qWithVersions, qhkWithVersions, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
@@ -149,3 +158,25 @@ func (o *ListObjectsParams) bindRecursive(rawData []string, hasKey bool, formats
return nil
}
// bindWithVersions binds and validates parameter WithVersions from query.
func (o *ListObjectsParams) bindWithVersions(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
return nil
}
value, err := swag.ConvertBool(raw)
if err != nil {
return errors.InvalidType("with_versions", "query", "bool", raw)
}
o.WithVersions = &value
return nil
}

View File

@@ -35,8 +35,9 @@ import (
type ListObjectsURL struct {
BucketName string
Prefix *string
Recursive *bool
Prefix *string
Recursive *bool
WithVersions *bool
_basePath string
// avoid unkeyed usage
@@ -95,6 +96,14 @@ func (o *ListObjectsURL) Build() (*url.URL, error) {
qs.Set("recursive", recursiveQ)
}
var withVersionsQ string
if o.WithVersions != nil {
withVersionsQ = swag.FormatBool(*o.WithVersions)
}
if withVersionsQ != "" {
qs.Set("with_versions", withVersionsQ)
}
_result.RawQuery = qs.Encode()
return &_result, nil

View File

@@ -19,16 +19,21 @@ package restapi
import (
"context"
"fmt"
"io"
"log"
"net/http"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
"github.com/minio/console/restapi/operations"
"github.com/minio/console/restapi/operations/user_api"
mc "github.com/minio/mc/cmd"
"github.com/minio/mc/pkg/probe"
"github.com/minio/minio-go/v7"
)
@@ -54,6 +59,22 @@ func registerObjectsHandlers(api *operations.ConsoleAPI) {
}
return user_api.NewDeleteObjectOK()
})
// download object
api.UserAPIDownloadObjectHandler = user_api.DownloadObjectHandlerFunc(func(params user_api.DownloadObjectParams, session *models.Principal) middleware.Responder {
resp, err := getDownloadObjectResponse(session, params)
if err != nil {
return user_api.NewDownloadObjectDefault(int(err.Code)).WithPayload(err)
}
return middleware.ResponderFunc(func(rw http.ResponseWriter, _ runtime.Producer) {
if _, err := io.Copy(rw, resp); err != nil {
log.Println(err)
} else {
if err := resp.Close(); err != nil {
log.Println(err)
}
}
})
})
}
// getListObjectsResponse returns a list of objects
@@ -62,12 +83,16 @@ func getListObjectsResponse(session *models.Principal, params user_api.ListObjec
defer cancel()
var prefix string
var recursive bool
var withVersions bool
if params.Prefix != nil {
prefix = *params.Prefix
}
if params.Recursive != nil {
recursive = *params.Recursive
}
if params.WithVersions != nil {
withVersions = *params.WithVersions
}
// bucket request needed to proceed
if params.BucketName == "" {
return nil, prepareError(errBucketNameNotInRequest)
@@ -80,7 +105,7 @@ func getListObjectsResponse(session *models.Principal, params user_api.ListObjec
// defining the client to be used
minioClient := minioClient{client: mClient}
objs, err := listBucketObjects(ctx, minioClient, params.BucketName, prefix, recursive)
objs, err := listBucketObjects(ctx, minioClient, params.BucketName, prefix, recursive, withVersions)
if err != nil {
return nil, prepareError(err)
}
@@ -93,23 +118,83 @@ func getListObjectsResponse(session *models.Principal, params user_api.ListObjec
}
// listBucketObjects gets an array of objects in a bucket
func listBucketObjects(ctx context.Context, client MinioClient, bucketName string, prefix string, recursive bool) ([]*models.BucketObject, error) {
func listBucketObjects(ctx context.Context, client MinioClient, bucketName string, prefix string, recursive, withVersions bool) ([]*models.BucketObject, error) {
var objects []*models.BucketObject
for lsObj := range client.listObjects(ctx, bucketName, minio.ListObjectsOptions{Prefix: prefix, Recursive: recursive}) {
for lsObj := range client.listObjects(ctx, bucketName, minio.ListObjectsOptions{Prefix: prefix, Recursive: recursive, WithVersions: withVersions}) {
if lsObj.Err != nil {
return nil, lsObj.Err
}
obj := &models.BucketObject{
Name: lsObj.Key,
Size: lsObj.Size,
LastModified: lsObj.LastModified.String(),
ContentType: lsObj.ContentType,
Name: lsObj.Key,
Size: lsObj.Size,
LastModified: lsObj.LastModified.String(),
ContentType: lsObj.ContentType,
VersionID: lsObj.VersionID,
IsLatest: lsObj.IsLatest,
IsDeleteMarker: lsObj.IsDeleteMarker,
UserTags: lsObj.UserTags,
}
if !lsObj.IsDeleteMarker {
// Add Legal Hold Status if available
legalHoldStatus, err := client.getObjectLegalHold(ctx, bucketName, lsObj.Key, minio.GetObjectLegalHoldOptions{VersionID: lsObj.VersionID})
if err != nil {
errResp := minio.ToErrorResponse(probe.NewError(err).ToGoError())
if errResp.Code != "NoSuchObjectLockConfiguration" {
log.Printf("error getting legal hold status for %s : %s", lsObj.VersionID, err)
}
} else {
if legalHoldStatus != nil {
obj.LegalHoldStatus = string(*legalHoldStatus)
}
}
// Add Retention Status if available
retention, retUntilDate, err := client.getObjectRetention(ctx, bucketName, lsObj.Key, lsObj.VersionID)
if err != nil {
errResp := minio.ToErrorResponse(probe.NewError(err).ToGoError())
if errResp.Code != "NoSuchObjectLockConfiguration" {
log.Printf("error getting retention status for %s : %s", lsObj.VersionID, err)
}
} else {
if retention != nil && retUntilDate != nil {
date := *retUntilDate
obj.RetentionMode = string(*retention)
obj.RetentionUntilDate = date.String()
}
}
}
objects = append(objects, obj)
}
return objects, nil
}
func getDownloadObjectResponse(session *models.Principal, params user_api.DownloadObjectParams) (io.ReadCloser, *models.Error) {
ctx := context.Background()
s3Client, err := newS3BucketClient(session, params.BucketName, params.Prefix)
if err != nil {
return nil, prepareError(err)
}
// create a mc S3Client interface implementation
// defining the client to be used
mcClient := mcClient{client: s3Client}
object, err := downloadObject(ctx, mcClient)
if err != nil {
return nil, prepareError(err)
}
return object, nil
}
func downloadObject(ctx context.Context, client MCClient) (io.ReadCloser, error) {
// TODO: handle version
// TODO: handle encripted files
var reader io.ReadCloser
reader, pErr := client.get(ctx, mc.GetOptions{})
if pErr != nil {
return nil, pErr.Cause
}
return reader, nil
}
// getDeleteObjectResponse returns whether there was an error on deletion of object
func getDeleteObjectResponse(session *models.Principal, params user_api.DeleteObjectParams) *models.Error {
ctx := context.Background()

View File

@@ -20,6 +20,7 @@ import (
"context"
"encoding/json"
"errors"
"io"
"reflect"
"testing"
"time"
@@ -31,33 +32,50 @@ import (
)
var minioListObjectsMock func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo
var minioGetObjectLegalHoldMock func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error)
var minioGetObjectRetentionMock func(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error)
var mcListMock func(ctx context.Context, opts mc.ListOptions) <-chan *mc.ClientContent
var mcRemoveMock func(ctx context.Context, isIncomplete, isRemoveBucket, isBypass bool, contentCh <-chan *mc.ClientContent) <-chan *probe.Error
var mcGetMock func(ctx context.Context, opts mc.GetOptions) (io.ReadCloser, *probe.Error)
// mock function of listObjects() needed for list objects
// mock functions for minioClientMock
func (ac minioClientMock) listObjects(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
return minioListObjectsMock(ctx, bucket, opts)
}
func (ac minioClientMock) getObjectLegalHold(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error) {
return minioGetObjectLegalHoldMock(ctx, bucketName, objectName, opts)
}
// implements mc.S3Client.List()
func (ac minioClientMock) getObjectRetention(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error) {
return minioGetObjectRetentionMock(ctx, bucketName, objectName, versionID)
}
// mock functions for s3ClientMock
func (c s3ClientMock) list(ctx context.Context, opts mc.ListOptions) <-chan *mc.ClientContent {
return mcListMock(ctx, opts)
}
// implements mc.S3Client.Remove()
func (c s3ClientMock) remove(ctx context.Context, isIncomplete, isRemoveBucket, isBypass bool, contentCh <-chan *mc.ClientContent) <-chan *probe.Error {
return mcRemoveMock(ctx, isIncomplete, isRemoveBucket, isBypass, contentCh)
}
func (c s3ClientMock) get(ctx context.Context, opts mc.GetOptions) (io.ReadCloser, *probe.Error) {
return mcGetMock(ctx, opts)
}
func Test_listObjects(t *testing.T) {
ctx := context.Background()
t1 := time.Now()
tretention := time.Now()
minClient := minioClientMock{}
type args struct {
bucketName string
prefix string
recursive bool
listFunc func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo
bucketName string
prefix string
recursive bool
withVersions bool
listFunc func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo
objectLegalHoldFunc func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error)
objectRetentionFunc func(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error)
}
tests := []struct {
test string
@@ -68,9 +86,10 @@ func Test_listObjects(t *testing.T) {
{
test: "Return objects",
args: args{
bucketName: "bucket1",
prefix: "prefix",
recursive: true,
bucketName: "bucket1",
prefix: "prefix",
recursive: true,
withVersions: false,
listFunc: func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
objectStatCh := make(chan minio.ObjectInfo, 1)
go func(objectStatCh chan<- minio.ObjectInfo) {
@@ -94,18 +113,32 @@ func Test_listObjects(t *testing.T) {
}(objectStatCh)
return objectStatCh
},
objectLegalHoldFunc: func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error) {
s := minio.LegalHoldEnabled
return &s, nil
},
objectRetentionFunc: func(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error) {
m := minio.Governance
return &m, &tretention, nil
},
},
expectedResp: []*models.BucketObject{
&models.BucketObject{
Name: "obj1",
LastModified: t1.String(),
Size: int64(1024),
ContentType: "content",
Name: "obj1",
LastModified: t1.String(),
Size: int64(1024),
ContentType: "content",
LegalHoldStatus: string(minio.LegalHoldEnabled),
RetentionMode: string(minio.Governance),
RetentionUntilDate: tretention.String(),
}, &models.BucketObject{
Name: "obj2",
LastModified: t1.String(),
Size: int64(512),
ContentType: "content",
Name: "obj2",
LastModified: t1.String(),
Size: int64(512),
ContentType: "content",
LegalHoldStatus: string(minio.LegalHoldEnabled),
RetentionMode: string(minio.Governance),
RetentionUntilDate: tretention.String(),
},
},
wantError: nil,
@@ -113,14 +146,23 @@ func Test_listObjects(t *testing.T) {
{
test: "Return zero objects",
args: args{
bucketName: "bucket1",
prefix: "prefix",
recursive: true,
bucketName: "bucket1",
prefix: "prefix",
recursive: true,
withVersions: false,
listFunc: func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
objectStatCh := make(chan minio.ObjectInfo, 1)
defer close(objectStatCh)
return objectStatCh
},
objectLegalHoldFunc: func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error) {
s := minio.LegalHoldEnabled
return &s, nil
},
objectRetentionFunc: func(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error) {
m := minio.Governance
return &m, &tretention, nil
},
},
expectedResp: nil,
wantError: nil,
@@ -128,9 +170,10 @@ func Test_listObjects(t *testing.T) {
{
test: "Handle error if present on object",
args: args{
bucketName: "bucket1",
prefix: "prefix",
recursive: true,
bucketName: "bucket1",
prefix: "prefix",
recursive: true,
withVersions: false,
listFunc: func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
objectStatCh := make(chan minio.ObjectInfo, 1)
go func(objectStatCh chan<- minio.ObjectInfo) {
@@ -151,16 +194,175 @@ func Test_listObjects(t *testing.T) {
}(objectStatCh)
return objectStatCh
},
objectLegalHoldFunc: func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error) {
s := minio.LegalHoldEnabled
return &s, nil
},
objectRetentionFunc: func(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error) {
m := minio.Governance
return &m, &tretention, nil
},
},
expectedResp: nil,
wantError: errors.New("error here"),
},
{
// Description: deleted objects with IsDeleteMarker
// should not call legsalhold or retention funcs
test: "Return deleted objects",
args: args{
bucketName: "bucket1",
prefix: "prefix",
recursive: true,
withVersions: false,
listFunc: func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
objectStatCh := make(chan minio.ObjectInfo, 1)
go func(objectStatCh chan<- minio.ObjectInfo) {
defer close(objectStatCh)
for _, bucket := range []minio.ObjectInfo{
minio.ObjectInfo{
Key: "obj1",
LastModified: t1,
Size: int64(1024),
ContentType: "content",
IsDeleteMarker: true,
},
minio.ObjectInfo{
Key: "obj2",
LastModified: t1,
Size: int64(512),
ContentType: "content",
},
} {
objectStatCh <- bucket
}
}(objectStatCh)
return objectStatCh
},
objectLegalHoldFunc: func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error) {
s := minio.LegalHoldEnabled
return &s, nil
},
objectRetentionFunc: func(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error) {
m := minio.Governance
return &m, &tretention, nil
},
},
expectedResp: []*models.BucketObject{
&models.BucketObject{
Name: "obj1",
LastModified: t1.String(),
Size: int64(1024),
ContentType: "content",
IsDeleteMarker: true,
}, &models.BucketObject{
Name: "obj2",
LastModified: t1.String(),
Size: int64(512),
ContentType: "content",
LegalHoldStatus: string(minio.LegalHoldEnabled),
RetentionMode: string(minio.Governance),
RetentionUntilDate: tretention.String(),
},
},
wantError: nil,
},
{
// Description: deleted objects with
// error on legalhold and retention funcs
// should only log errors
test: "Return deleted objects, error on legalhold and retention",
args: args{
bucketName: "bucket1",
prefix: "prefix",
recursive: true,
withVersions: false,
listFunc: func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
objectStatCh := make(chan minio.ObjectInfo, 1)
go func(objectStatCh chan<- minio.ObjectInfo) {
defer close(objectStatCh)
for _, bucket := range []minio.ObjectInfo{
minio.ObjectInfo{
Key: "obj1",
LastModified: t1,
Size: int64(1024),
ContentType: "content",
},
} {
objectStatCh <- bucket
}
}(objectStatCh)
return objectStatCh
},
objectLegalHoldFunc: func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error) {
return nil, errors.New("error legal")
},
objectRetentionFunc: func(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error) {
return nil, nil, errors.New("error retention")
},
},
expectedResp: []*models.BucketObject{
&models.BucketObject{
Name: "obj1",
LastModified: t1.String(),
Size: int64(1024),
ContentType: "content",
},
},
wantError: nil,
},
{
// Description: deleted objects with
// error on legalhold and retention funcs
// should only log errors
test: "Return deleted objects, error on legalhold and retention",
args: args{
bucketName: "bucket1",
prefix: "prefix",
recursive: true,
withVersions: false,
listFunc: func(ctx context.Context, bucket string, opts minio.ListObjectsOptions) <-chan minio.ObjectInfo {
objectStatCh := make(chan minio.ObjectInfo, 1)
go func(objectStatCh chan<- minio.ObjectInfo) {
defer close(objectStatCh)
for _, bucket := range []minio.ObjectInfo{
minio.ObjectInfo{
Key: "obj1",
LastModified: t1,
Size: int64(1024),
ContentType: "content",
},
} {
objectStatCh <- bucket
}
}(objectStatCh)
return objectStatCh
},
objectLegalHoldFunc: func(ctx context.Context, bucketName, objectName string, opts minio.GetObjectLegalHoldOptions) (status *minio.LegalHoldStatus, err error) {
return nil, errors.New("error legal")
},
objectRetentionFunc: func(ctx context.Context, bucketName, objectName, versionID string) (mode *minio.RetentionMode, retainUntilDate *time.Time, err error) {
return nil, nil, errors.New("error retention")
},
},
expectedResp: []*models.BucketObject{
&models.BucketObject{
Name: "obj1",
LastModified: t1.String(),
Size: int64(1024),
ContentType: "content",
},
},
wantError: nil,
},
}
for _, tt := range tests {
t.Run(tt.test, func(t *testing.T) {
minioListObjectsMock = tt.args.listFunc
resp, err := listBucketObjects(ctx, minClient, tt.args.bucketName, tt.args.prefix, tt.args.recursive)
minioGetObjectLegalHoldMock = tt.args.objectLegalHoldFunc
minioGetObjectRetentionMock = tt.args.objectRetentionFunc
resp, err := listBucketObjects(ctx, minClient, tt.args.bucketName, tt.args.prefix, tt.args.recursive, tt.args.withVersions)
if !reflect.DeepEqual(err, tt.wantError) {
t.Errorf("listBucketObjects() error: %v, wantErr: %v", err, tt.wantError)
return

View File

@@ -240,6 +240,10 @@ paths:
in: query
required: false
type: boolean
- name: with_versions
in: query
required: false
type: boolean
responses:
200:
description: A successful response.
@@ -281,6 +285,33 @@ paths:
tags:
- UserAPI
/buckets/{bucket_name}/objects/download:
get:
summary: Download Object
operationId: Download Object
produces:
- application/octet-stream
parameters:
- name: bucket_name
in: path
required: true
type: string
- name: prefix
in: query
required: true
type: string
responses:
200:
description: A successful response.
schema:
type: file
default:
description: Generic error response.
schema:
$ref: "#/definitions/error"
tags:
- UserAPI
/buckets/{name}/set-policy:
put:
summary: Bucket Set Policy
@@ -1661,6 +1692,26 @@ definitions:
type: string
last_modified:
type: string
is_latest:
type: boolean
is_delete_marker:
type: boolean
version_id:
type: string
user_tags:
type: object
additionalProperties:
type: string
expiration:
type: string
expiration_rule_id:
type: string
legal_hold_status:
type: string
retention_mode:
type: string
retention_until_date:
type: string
makeBucketRequest:
type: object