UI Add Notification Targets (#73)

This commit is contained in:
Daniel Valdivia
2020-04-20 20:53:58 -07:00
committed by GitHub
parent 0bcf88eb7c
commit 068ac281ea
25 changed files with 2183 additions and 143 deletions

File diff suppressed because one or more lines are too long

BIN
portal-ui/public/amqp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
portal-ui/public/kafka.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
portal-ui/public/mqtt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
portal-ui/public/mysql.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
portal-ui/public/nats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

BIN
portal-ui/public/redis.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -14,7 +14,12 @@
// 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 { MENU_OPEN, USER_LOGGED } from "./types";
import {
MENU_OPEN,
SERVER_IS_LOADING,
SERVER_NEEDS_RESTART,
USER_LOGGED
} from "./types";
export function userLoggedIn(loggedIn: boolean) {
return {
@@ -29,3 +34,17 @@ export function setMenuOpen(open: boolean) {
open: open
};
}
export function serverNeedsRestart(needsRestart: boolean) {
return {
type: SERVER_NEEDS_RESTART,
needsRestart: needsRestart
};
}
export function serverIsLoading(isLoading: boolean) {
return {
type: SERVER_IS_LOADING,
isLoading: isLoading
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

View File

@@ -14,13 +14,22 @@
// 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 {MENU_OPEN, SystemActionTypes, SystemState, USER_LOGGED} from "./types";
import {
MENU_OPEN,
SERVER_IS_LOADING,
SERVER_NEEDS_RESTART,
SystemActionTypes,
SystemState,
USER_LOGGED
} from "./types";
const initialState: SystemState = {
loggedIn: false,
session: "",
userName: "",
sidebarOpen:true,
sidebarOpen: true,
serverNeedsRestart: false,
serverIsLoading: false
};
export function systemReducer(
@@ -38,6 +47,17 @@ export function systemReducer(
...state,
sidebarOpen: action.open
};
case SERVER_NEEDS_RESTART:
return {
...state,
serverNeedsRestart: action.needsRestart
};
case SERVER_IS_LOADING:
return {
...state,
serverIsLoading: action.isLoading
};
default:
return state;
}

View File

@@ -14,7 +14,13 @@
// 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 { TextField, Grid, InputLabel, TextFieldProps } from "@material-ui/core";
import {
Grid,
InputLabel,
TextField,
TextFieldProps,
Tooltip
} from "@material-ui/core";
import { OutlinedInputProps } from "@material-ui/core/OutlinedInput";
import {
createStyles,
@@ -23,16 +29,19 @@ import {
withStyles
} from "@material-ui/core/styles";
import { fieldBasic } from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
interface InputBoxProps {
label: string;
classes: any;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
value: string;
value: string | boolean;
id: string;
name: string;
disabled?: boolean;
multiline?: boolean;
type?: string;
tooltip?: string;
autoComplete?: string;
}
@@ -78,6 +87,8 @@ const InputBoxWrapper = ({
type = "text",
autoComplete = "off",
disabled = false,
multiline = false,
tooltip = "",
classes
}: InputBoxProps) => {
return (
@@ -97,9 +108,17 @@ const InputBoxWrapper = ({
disabled={disabled}
onChange={onChange}
type={type}
multiline={multiline}
autoComplete={autoComplete}
/>
</div>
{tooltip !== "" && (
<div>
<Tooltip title={tooltip} placement="left">
<HelpIcon />
</Tooltip>
</div>
)}
</Grid>
</React.Fragment>
);

View File

@@ -18,7 +18,7 @@ import Grid from "@material-ui/core/Grid";
import RadioGroup from "@material-ui/core/RadioGroup";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Radio, { RadioProps } from "@material-ui/core/Radio";
import { InputLabel } from "@material-ui/core";
import { InputLabel, Tooltip } from "@material-ui/core";
import {
createStyles,
Theme,
@@ -26,18 +26,20 @@ import {
makeStyles
} from "@material-ui/core/styles";
import { fieldBasic } from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
interface selectorTypes {
export interface SelectorTypes {
label: string;
value: string;
}
interface RadioGroupProps {
selectorOptions: selectorTypes[];
selectorOptions: SelectorTypes[];
currentSelection: string;
label: string;
id: string;
name: string;
tooltip?: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
classes: any;
displayInColumn?: boolean;
@@ -106,6 +108,7 @@ export const RadioGroupSelector = ({
id,
name,
onChange,
tooltip = "",
classes,
displayInColumn = false
}: RadioGroupProps) => {
@@ -136,6 +139,13 @@ export const RadioGroupSelector = ({
})}
</RadioGroup>
</div>
{tooltip !== "" && (
<div>
<Tooltip title={tooltip} placement="left">
<HelpIcon />
</Tooltip>
</div>
)}
</Grid>
</React.Fragment>
);

View File

@@ -0,0 +1,307 @@
// 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, { useCallback, useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { FormControlLabel, Switch } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
interface IConfMySqlProps {
onChange: (newValue: Map<string, string>) => void;
classes: any;
}
const styles = (theme: Theme) => createStyles({});
const ConfMySql = ({ onChange, classes }: IConfMySqlProps) => {
//Local States
const [useDsnString, setUseDsnString] = useState<boolean>(false);
const [dsnString, setDsnString] = useState<string>("");
const [host, setHostname] = useState<string>("");
const [dbName, setDbName] = useState<string>("");
const [port, setPort] = useState<string>("");
const [user, setUser] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [table, setTable] = useState<string>("");
const [format, setFormat] = useState<string>("namespace");
const [queueDir, setQueueDir] = useState<string>("");
const [queueLimit, setQueueLimit] = useState<string>("");
const [comment, setComment] = useState<string>("");
// dsn_string* (string) MySQL data-source-name connection string e.g. "<user>:<password>@tcp(<host>:<port>)/<database>"
// table* (string) DB table name to store/update events, table is auto-created
// format* (namespace*|access) 'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'
// queue_dir (path) staging dir for undelivered messages e.g. '/home/events'
// queue_limit (number) maximum limit for undelivered messages, defaults to '100000'
// comment (sentence) optionally add a comment to this setting
const parseDsnString = (
input: string,
keys: string[]
): Map<string, string> => {
let kvFields: Map<string, string> = new Map();
const regex = /(.*?):(.*?)@tcp\((.*?):(.*?)\)\/(.*?)$/gm;
let m;
while ((m = regex.exec(input)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (m.index === regex.lastIndex) {
regex.lastIndex++;
}
kvFields.set("user", m[1]);
kvFields.set("password", m[2]);
kvFields.set("host", m[3]);
kvFields.set("port", m[4]);
kvFields.set("dbname", m[5]);
}
return kvFields;
};
const configToDsnString = useCallback((): string => {
return `${user}:${password}@tcp(${host}:${port})/${dbName}`;
}, [user, password, host, port, dbName]);
useEffect(() => {
if (dsnString !== "") {
let values: Map<string, string> = new Map();
if (dsnString !== "") {
values.set("dsn_string", dsnString);
}
if (table !== "") {
values.set("table", table);
}
if (format !== "") {
values.set("format", format);
}
if (queueDir !== "") {
values.set("queue_dir", queueDir);
}
if (queueLimit !== "") {
values.set("queue_limit", queueLimit);
}
if (comment !== "") {
values.set("comment", comment);
}
onChange(values);
}
}, [dsnString, table, format, queueDir, queueLimit, comment, onChange]);
useEffect(() => {
const cs = configToDsnString();
setDsnString(cs);
}, [user, dbName, password, port, host, setDsnString, configToDsnString]);
return (
<Grid container>
<Grid item xs={12}>
<FormControlLabel
control={
<Switch
checked={useDsnString}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
// build dsn_string
const cs = configToDsnString();
setDsnString(cs);
} else {
// parse dsn_string
const kv = parseDsnString(dsnString, [
"host",
"port",
"dbname",
"user",
"password"
]);
setHostname(kv.get("host") ? kv.get("host") + "" : "");
setPort(kv.get("port") ? kv.get("port") + "" : "");
setDbName(kv.get("dbname") ? kv.get("dbname") + "" : "");
setUser(kv.get("user") ? kv.get("user") + "" : "");
setPassword(
kv.get("password") ? kv.get("password") + "" : ""
);
}
setUseDsnString(event.target.checked);
}}
name="checkedB"
color="primary"
/>
}
label="Enter DSN String"
/>
</Grid>
{useDsnString ? (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="dsn-string"
name="dsn_string"
label="DSN String"
value={dsnString}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setDsnString(e.target.value);
}}
/>
</Grid>
</React.Fragment>
) : (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="host"
name="host"
label="Host"
value={host}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setHostname(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="db-name"
name="db-name"
label="DB Name"
value={dbName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setDbName(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="port"
name="port"
label="Port"
value={port}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPort(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="user"
name="user"
label="User"
value={user}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUser(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="password"
name="password"
label="Password"
type="password"
value={password}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
}}
/>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="table"
name="table"
label="Table"
value={table}
tooltip="DB table name to store/update events, table is auto-created"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTable(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<RadioGroupSelector
currentSelection={format}
id="format"
name="format"
label="Format"
onChange={e => {
setFormat(e.target.value);
}}
tooltip="'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'"
selectorOptions={[
{ label: "Namespace", value: "namespace" },
{ label: "Access", value: "access" }
]}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="queue-dir"
name="queue_dir"
label="Queue Dir"
value={queueDir}
tooltip="staging dir for undelivered messages e.g. '/home/events'"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setQueueDir(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="queue-limit"
name="queue_limit"
label="Queue Limit"
type="number"
value={queueLimit}
tooltip="maximum limit for undelivered messages, defaults to '10000'"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setQueueLimit(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="comment"
name="comment"
label="Comment"
multiline={true}
value={comment}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setComment(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<br />
</Grid>
</Grid>
);
};
export default withStyles(styles)(ConfMySql);

View File

@@ -0,0 +1,388 @@
// 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, { useCallback, useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { FormControlLabel, Switch } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
interface IConfPostgresProps {
onChange: (newValue: Map<string, string>) => void;
classes: any;
}
const styles = (theme: Theme) => createStyles({});
const ConfPostgres = ({ onChange, classes }: IConfPostgresProps) => {
//Local States
const [useConnectionString, setUseConnectionString] = useState<boolean>(
false
);
const [connectionString, setConnectionString] = useState<string>("");
const [host, setHostname] = useState<string>("");
const [dbName, setDbName] = useState<string>("");
const [port, setPort] = useState<string>("");
const [user, setUser] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [sslMode, setSslMode] = useState<boolean>(true);
const [table, setTable] = useState<string>("");
const [format, setFormat] = useState<string>("namespace");
const [queueDir, setQueueDir] = useState<string>("");
const [queueLimit, setQueueLimit] = useState<string>("");
const [comment, setComment] = useState<string>("");
// connection_string* (string) Postgres server connection-string e.g. "host=localhost port=5432 dbname=minio_events user=postgres password=password sslmode=disable"
// "host=localhost
// port=5432
//dbname=minio_events
//user=postgres
//password=password
//sslmode=disable"
// table* (string) DB table name to store/update events, table is auto-created
// format* (namespace*|access) 'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'
// queue_dir (path) staging dir for undelivered messages e.g. '/home/events'
// queue_limit (number) maximum limit for undelivered messages, defaults to '10000'
// comment (sentence) optionally add a comment to this setting
const KvSeparator = "=";
const parseConnectionString = (
input: string,
keys: string[]
): Map<string, string> => {
let valueIndexes: number[] = [];
for (const key of keys) {
const i = input.indexOf(key + KvSeparator);
if (i === -1) {
continue;
}
valueIndexes.push(i);
}
valueIndexes.sort((n1, n2) => n1 - n2);
let kvFields = new Map<string, string>();
let fields: string[] = new Array<string>(valueIndexes.length);
for (let i = 0; i < valueIndexes.length; i++) {
const j = i + 1;
if (j < valueIndexes.length) {
fields[i] = input.substr(
valueIndexes[i],
valueIndexes[j] - valueIndexes[i]
);
} else {
fields[i] = input.substr(valueIndexes[i]);
}
}
for (let field of fields) {
if (field === undefined) {
continue;
}
const key = field.substr(0, field.indexOf("="));
const value = field.substr(field.indexOf("=") + 1).trim();
kvFields.set(key, value);
}
return kvFields;
};
const configToString = useCallback((): string => {
let strValue = "";
if (host !== "") {
strValue = `${strValue} host=${host}`;
}
if (dbName !== "") {
strValue = `${strValue} dbname=${dbName}`;
}
if (user !== "") {
strValue = `${strValue} user=${user}`;
}
if (password !== "") {
strValue = `${strValue} password=${password}`;
}
if (port !== "") {
strValue = `${strValue} port=${port}`;
}
const sslModeVal = sslMode ? "enable" : "disable";
strValue = `${strValue} sslmode=${sslModeVal}`;
return strValue.trim();
}, [host, dbName, user, password, port, sslMode]);
useEffect(() => {
if (connectionString !== "") {
let values: Map<string, string> = new Map();
if (connectionString !== "") {
values.set("connection_string", connectionString);
}
if (table !== "") {
values.set("table", table);
}
if (format !== "") {
values.set("format", format);
}
if (queueDir !== "") {
values.set("queue_dir", queueDir);
}
if (queueLimit !== "") {
values.set("queue_limit", queueLimit);
}
if (comment !== "") {
values.set("comment", comment);
}
onChange(values);
}
}, [
connectionString,
table,
format,
queueDir,
queueLimit,
comment,
onChange
]);
useEffect(() => {
const cs = configToString();
setConnectionString(cs);
}, [
user,
dbName,
password,
port,
sslMode,
host,
setConnectionString,
configToString
]);
return (
<Grid container>
<Grid item xs={12}>
<FormControlLabel
control={
<Switch
checked={useConnectionString}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
// build connection_string
const cs = configToString();
setConnectionString(cs);
} else {
// parse connection_string
const kv = parseConnectionString(connectionString, [
"host",
"port",
"dbname",
"user",
"password",
"sslmode"
]);
setHostname(kv.get("host") ? kv.get("host") + "" : "");
setPort(kv.get("port") ? kv.get("port") + "" : "");
setDbName(kv.get("dbname") ? kv.get("dbname") + "" : "");
setUser(kv.get("user") ? kv.get("user") + "" : "");
setPassword(
kv.get("password") ? kv.get("password") + "" : ""
);
setSslMode(kv.get("sslmode") === "true");
}
setUseConnectionString(event.target.checked);
}}
name="checkedB"
color="primary"
/>
}
label="Enter Connection String"
/>
</Grid>
{useConnectionString ? (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="connection-string"
name="connection_string"
label="Connection String"
value={connectionString}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setConnectionString(e.target.value);
}}
/>
</Grid>
</React.Fragment>
) : (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="host"
name="host"
label="Host"
value={host}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setHostname(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="db-name"
name="db-name"
label="DB Name"
value={dbName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setDbName(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="port"
name="port"
label="Port"
value={port}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPort(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<RadioGroupSelector
currentSelection={sslMode + ""}
id="sslmode"
name="sslmode"
label="SSL Mode"
onChange={e => {
setSslMode(e.target.value === "true");
}}
selectorOptions={[
{ label: "Enabled", value: "true" },
{ label: "Disabled", value: "false" }
]}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="user"
name="user"
label="User"
value={user}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setUser(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="password"
name="password"
label="Password"
type="password"
value={password}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
}}
/>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="table"
name="table"
label="Table"
value={table}
tooltip="DB table name to store/update events, table is auto-created"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTable(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<RadioGroupSelector
currentSelection={format}
id="format"
name="format"
label="Format"
onChange={e => {
setFormat(e.target.value);
}}
tooltip="'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'"
selectorOptions={[
{ label: "Namespace", value: "namespace" },
{ label: "Access", value: "access" }
]}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="queue-dir"
name="queue_dir"
label="Queue Dir"
value={queueDir}
tooltip="staging dir for undelivered messages e.g. '/home/events'"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setQueueDir(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="queue-limit"
name="queue_limit"
label="Queue Limit"
type="number"
value={queueLimit}
tooltip="maximum limit for undelivered messages, defaults to '10000'"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setQueueLimit(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="comment"
name="comment"
label="Comment"
multiline={true}
value={comment}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setComment(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<br />
</Grid>
</Grid>
);
};
export default withStyles(styles)(ConfPostgres);

View File

@@ -0,0 +1,141 @@
// 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 Grid from "@material-ui/core/Grid";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RadioGroupSelector from "../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import { KVField } from "./types";
interface IConfGenericProps {
onChange: (newValue: Map<string, string>) => void;
fields: KVField[];
classes: any;
}
const styles = (theme: Theme) => createStyles({});
const ConfTargetGeneric = ({
onChange,
fields,
classes
}: IConfGenericProps) => {
//Local States
const [keyValues, setKeyValues] = useState<Map<string, string>>(new Map());
useEffect(() => {
if (keyValues.size > 0) {
onChange(keyValues);
}
}, [keyValues, onChange]);
const val = (key: string): string => {
return keyValues.get(key) === undefined ? "" : keyValues.get(key) + "";
};
const valFall = (key: string, fallback: string): string => {
return keyValues.get(key) === undefined
? fallback
: keyValues.get(key) + "";
};
return (
<Grid container>
{fields.map(field => (
<React.Fragment key={field.name}>
{field.type === "on|off" ? (
<Grid item xs={12}>
<RadioGroupSelector
currentSelection={valFall(field.name, "false")}
id={field.name}
name={field.name}
label={field.label}
tooltip={field.tooltip}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setKeyValues(keyValues.set(field.name, e.target.value));
}}
selectorOptions={[
{ label: "On", value: "true" },
{ label: "Off", value: "false" }
]}
/>
</Grid>
) : (
<Grid item xs={12}>
<InputBoxWrapper
id={field.name}
name={field.name}
label={field.label}
tooltip={field.tooltip}
value={val(field.name)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setKeyValues(keyValues.set(field.name, e.target.value));
}}
/>
</Grid>
)}
</React.Fragment>
))}
<Grid item xs={12}>
<InputBoxWrapper
id="queue-dir"
name="queue_dir"
label="Queue Dir"
value={val("queue_dir")}
tooltip="staging dir for undelivered messages e.g. '/home/events'"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setKeyValues(keyValues.set("queue_dir", e.target.value));
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="queue-limit"
name="queue_limit"
label="Queue Limit"
type="number"
value={val("queue_limit")}
tooltip="maximum limit for undelivered messages, defaults to '10000'"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setKeyValues(keyValues.set("queue_limit", e.target.value));
}}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="comment"
name="comment"
label="Comment"
multiline={true}
value={val("comment")}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setKeyValues(keyValues.set("comment", e.target.value));
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<br />
</Grid>
</Grid>
);
};
export default withStyles(styles)(ConfTargetGeneric);

View File

@@ -0,0 +1,38 @@
// 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 { SelectorTypes } from "../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
export type KVFieldType =
| "string"
| "number"
| "on|off"
| "enum"
| "path"
| "url"
| "address"
| "duration"
| "uri"
| "sentence";
export interface KVField {
name: string;
label: string;
tooltip: string;
required?: boolean;
type: KVFieldType;
options?: SelectorTypes[];
}

View File

@@ -40,7 +40,11 @@ import {
} from "react-router-dom";
import { connect } from "react-redux";
import { AppState } from "../../store";
import { setMenuOpen } from "../../actions";
import {
serverIsLoading,
serverNeedsRestart,
setMenuOpen
} from "../../actions";
import { ThemedComponentProps } from "@material-ui/core/styles/withTheme";
import Buckets from "./Buckets/Buckets";
import Policies from "./Policies/Policies";
@@ -54,6 +58,7 @@ import ServiceAccounts from "./ServiceAccounts/ServiceAccounts";
import Users from "./Users/Users";
import Groups from "./Groups/Groups";
import ListNotificationEndpoints from "./NotificationEndopoints/ListNotificationEndpoints";
import { Button, LinearProgress } from "@material-ui/core";
function Copyright() {
return (
@@ -151,24 +156,42 @@ const styles = (theme: Theme) =>
},
fixedHeight: {
minHeight: 240
},
warningBar: {
background: theme.palette.primary.main,
color: "white",
heigh: "60px",
widht: "100%",
lineHeight: "60px",
textAlign: "center"
}
});
const mapState = (state: AppState) => ({
open: state.system.sidebarOpen
open: state.system.sidebarOpen,
needsRestart: state.system.serverNeedsRestart,
isServerLoading: state.system.serverIsLoading
});
const connector = connect(mapState, { setMenuOpen });
const connector = connect(mapState, {
setMenuOpen,
serverNeedsRestart,
serverIsLoading
});
interface ConsoleProps {
interface IConsoleProps {
open: boolean;
needsRestart: boolean;
isServerLoading: boolean;
title: string;
classes: any;
setMenuOpen: typeof setMenuOpen;
serverNeedsRestart: typeof serverNeedsRestart;
serverIsLoading: typeof serverIsLoading;
}
class Console extends React.Component<
ConsoleProps & RouteComponentProps & StyledProps & ThemedComponentProps
IConsoleProps & RouteComponentProps & StyledProps & ThemedComponentProps
> {
componentDidMount(): void {
api
@@ -182,8 +205,25 @@ class Console extends React.Component<
});
}
restartServer() {
this.props.serverIsLoading(true);
api
.invoke("POST", "/api/v1/service/restart", {})
.then(res => {
console.log("success restarting service");
console.log(res);
this.props.serverIsLoading(false);
this.props.serverNeedsRestart(false);
})
.catch(err => {
this.props.serverIsLoading(false);
console.log("failure restarting service");
console.log(err);
});
}
render() {
const { classes, open } = this.props;
const { classes, open, needsRestart, isServerLoading } = this.props;
return (
<div className={classes.root}>
<CssBaseline />
@@ -209,6 +249,30 @@ class Console extends React.Component<
</Drawer>
<main className={classes.content}>
{needsRestart && (
<div className={classes.warningBar}>
{isServerLoading ? (
<React.Fragment>
The server is restarting.
<LinearProgress />
</React.Fragment>
) : (
<React.Fragment>
The instance needs to be restarted for configuration changes
to take effect.{" "}
<Button
color="secondary"
size="small"
onClick={() => {
this.restartServer();
}}
>
Restart
</Button>
</React.Fragment>
)}
</div>
)}
<div className={classes.appBarSpacer} />
<Container maxWidth="lg" className={classes.container}>
<Router history={history}>

View File

@@ -46,7 +46,7 @@ const styles = (theme: Theme) =>
marginBottom: "20px",
textAlign: "center",
"& img": {
width: "160px"
width: "120px"
}
},
menuList: {

View File

@@ -0,0 +1,801 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useCallback, useEffect, useState } from "react";
import { 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 ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import ConfPostgres from "../Configurations/ConfPostgres";
import api from "../../../common/api";
import { serverNeedsRestart } from "../../../actions";
import { connect } from "react-redux";
import ConfMySql from "../Configurations/ConfMySql";
import ConfTargetGeneric from "../Configurations/ConfTargetGeneric";
import { KVField } from "../Configurations/types";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
},
strongText: {
fontWeight: 700
},
keyName: {
marginLeft: 5
},
buttonContainer: {
textAlign: "right"
},
logoButton: {
height: "80px"
}
});
const notifyPostgres = "notify_postgres";
const notifyMysql = "notify_mysql";
const notifyKafka = "notify_kafka";
const notifyAmqp = "notify_amqp";
const notifyMqtt = "notify_mqtt";
const notifyRedis = "notify_redis";
const notifyNats = "notify_nats";
const notifyElasticsearch = "notify_elasticsearch";
const notifyWebhooks = "notify_webhooks";
const notifyNsq = "notify_nsq";
interface IAddNotificationEndpointProps {
open: boolean;
closeModalAndRefresh: any;
serverNeedsRestart: typeof serverNeedsRestart;
classes: any;
}
const AddNotificationEndpoint = ({
open,
closeModalAndRefresh,
serverNeedsRestart,
classes
}: IAddNotificationEndpointProps) => {
//Local States
const [service, setService] = useState<string>("");
const [valuesObj, setValueObj] = useState<Map<string, string>>(new Map());
const [saving, setSaving] = useState<boolean>(false);
const [addError, setError] = useState<string>("");
//Effects
useEffect(() => {
if (saving) {
let keyValues: Array<object> = new Array<object>();
valuesObj.forEach((value: string, key: string) => {
keyValues.push({ key: key, value: value });
});
let payload = {
key_values: keyValues
};
api
.invoke("PUT", `/api/v1/configs/${service}`, payload)
.then(res => {
setSaving(false);
setError("");
serverNeedsRestart(true);
closeModalAndRefresh();
})
.catch(err => {
setSaving(false);
setError(err);
});
}
}, [saving, serverNeedsRestart, service, valuesObj, closeModalAndRefresh]);
//Fetch Actions
const submitForm = (event: React.FormEvent) => {
event.preventDefault();
setSaving(true);
};
const onValueChange = useCallback(
newValue => {
setValueObj(newValue);
},
[setValueObj]
);
let srvComponent = <React.Fragment />;
switch (service) {
case notifyPostgres: {
srvComponent = <ConfPostgres onChange={onValueChange} />;
break;
}
case notifyMysql: {
srvComponent = <ConfMySql onChange={onValueChange} />;
break;
}
case notifyKafka: {
const fields: KVField[] = [
{
name: "brokers",
label: "Brokers",
required: true,
tooltip: "comma separated list of Kafka broker addresses",
type: "string"
},
{
name: "topic",
label: "Topic",
tooltip: "Kafka topic used for bucket notifications",
type: "string"
},
{
name: "sasl_username",
label: "SASL Username",
tooltip: "username for SASL/PLAIN or SASL/SCRAM authentication",
type: "string"
},
{
name: "sasl_password",
label: "SASL Password",
tooltip: "password for SASL/PLAIN or SASL/SCRAM authentication",
type: "string"
},
{
name: "sasl_mechanism",
label: "SASL Mechanism",
tooltip: "sasl authentication mechanism, default 'PLAIN'",
type: "string"
},
{
name: "tls_client_auth",
label: "TLS Client Auth",
tooltip:
"clientAuth determines the Kafka server's policy for TLS client auth",
type: "string"
},
{
name: "sasl",
label: "SASL",
tooltip: "set to 'on' to enable SASL authentication",
type: "on|off"
},
{
name: "tls",
label: "TLS",
tooltip: "set to 'on' to enable TLS",
type: "on|off"
},
{
name: "tls_skip_verify",
label: "TLS skip verify",
tooltip:
'trust server TLS without verification, defaults to "on" (verify)',
type: "on|off"
},
{
name: "client_tls_cert",
label: "client TLS cert",
tooltip: "path to client certificate for mTLS auth",
type: "path"
},
{
name: "client_tls_key",
label: "client TLS key",
tooltip: "path to client key for mTLS auth",
type: "path"
},
{
name: "version",
label: "Version",
tooltip: "specify the version of the Kafka cluster e.g '2.2.0'",
type: "string"
}
];
srvComponent = (
<ConfTargetGeneric fields={fields} onChange={onValueChange} />
);
break;
}
case notifyAmqp: {
const fields: KVField[] = [
{
name: "url",
required: true,
label: "url",
tooltip:
"AMQP server endpoint e.g. `amqp://myuser:mypassword@localhost:5672`",
type: "url"
},
{
name: "exchange",
label: "exchange",
tooltip: "name of the AMQP exchange",
type: "string"
},
{
name: "exchange_type",
label: "exchange_type",
tooltip: "AMQP exchange type",
type: "string"
},
{
name: "routing_key",
label: "routing_key",
tooltip: "routing key for publishing",
type: "string"
},
{
name: "mandatory",
label: "mandatory",
tooltip:
"quietly ignore undelivered messages when set to 'off', default is 'on'",
type: "on|off"
},
{
name: "durable",
label: "durable",
tooltip:
"persist queue across broker restarts when set to 'on', default is 'off'",
type: "on|off"
},
{
name: "no_wait",
label: "no_wait",
tooltip:
"non-blocking message delivery when set to 'on', default is 'off'",
type: "on|off"
},
{
name: "internal",
label: "internal",
tooltip:
"set to 'on' for exchange to be not used directly by publishers, but only when bound to other exchanges",
type: "on|off"
},
{
name: "auto_deleted",
label: "auto_deleted",
tooltip:
"auto delete queue when set to 'on', when there are no consumers",
type: "on|off"
},
{
name: "delivery_mode",
label: "delivery_mode",
tooltip: "set to '1' for non-persistent or '2' for persistent queue",
type: "number"
}
];
srvComponent = (
<ConfTargetGeneric fields={fields} onChange={onValueChange} />
);
break;
}
case notifyRedis: {
const fields: KVField[] = [
{
name: "address",
required: true,
label: "address",
tooltip: "Redis server's address. For example: `localhost:6379`",
type: "address"
},
{
name: "key",
required: true,
label: "key",
tooltip: "Redis key to store/update events, key is auto-created",
type: "string"
},
{
name: "password",
label: "password",
tooltip: "Redis server password",
type: "string"
}
];
srvComponent = (
<ConfTargetGeneric fields={fields} onChange={onValueChange} />
);
break;
}
case notifyMqtt: {
const fields: KVField[] = [
{
name: "broker",
required: true,
label: "broker",
tooltip: "MQTT server endpoint e.g. `tcp://localhost:1883`",
type: "uri"
},
{
name: "topic",
required: true,
label: "topic",
tooltip: "name of the MQTT topic to publish",
type: "string"
},
{
name: "username",
label: "username",
tooltip: "MQTT username",
type: "string"
},
{
name: "password",
label: "password",
tooltip: "MQTT password",
type: "string"
},
{
name: "qos",
label: "qos",
tooltip: "set the quality of service priority, defaults to '0'",
type: "number"
},
{
name: "keep_alive_interval",
label: "keep_alive_interval",
tooltip: "keep-alive interval for MQTT connections in s,m,h,d",
type: "duration"
},
{
name: "reconnect_interval",
label: "reconnect_interval",
tooltip: "reconnect interval for MQTT connections in s,m,h,d",
type: "duration"
}
];
srvComponent = (
<ConfTargetGeneric fields={fields} onChange={onValueChange} />
);
break;
}
case notifyNats: {
const fields: KVField[] = [
{
name: "address",
required: true,
label: "address",
tooltip: "NATS server address e.g. '0.0.0.0:4222'",
type: "address"
},
{
name: "subject",
required: true,
label: "subject",
tooltip: "NATS subscription subject",
type: "string"
},
{
name: "username",
label: "username",
tooltip: "NATS username",
type: "string"
},
{
name: "password",
label: "password",
tooltip: "NATS password",
type: "string"
},
{
name: "token",
label: "token",
tooltip: "NATS token",
type: "string"
},
{
name: "tls",
label: "tls",
tooltip: "set to 'on' to enable TLS",
type: "on|off"
},
{
name: "tls_skip_verify",
label: "tls_skip_verify",
tooltip:
'trust server TLS without verification, defaults to "on" (verify)',
type: "on|off"
},
{
name: "ping_interval",
label: "ping_interval",
tooltip:
"client ping commands interval in s,m,h,d. Disabled by default",
type: "duration"
},
{
name: "streaming",
label: "streaming",
tooltip: "set to 'on', to use streaming NATS server",
type: "on|off"
},
{
name: "streaming_async",
label: "streaming_async",
tooltip: "set to 'on', to enable asynchronous publish",
type: "on|off"
},
{
name: "streaming_max_pub_acks_in_flight",
label: "streaming_max_pub_acks_in_flight",
tooltip: "number of messages to publish without waiting for ACKs",
type: "number"
},
{
name: "streaming_cluster_id",
label: "streaming_cluster_id",
tooltip: "unique ID for NATS streaming cluster",
type: "string"
},
{
name: "cert_authority",
label: "cert_authority",
tooltip: "path to certificate chain of the target NATS server",
type: "string"
},
{
name: "client_cert",
label: "client_cert",
tooltip: "client cert for NATS mTLS auth",
type: "string"
},
{
name: "client_key",
label: "client_key",
tooltip: "client cert key for NATS mTLS auth",
type: "string"
}
];
srvComponent = (
<ConfTargetGeneric fields={fields} onChange={onValueChange} />
);
break;
}
case notifyElasticsearch: {
const fields: KVField[] = [
{
name: "url",
required: true,
label: "url",
tooltip:
"Elasticsearch server's address, with optional authentication info",
type: "url"
},
{
name: "index",
required: true,
label: "index",
tooltip:
"Elasticsearch index to store/update events, index is auto-created",
type: "string"
},
{
name: "format",
required: true,
label: "format",
tooltip:
"'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'",
type: "enum"
}
];
srvComponent = (
<ConfTargetGeneric fields={fields} onChange={onValueChange} />
);
break;
}
case notifyWebhooks: {
const fields: KVField[] = [
{
name: "endpoint",
required: true,
label: "endpoint",
tooltip:
"webhook server endpoint e.g. http://localhost:8080/minio/events",
type: "url"
},
{
name: "auth_token",
label: "auth_token",
tooltip: "opaque string or JWT authorization token",
type: "string"
}
];
srvComponent = (
<ConfTargetGeneric fields={fields} onChange={onValueChange} />
);
break;
}
case notifyNsq: {
const fields: KVField[] = [
{
name: "nsqd_address",
required: true,
label: "nsqd_address",
tooltip: "NSQ server address e.g. '127.0.0.1:4150'",
type: "address"
},
{
name: "topic",
required: true,
label: "topic",
tooltip: "NSQ topic",
type: "string"
},
{
name: "tls",
label: "tls",
tooltip: "set to 'on' to enable TLS",
type: "on|off"
},
{
name: "tls_skip_verify",
label: "tls_skip_verify",
tooltip:
'trust server TLS without verification, defaults to "on" (verify)',
type: "on|off"
}
];
srvComponent = (
<ConfTargetGeneric fields={fields} onChange={onValueChange} />
);
break;
}
}
let targetTitle = "";
switch (service) {
case notifyNsq:
targetTitle = "NSQ";
break;
case notifyWebhooks:
targetTitle = "Webhooks";
break;
case notifyElasticsearch:
targetTitle = "Elastic Search";
break;
case notifyNats:
targetTitle = "NATS";
break;
case notifyMqtt:
targetTitle = "MQTT";
break;
case notifyRedis:
targetTitle = "Redis";
break;
case notifyKafka:
targetTitle = "Kafka";
break;
case notifyPostgres:
targetTitle = "Postgres";
break;
case notifyMysql:
targetTitle = "Mysql";
break;
case notifyAmqp:
targetTitle = "AMQP";
break;
}
return (
<ModalWrapper
modalOpen={open}
onClose={closeModalAndRefresh}
title={`Add Lambda Notification Target ${targetTitle}`}
>
{service === "" && (
<Grid container>
<Grid item xs={12}>
<p>Pick a supported service:</p>
<table className={classes.chooseTable} style={{ width: "100%" }}>
<tbody>
<tr>
<td>
<Button
onClick={() => {
setService(notifyPostgres);
}}
>
<img
src="/postgres.png"
className={classes.logoButton}
alt="postgres"
/>
</Button>
</td>
<td>
<Button
onClick={() => {
setService(notifyKafka);
}}
>
<img
src="/kafka.png"
className={classes.logoButton}
alt="kafka"
/>
</Button>
</td>
<td>
<Button
onClick={() => {
setService(notifyAmqp);
}}
>
<img
src="/amqp.png"
className={classes.logoButton}
alt="amqp"
/>
</Button>
</td>
</tr>
<tr>
<td>
<Button
onClick={() => {
setService(notifyMqtt);
}}
>
<img
src="/mqtt.png"
className={classes.logoButton}
alt="mqtt"
/>
</Button>
</td>
<td>
<Button
onClick={() => {
setService(notifyRedis);
}}
>
<img
src="/redis.png"
className={classes.logoButton}
alt="redis"
/>
</Button>
</td>
<td>
<Button
onClick={() => {
setService(notifyNats);
}}
>
<img
src="/nats.png"
className={classes.logoButton}
alt="nats"
/>
</Button>
</td>
</tr>
<tr>
<td>
<Button
onClick={() => {
setService(notifyMysql);
}}
>
<img
src="/mysql.png"
className={classes.logoButton}
alt="mysql"
/>
</Button>
</td>
<td>
<Button
onClick={() => {
setService(notifyElasticsearch);
}}
>
<img
src="/elasticsearch.png"
className={classes.logoButton}
alt="elasticsearch"
/>
</Button>
</td>
<td></td>
</tr>
<tr>
<td>
<Button
onClick={() => {
setService(notifyWebhooks);
}}
>
Webhook
</Button>
</td>
<td>
<Button
onClick={() => {
setService(notifyNsq);
}}
>
NSQ
</Button>
</td>
<td />
</tr>
</tbody>
</table>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{saving && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
)}
{service !== "" && (
<React.Fragment>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<form noValidate onSubmit={submitForm}>
{srvComponent}
<Grid item xs={3} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
disabled={saving}
>
Save
</Button>
</Grid>
<Grid item xs={9} />
</form>
</React.Fragment>
)}
</ModalWrapper>
);
};
const connector = connect(null, { serverNeedsRestart });
export default connector(withStyles(styles)(AddNotificationEndpoint));

View File

@@ -17,6 +17,7 @@
import React, { useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
Button,
IconButton,
LinearProgress,
TableFooter,
@@ -27,6 +28,7 @@ import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import { CreateIcon } from "../../../icons";
import Paper from "@material-ui/core/Paper";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
@@ -41,6 +43,7 @@ import { NotificationEndpointItem, NotificationEndpointsList } from "./types";
import api from "../../../common/api";
import FiberManualRecordIcon from "@material-ui/icons/FiberManualRecord";
import { red } from "@material-ui/core/colors";
import AddNotificationEndpoint from "./AddNotificationEndpoint";
interface IListNotificationEndpoints {
classes: any;
@@ -83,6 +86,7 @@ const ListNotificationEndpoints = ({ classes }: IListNotificationEndpoints) => {
const [filter, setFilter] = useState<string>("");
const [error, setError] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(false);
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
//Effects
// load records on mount
@@ -116,6 +120,15 @@ const ListNotificationEndpoints = ({ classes }: IListNotificationEndpoints) => {
return (
<React.Fragment>
{addScreenOpen && (
<AddNotificationEndpoint
open={addScreenOpen}
closeModalAndRefresh={() => {
setIsLoading(true);
setAddScreenOpen(false);
}}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Lambda Notification Targets</Typography>
@@ -142,6 +155,16 @@ const ListNotificationEndpoints = ({ classes }: IListNotificationEndpoints) => {
)
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
}}
>
Add Notification Target
</Button>
</Grid>
<Grid item xs={12}>
<br />

View File

@@ -19,10 +19,14 @@ export interface SystemState {
sidebarOpen: boolean;
session: string;
userName: string;
serverNeedsRestart: boolean;
serverIsLoading: boolean;
}
export const USER_LOGGED = "USER_LOGGED";
export const MENU_OPEN = "MENU_OPEN";
export const SERVER_NEEDS_RESTART = "SERVER_NEEDS_RESTART";
export const SERVER_IS_LOADING = "SERVER_IS_LOADING";
interface UserLoggedAction {
type: typeof USER_LOGGED;
@@ -34,4 +38,18 @@ interface SetMenuOpenAction {
open: boolean;
}
export type SystemActionTypes = UserLoggedAction | SetMenuOpenAction;
interface ServerNeedsRestartAction {
type: typeof SERVER_NEEDS_RESTART;
needsRestart: boolean;
}
interface ServerIsLoading {
type: typeof SERVER_IS_LOADING;
isLoading: boolean;
}
export type SystemActionTypes =
| UserLoggedAction
| SetMenuOpenAction
| ServerNeedsRestartAction
| ServerIsLoading;

View File

@@ -168,7 +168,7 @@ func setConfigWithARNAccountID(ctx context.Context, client MinioAdmin, configNam
func buildConfig(configName *string, kvs []*models.ConfigurationKV) *string {
configElements := []string{*configName}
for _, kv := range kvs {
configElements = append(configElements, kv.Key+"="+kv.Value)
configElements = append(configElements, fmt.Sprintf("%s=%s", kv.Key, kv.Value))
}
config := strings.Join(configElements, " ")
return &config

View File

@@ -19,6 +19,7 @@ package restapi
import (
"bytes"
"context"
"encoding/json"
"fmt"
"testing"
@@ -141,7 +142,14 @@ func TestAddPolicy(t *testing.T) {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
} else {
funcAssert.Equal(policy.Name, assertPolicy.Name)
funcAssert.Equal(policy.Policy, assertPolicy.Policy)
var expectedPolicy iampolicy.Policy
var actualPolicy iampolicy.Policy
err1 := json.Unmarshal([]byte(policy.Policy), &expectedPolicy)
funcAssert.NoError(err1)
err2 := json.Unmarshal([]byte(assertPolicy.Policy), &actualPolicy)
funcAssert.NoError(err2)
funcAssert.Equal(expectedPolicy, actualPolicy)
}
// Test-2 : addPolicy() got an error while adding policy
minioAddPolicyMock = func(name string, policy *iampolicy.Policy) error {