Skip to content

Commit 959243d

Browse files
authored
Merge branch 'master' into release-v0.14.0
2 parents d09bfdf + cc8d5ab commit 959243d

File tree

7 files changed

+309
-10
lines changed

7 files changed

+309
-10
lines changed

integration/buckets_test.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,54 @@ func ListBuckets() (*http.Response, error) {
239239
return response, err
240240
}
241241

242+
func BucketInfo(name string) (*http.Response, error) {
243+
/*
244+
Helper function to test Bucket Info End Point
245+
GET: {{baseUrl}}/buckets/:name
246+
*/
247+
bucketInformationRequest, bucketInformationError := http.NewRequest(
248+
"GET", "http://localhost:9090/api/v1/buckets/"+name, nil)
249+
if bucketInformationError != nil {
250+
log.Println(bucketInformationError)
251+
}
252+
bucketInformationRequest.Header.Add("Cookie",
253+
fmt.Sprintf("token=%s", token))
254+
bucketInformationRequest.Header.Add("Content-Type", "application/json")
255+
client := &http.Client{
256+
Timeout: 2 * time.Second,
257+
}
258+
response, err := client.Do(bucketInformationRequest)
259+
return response, err
260+
}
261+
262+
func PutBucketsTags(bucketName string, tags map[string]string) (*http.Response, error) {
263+
/*
264+
Helper function to put bucket's tags.
265+
PUT: {{baseUrl}}/buckets/:bucket_name/tags
266+
{
267+
"tags": {}
268+
}
269+
*/
270+
requestDataAdd := map[string]interface{}{
271+
"tags": tags,
272+
}
273+
requestDataJSON, _ := json.Marshal(requestDataAdd)
274+
requestDataBody := bytes.NewReader(requestDataJSON)
275+
request, err := http.NewRequest("PUT",
276+
"http://localhost:9090/api/v1/buckets/"+bucketName+"/tags",
277+
requestDataBody)
278+
if err != nil {
279+
log.Println(err)
280+
}
281+
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
282+
request.Header.Add("Content-Type", "application/json")
283+
client := &http.Client{
284+
Timeout: 2 * time.Second,
285+
}
286+
response, err := client.Do(request)
287+
return response, err
288+
}
289+
242290
func TestAddBucket(t *testing.T) {
243291
assert := assert.New(t)
244292

@@ -592,3 +640,110 @@ func TestListBuckets(t *testing.T) {
592640
"testlistbuckets"+strconv.Itoa(i)))
593641
}
594642
}
643+
644+
func TestBucketInformationSuccessfulResponse(t *testing.T) {
645+
/*
646+
Test Bucket Info End Point with a Successful Response.
647+
*/
648+
649+
// 1. Create the bucket
650+
assert := assert.New(t)
651+
response, err := AddBucket("bucketinformation1", false, false)
652+
assert.Nil(err)
653+
if err != nil {
654+
log.Println(err)
655+
return
656+
}
657+
if response != nil {
658+
assert.Equal(201, response.StatusCode, inspectHTTPResponse(response))
659+
}
660+
661+
// 2. Add a tag to the bucket
662+
tags := make(map[string]string)
663+
tags["tag1"] = "tag1"
664+
putBucketTagResponse, putBucketTagError := PutBucketsTags(
665+
"bucketinformation1", tags)
666+
if putBucketTagError != nil {
667+
log.Println(putBucketTagError)
668+
assert.Fail("Error creating the bucket")
669+
return
670+
}
671+
if putBucketTagResponse != nil {
672+
assert.Equal(
673+
200, putBucketTagResponse.StatusCode,
674+
inspectHTTPResponse(putBucketTagResponse))
675+
}
676+
677+
// 3. Get the information
678+
bucketInfoResponse, bucketInfoError := BucketInfo("bucketinformation1")
679+
if bucketInfoError != nil {
680+
log.Println(bucketInfoError)
681+
assert.Fail("Error getting the bucket information")
682+
return
683+
}
684+
debugResponse := inspectHTTPResponse(bucketInfoResponse) // call it once
685+
if bucketInfoResponse != nil {
686+
assert.Equal(200, bucketInfoResponse.StatusCode,
687+
debugResponse)
688+
}
689+
printMessage(debugResponse)
690+
691+
// 4. Verify the information
692+
assert.True(
693+
strings.Contains(debugResponse, "bucketinformation1"),
694+
inspectHTTPResponse(bucketInfoResponse))
695+
assert.True(
696+
strings.Contains(debugResponse, "tag1"),
697+
inspectHTTPResponse(bucketInfoResponse))
698+
}
699+
700+
func TestBucketInformationGenericErrorResponse(t *testing.T) {
701+
/*
702+
Test Bucket Info End Point with a Generic Error Response.
703+
*/
704+
// 1. Create the bucket
705+
assert := assert.New(t)
706+
response, err := AddBucket("bucketinformation2", false, false)
707+
assert.Nil(err)
708+
if err != nil {
709+
log.Println(err)
710+
assert.Fail("Error creating the bucket")
711+
return
712+
}
713+
if response != nil {
714+
assert.Equal(201, response.StatusCode, inspectHTTPResponse(response))
715+
}
716+
717+
// 2. Add a tag to the bucket
718+
tags := make(map[string]string)
719+
tags["tag2"] = "tag2"
720+
putBucketTagResponse, putBucketTagError := PutBucketsTags(
721+
"bucketinformation2", tags)
722+
if putBucketTagError != nil {
723+
log.Println(putBucketTagError)
724+
assert.Fail("Error creating the bucket")
725+
return
726+
}
727+
if putBucketTagResponse != nil {
728+
assert.Equal(
729+
200, putBucketTagResponse.StatusCode,
730+
inspectHTTPResponse(putBucketTagResponse))
731+
}
732+
733+
// 3. Get the information
734+
bucketInfoResponse, bucketInfoError := BucketInfo("bucketinformation3")
735+
if bucketInfoError != nil {
736+
log.Println(bucketInfoError)
737+
assert.Fail("Error getting the bucket information")
738+
return
739+
}
740+
finalResponse := inspectHTTPResponse(bucketInfoResponse)
741+
if bucketInfoResponse != nil {
742+
assert.Equal(200, bucketInfoResponse.StatusCode)
743+
}
744+
745+
// 4. Verify the information
746+
// Since bucketinformation3 hasn't been created, then it is expected that
747+
// tag2 is not part of the response, this is why assert.False is used.
748+
assert.False(strings.Contains(finalResponse, "tag2"), finalResponse)
749+
}

portal-ui/src/common/SecureComponent/permissions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ export const IAM_PAGES = {
199199
NAMESPACE_TENANT_HOP: "/namespaces/:tenantNamespace/tenants/:tenantName/hop",
200200
NAMESPACE_TENANT_PODS:
201201
"/namespaces/:tenantNamespace/tenants/:tenantName/pods/:podName",
202+
NAMESPACE_TENANT_PVCS:
203+
"/namespaces/:tenantNamespace/tenants/:tenantName/pvcs/:PVCName",
202204
NAMESPACE_TENANT_PODS_LIST:
203205
"/namespaces/:tenantNamespace/tenants/:tenantName/pods",
204206
NAMESPACE_TENANT_SUMMARY:

portal-ui/src/screens/Console/Console.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,11 @@ const Console = ({
416416
path: IAM_PAGES.NAMESPACE_TENANT_PODS,
417417
forceDisplay: true,
418418
},
419+
{
420+
component: TenantDetails,
421+
path: IAM_PAGES.NAMESPACE_TENANT_PVCS,
422+
forceDisplay: true,
423+
},
419424
{
420425
component: TenantDetails,
421426
path: IAM_PAGES.NAMESPACE_TENANT_SUMMARY,
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// This file is part of MinIO Console Server
2+
// Copyright (c) 2021 MinIO, Inc.
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
import React, { Fragment, useEffect, useState } from "react";
18+
import { Theme } from "@mui/material/styles";
19+
import createStyles from "@mui/styles/createStyles";
20+
import withStyles from "@mui/styles/withStyles";
21+
import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary";
22+
import Grid from "@mui/material/Grid";
23+
import { Link } from "react-router-dom";
24+
import { setErrorSnackMessage } from "../../../../actions";
25+
import api from "../../../../common/api";
26+
import { IEvent } from "../ListTenants/types";
27+
import { niceDays } from "../../../../common/utils";
28+
import { ErrorResponseHandler } from "../../../../common/types";
29+
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
30+
31+
interface IPVCDetailsProps {
32+
classes: any;
33+
match: any;
34+
setErrorSnackMessage: typeof setErrorSnackMessage;
35+
}
36+
37+
const styles = (theme: Theme) =>
38+
createStyles({
39+
breadcrumLink: {
40+
textDecoration: "none",
41+
color: "black",
42+
},
43+
...containerForHeader(theme.spacing(4)),
44+
});
45+
46+
const PVCDetails = ({
47+
classes,
48+
match,
49+
setErrorSnackMessage,
50+
}: IPVCDetailsProps) => {
51+
const [loading, setLoading] = useState<boolean>(true);
52+
const tenantNamespace = match.params["tenantNamespace"];
53+
const tenantName = match.params["tenantName"];
54+
const PVCName = match.params["PVCName"];
55+
const [event, setEvent] = useState<IEvent[]>([]);
56+
57+
useEffect(() => {
58+
if (loading) {
59+
api
60+
.invoke(
61+
"GET",
62+
`/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/pvcs/${PVCName}/events`
63+
)
64+
.then((res: IEvent[]) => {
65+
for (let i = 0; i < res.length; i++) {
66+
let currentTime = (Date.now() / 1000) | 0;
67+
68+
res[i].seen = niceDays((currentTime - res[i].last_seen).toString());
69+
}
70+
setEvent(res);
71+
setLoading(false);
72+
})
73+
.catch((err: ErrorResponseHandler) => {
74+
setErrorSnackMessage(err);
75+
setLoading(false);
76+
});
77+
}
78+
}, [loading, PVCName, tenantNamespace, tenantName, setErrorSnackMessage]);
79+
80+
return (
81+
<Fragment>
82+
<Grid item xs={12}>
83+
<h1 className={classes.sectionTitle}>
84+
<Link
85+
to={`/namespaces/${tenantNamespace}/tenants/${tenantName}/volumes`}
86+
className={classes.breadcrumLink}
87+
>
88+
PVCs
89+
</Link>{" "}
90+
&gt; {PVCName}
91+
</h1>
92+
</Grid>
93+
<Grid container>
94+
<h1 className={classes.sectionTitle}>Events</h1>
95+
<TableWrapper
96+
itemActions={[]}
97+
columns={[
98+
{ label: "Namespace", elementKey: "namespace" },
99+
{ label: "Last Seen", elementKey: "seen" },
100+
{ label: "Message", elementKey: "message" },
101+
{ label: "Event Type", elementKey: "event_type" },
102+
{ label: "Reason", elementKey: "reason" },
103+
]}
104+
isLoading={loading}
105+
records={event}
106+
entityName="Events"
107+
idField="event"
108+
/>
109+
</Grid>
110+
</Fragment>
111+
);
112+
};
113+
114+
export default withStyles(styles)(PVCDetails);

portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import BackLink from "../../../../common/BackLink";
5050
import VerticalTabs from "../../Common/VerticalTabs/VerticalTabs";
5151
import BoxIconButton from "../../Common/BoxIconButton/BoxIconButton";
5252
import withSuspense from "../../Common/Components/withSuspense";
53+
import PVCDetails from "./PVCDetails";
5354

5455
const TenantYAML = withSuspense(React.lazy(() => import("./TenantYAML")));
5556
const TenantSummary = withSuspense(React.lazy(() => import("./TenantSummary")));
@@ -438,6 +439,10 @@ const TenantDetails = ({
438439
path="/namespaces/:tenantNamespace/tenants/:tenantName/pods"
439440
component={PodsSummary}
440441
/>
442+
<Route
443+
path="/namespaces/:tenantNamespace/tenants/:tenantName/pvcs/:PVCName"
444+
component={PVCDetails}
445+
/>
441446
<Route
442447
path="/namespaces/:tenantNamespace/tenants/:tenantName/volumes"
443448
component={VolumesSummary}

portal-ui/src/screens/Console/Tenants/TenantDetails/VolumesSummary.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { ErrorResponseHandler } from "../../../../common/types";
3333
import api from "../../../../common/api";
3434
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
3535
import SearchIcon from "../../../../icons/SearchIcon";
36+
import { IPodListElement } from "../ListTenants/types";
3637
import withSuspense from "../../Common/Components/withSuspense";
3738
import { setTenantDetailsLoad } from "../actions";
3839
import { AppState } from "../../../../store";
@@ -42,6 +43,7 @@ const DeletePVC = withSuspense(React.lazy(() => import("./DeletePVC")));
4243
interface ITenantVolumesProps {
4344
classes: any;
4445
setErrorSnackMessage: typeof setErrorSnackMessage;
46+
history: any;
4547
match: any;
4648
loadingTenant: boolean;
4749
setTenantDetailsLoad: typeof setTenantDetailsLoad;
@@ -61,6 +63,7 @@ const styles = (theme: Theme) =>
6163
const TenantVolumes = ({
6264
classes,
6365
setErrorSnackMessage,
66+
history,
6467
match,
6568
loadingTenant,
6669
}: ITenantVolumesProps) => {
@@ -106,6 +109,13 @@ const TenantVolumes = ({
106109
elementItem.name.includes(filter)
107110
);
108111

112+
const PVCViewAction = (PVC: IPodListElement) => {
113+
history.push(
114+
`/namespaces/${tenantNamespace}/tenants/${tenantName}/pvcs/${PVC.name}`
115+
);
116+
return;
117+
};
118+
109119
const closeDeleteModalAndRefresh = (reloadData: boolean) => {
110120
setDeleteOpen(false);
111121
setLoading(true);
@@ -152,7 +162,7 @@ const TenantVolumes = ({
152162
</Grid>
153163
<Grid item xs={12} className={classes.tableBlock}>
154164
<TableWrapper
155-
itemActions={[{ type: "delete", onClick: confirmDeletePVC }]}
165+
itemActions={[{ type: "view", onClick: PVCViewAction }, { type: "delete", onClick: confirmDeletePVC }]}
156166
columns={[
157167
{
158168
label: "Name",

0 commit comments

Comments
 (0)