Added Service Accounts page to settings (#128)

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2020-05-19 15:41:46 -05:00
committed by GitHub
parent 35d575e7ac
commit 989e6f3471
30 changed files with 1009 additions and 1534 deletions

View File

@@ -20,17 +20,18 @@ import iampolicy "github.com/minio/minio/pkg/iam/policy"
// endpoints definition
var (
configuration = "/configurations-list"
users = "/users"
groups = "/groups"
iamPolicies = "/policies"
dashboard = "/dashboard"
profiling = "/profiling"
trace = "/trace"
logs = "/logs"
watch = "/watch"
notifications = "/notification-endpoints"
buckets = "/buckets"
configuration = "/configurations-list"
users = "/users"
groups = "/groups"
iamPolicies = "/policies"
dashboard = "/dashboard"
profiling = "/profiling"
trace = "/trace"
logs = "/logs"
watch = "/watch"
notifications = "/notification-endpoints"
buckets = "/buckets"
serviceAccounts = "/service-accounts"
)
type ConfigurationActionSet struct {
@@ -179,19 +180,26 @@ var bucketsActionSet = ConfigurationActionSet{
),
}
// serviceAccountsActionSet contains the list of admin actions required for this endpoint to work
var serviceAccountsActionSet = 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,
users: usersActionSet,
groups: groupsActionSet,
iamPolicies: iamPoliciesActionSet,
dashboard: dashboardActionSet,
profiling: profilingActionSet,
trace: traceActionSet,
logs: logsActionSet,
watch: watchActionSet,
notifications: notificationsActionSet,
buckets: bucketsActionSet,
configuration: configurationActionSet,
users: usersActionSet,
groups: groupsActionSet,
iamPolicies: iamPoliciesActionSet,
dashboard: dashboardActionSet,
profiling: profilingActionSet,
trace: traceActionSet,
logs: logsActionSet,
watch: watchActionSet,
notifications: notificationsActionSet,
buckets: bucketsActionSet,
serviceAccounts: serviceAccountsActionSet,
}
// GetActionsStringFromPolicy extract the admin/s3 actions from a given policy and return them in []string format

View File

@@ -37,7 +37,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
args: args{
[]string{"admin:ServerInfo"},
},
want: 1,
want: 2,
},
{
name: "policies endpoint",
@@ -50,7 +50,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:ListUserPolicies",
},
},
want: 1,
want: 2,
},
{
name: "all admin endpoints",
@@ -59,7 +59,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:*",
},
},
want: 9,
want: 10,
},
{
name: "all s3 endpoints",
@@ -68,7 +68,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 2,
want: 3,
},
{
name: "all admin and s3 endpoints",
@@ -78,7 +78,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 11,
want: 12,
},
{
name: "no endpoints",

File diff suppressed because one or more lines are too long

View File

@@ -35,7 +35,7 @@ const isLoggedIn = () => {
};
const mapState = (state: AppState) => ({
loggedIn: state.system.loggedIn
loggedIn: state.system.loggedIn,
});
const connector = connect(mapState, { userLoggedIn });

View File

@@ -24,8 +24,8 @@ export class API {
return request(method, url)
.set("Authorization", `Bearer ${token}`)
.send(data)
.then(res => res.body)
.catch(err => {
.then((res) => res.body)
.catch((err) => {
// if we get unauthorized, kick out the user
if (err.status === 401) {
storage.removeItem("token");

View File

@@ -35,36 +35,36 @@ interface IListConfiguration {
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
strongText: {
fontWeight: 700
fontWeight: 700,
},
keyName: {
marginLeft: 5
marginLeft: 5,
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
boxShadow: "0px 3px 6px #00000012",
},
iconText: {
lineHeight: "24px"
}
lineHeight: "24px",
},
});
const ConfigurationsList = ({ classes }: IListConfiguration) => {
const [editScreenOpen, setEditScreenOpen] = useState(false);
const [selectedConfiguration, setSelectedConfiguration] = useState({
configuration_id: "",
configuration_label: ""
configuration_label: "",
});
const [error, setError] = useState("");
const [filter, setFilter] = useState("");
@@ -81,12 +81,12 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
setSelectedConfiguration(element);
setEditScreenOpen(true);
}
}
}
},
},
];
const filteredRecords: IConfigurationElement[] = configurationElements.filter(
elementItem =>
(elementItem) =>
elementItem.configuration_id
.toLocaleLowerCase()
.includes(filter.toLocaleLowerCase())
@@ -117,7 +117,7 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
className={classes.searchField}
id="search-resource"
label=""
onChange={event => {
onChange={(event) => {
setFilter(event.target.value);
}}
InputProps={{
@@ -126,7 +126,7 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
/>
</Grid>
@@ -137,7 +137,7 @@ const ConfigurationsList = ({ classes }: IListConfiguration) => {
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Configuration", elementKey: "configuration_id" }
{ label: "Configuration", elementKey: "configuration_id" },
]}
isLoading={false}
records={filteredRecords}

View File

@@ -44,29 +44,29 @@ interface IWebhook {
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
color: "red",
},
strongText: {
fontWeight: 700
fontWeight: 700,
},
keyName: {
marginLeft: 5
marginLeft: 5,
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
boxShadow: "0px 3px 6px #00000012",
},
iconText: {
lineHeight: "24px"
}
lineHeight: "24px",
},
});
const panels = {
@@ -77,8 +77,8 @@ const panels = {
apiURL: "",
configuration: {
configuration_id: "logger_webhook",
configuration_label: "Logger Webhook"
}
configuration_label: "Logger Webhook",
},
},
audit: {
main: "audit",
@@ -87,9 +87,9 @@ const panels = {
apiURL: "",
configuration: {
configuration_id: "audit_webhook",
configuration_label: "Audit Webhook"
}
}
configuration_label: "Audit Webhook",
},
},
};
const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
@@ -107,15 +107,15 @@ const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
return null;
}
const filteredRecords: IWebhook[] = webhooks.filter(elementItem =>
const filteredRecords: IWebhook[] = webhooks.filter((elementItem) =>
elementItem.name.toLocaleLowerCase().includes(filter.toLocaleLowerCase())
);
const tableActions = [
{
type: "edit",
onClick: () => {}
}
onClick: () => {},
},
];
return (
@@ -144,7 +144,7 @@ const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
className={classes.searchField}
id="search-resource"
label=""
onChange={event => {
onChange={(event) => {
setFilter(event.target.value);
}}
InputProps={{
@@ -153,7 +153,7 @@ const WebhookPanel = ({ match, classes }: IWebhookPanel) => {
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
/>
<Button

View File

@@ -31,34 +31,34 @@ export const configurationElements: IConfigurationElement[] = [
{ configuration_id: "cache", configuration_label: "Cache Configuration" },
{
configuration_id: "compression",
configuration_label: "Compression Configuration"
configuration_label: "Compression Configuration",
},
{ configuration_id: "etcd", configuration_label: "Etcd Configuration" },
{
configuration_id: "identity_openid",
configuration_label: "Identity Openid Configuration"
configuration_label: "Identity Openid Configuration",
},
{
configuration_id: "identity_ldap",
configuration_label: "Identity LDAP Configuration"
configuration_label: "Identity LDAP Configuration",
},
{
configuration_id: "policy_opa",
configuration_label: "Policy OPA Configuration"
configuration_label: "Policy OPA Configuration",
},
{
configuration_id: "kms_vault",
configuration_label: "KMS Vault Configuration"
configuration_label: "KMS Vault Configuration",
},
{ configuration_id: "kms_kes", configuration_label: "KMS KES Configuration" },
{
configuration_id: "logger_webhook",
configuration_label: "Logger Webhook Configuration"
configuration_label: "Logger Webhook Configuration",
},
{
configuration_id: "audit_webhook",
configuration_label: "Audit Webhook Configuration"
}
configuration_label: "Audit Webhook Configuration",
},
];
export const fieldsConfigurations: any = {
@@ -68,7 +68,7 @@ export const fieldsConfigurations: any = {
required: true,
label: "name",
tooltip: 'Name of the location of the server e.g. "us-west-rack2"',
type: "string"
type: "string",
},
{
name: "comment",
@@ -76,8 +76,8 @@ export const fieldsConfigurations: any = {
label: "comment",
tooltip: "You can add a comment to this setting",
type: "string",
multiline: true
}
multiline: true,
},
],
cache: [
{
@@ -86,21 +86,21 @@ export const fieldsConfigurations: any = {
label: "Drives",
tooltip:
'Mountpoints e.g. "/optane1" or "/optane2", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "expiry",
required: false,
label: "Expiry",
tooltip: 'Cache expiry duration in days e.g. "90"',
type: "number"
type: "number",
},
{
name: "quota",
required: false,
label: "Quota",
tooltip: 'Limit cache drive usage in percentage e.g. "90"',
type: "number"
type: "number",
},
{
name: "exclude",
@@ -108,28 +108,28 @@ export const fieldsConfigurations: any = {
label: "Exclude",
tooltip:
'Wildcard exclusion patterns e.g. "bucket/*.tmp" or "*.exe", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "after",
required: false,
label: "After",
tooltip: "Minimum number of access before caching an object",
type: "number"
type: "number",
},
{
name: "watermark_low",
required: false,
label: "Watermark Low",
tooltip: "Watermark Low",
type: "number"
type: "number",
},
{
name: "watermark_high",
required: false,
label: "Watermark High",
tooltip: "Watermark High",
type: "number"
type: "number",
},
{
name: "comment",
@@ -137,8 +137,8 @@ export const fieldsConfigurations: any = {
label: "Comment",
tooltip: "You can add a comment to this setting",
type: "string",
multiline: true
}
multiline: true,
},
],
compression: [
{
@@ -147,7 +147,7 @@ export const fieldsConfigurations: any = {
label: "Extensions",
tooltip:
'Extensions to compress e.g. ".txt",".log" or ".csv", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "mime_types",
@@ -155,8 +155,8 @@ export const fieldsConfigurations: any = {
label: "Mime Types",
tooltip:
'Mime types e.g. "text/*","application/json" or "application/xml", you can write one per field',
type: "csv"
}
type: "csv",
},
],
etcd: [
{
@@ -165,35 +165,35 @@ export const fieldsConfigurations: any = {
label: "Endpoints",
tooltip:
'List of etcd endpoints e.g. "http://localhost:2379", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "path_prefix",
required: false,
label: "Path Prefix",
tooltip: 'namespace prefix to isolate tenants e.g. "customer1/"',
type: "string"
type: "string",
},
{
name: "coredns_path",
required: false,
label: "Coredns Path",
tooltip: 'Shared bucket DNS records, default is "/skydns"',
type: "string"
type: "string",
},
{
name: "client_cert",
required: false,
label: "Client Cert",
tooltip: "Client cert for mTLS authentication",
type: "string"
type: "string",
},
{
name: "client_cert_key",
required: false,
label: "Client Cert Key",
tooltip: "Client cert key for mTLS authentication",
type: "string"
type: "string",
},
{
name: "comment",
@@ -201,8 +201,8 @@ export const fieldsConfigurations: any = {
label: "Comment",
tooltip: "You can add a comment to this setting",
type: "string",
multiline: true
}
multiline: true,
},
],
identity_openid: [
{
@@ -210,28 +210,28 @@ export const fieldsConfigurations: any = {
required: false,
label: "Config URL",
tooltip: "Config URL for Client ID configuration",
type: "string"
type: "string",
},
{
name: "client_id",
required: false,
label: "Client ID",
type: "string"
type: "string",
},
{
name: "claim_name",
required: false,
label: "Claim Name",
tooltip: "Claim Name",
type: "string"
type: "string",
},
{
name: "claim_prefix",
required: false,
label: "Claim Prefix",
tooltip: "Claim Prefix",
type: "string"
}
type: "string",
},
],
identity_ldap: [
{
@@ -239,7 +239,7 @@ export const fieldsConfigurations: any = {
required: true,
label: "Server ADDR",
tooltip: 'AD/LDAP server address e.g. "myldapserver.com:636"',
type: "string"
type: "string",
},
{
name: "username_format",
@@ -247,7 +247,7 @@ export const fieldsConfigurations: any = {
label: "Username Format",
tooltip:
'List of username bind DNs e.g. "uid=%s","cn=accounts","dc=myldapserver" or "dc=com", you can write one per field',
type: "csv"
type: "csv",
},
{
name: "username_search_filter",
@@ -255,7 +255,7 @@ export const fieldsConfigurations: any = {
label: "Username Search Filter",
tooltip:
'User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"',
type: "string"
type: "string",
},
{
name: "group_search_filter",
@@ -263,21 +263,21 @@ export const fieldsConfigurations: any = {
label: "Group Search Filter",
tooltip:
'Search filter for groups e.g. "(&(objectclass=groupOfNames)(memberUid=%s))"',
type: "string"
type: "string",
},
{
name: "username_search_base_dn",
required: false,
label: "Username Search Base DN",
tooltip: "List of username search DNs, you can write one per field",
type: "csv"
type: "csv",
},
{
name: "group_name_attribute",
required: false,
label: "Group Name Attribute",
tooltip: 'Search attribute for group name e.g. "cn"',
type: "string"
type: "string",
},
{
name: "sts_expiry",
@@ -285,7 +285,7 @@ export const fieldsConfigurations: any = {
label: "STS Expiry",
tooltip:
'temporary credentials validity duration in s,m,h,d. Default is "1h"',
type: "string"
type: "string",
},
{
name: "tls_skip_verify",
@@ -293,7 +293,7 @@ export const fieldsConfigurations: any = {
label: "TLS Skip Verify",
tooltip:
'Trust server TLS without verification, defaults to "off" (verify)',
type: "on|off"
type: "on|off",
},
{
name: "server_insecure",
@@ -301,7 +301,7 @@ export const fieldsConfigurations: any = {
label: "Server Insecure",
tooltip:
'Allow plain text connection to AD/LDAP server, defaults to "off"',
type: "on|off"
type: "on|off",
},
{
name: "comment",
@@ -309,28 +309,28 @@ export const fieldsConfigurations: any = {
label: "Comment",
tooltip: "Optionally add a comment to this setting",
type: "string",
multiline: true
}
multiline: true,
},
],
policy_opa: [
{
name: "url",
required: true,
label: "OPA URL",
type: "string"
type: "string",
},
{
name: "auth_token",
required: true,
label: "Auth Token",
type: "string"
type: "string",
},
{
name: "policy_opa",
required: true,
label: "Policy OPA",
type: "string"
}
type: "string",
},
],
kms_vault: [],
kms_kes: [],
@@ -339,29 +339,29 @@ export const fieldsConfigurations: any = {
name: "endpoint",
required: true,
label: "Endpoint",
type: "string"
type: "string",
},
{
name: "auth_token",
required: true,
label: "Auth Token",
type: "string"
}
type: "string",
},
],
audit_webhook: [
{
name: "endpoint",
required: true,
label: "Endpoint",
type: "string"
type: "string",
},
{
name: "auth_token",
required: true,
label: "Auth Token",
type: "string"
}
]
type: "string",
},
],
};
const commonFields = [
@@ -371,7 +371,7 @@ const commonFields = [
required: true,
tooltip: "staging dir for undelivered messages e.g. '/home/events'",
type: "string"
type: "string",
},
{
name: "queue-limit",
@@ -379,15 +379,15 @@ const commonFields = [
required: false,
tooltip: "maximum limit for undelivered messages, defaults to '10000'",
type: "number"
type: "number",
},
{
name: "comment",
label: "Comment",
required: false,
type: "string",
multiline: true
}
multiline: true,
},
];
export const notificationEndpointsFields: any = {
@@ -398,77 +398,77 @@ export const notificationEndpointsFields: any = {
required: true,
tooltip: "comma separated list of Kafka broker addresses",
type: "string"
type: "string",
},
{
name: "topic",
label: "Topic",
tooltip: "Kafka topic used for bucket notifications",
type: "string"
type: "string",
},
{
name: "sasl_username",
label: "SASL Username",
tooltip: "username for SASL/PLAIN or SASL/SCRAM authentication",
type: "string"
type: "string",
},
{
name: "sasl_password",
label: "SASL Password",
tooltip: "password for SASL/PLAIN or SASL/SCRAM authentication",
type: "string"
type: "string",
},
{
name: "sasl_mechanism",
label: "SASL Mechanism",
tooltip: "sasl authentication mechanism, default 'PLAIN'",
type: "string"
type: "string",
},
{
name: "tls_client_auth",
label: "TLS Client Auth",
tooltip:
"clientAuth determines the Kafka server's policy for TLS client auth",
type: "string"
type: "string",
},
{
name: "sasl",
label: "SASL",
tooltip: "set to 'on' to enable SASL authentication",
type: "on|off"
type: "on|off",
},
{
name: "tls",
label: "TLS",
tooltip: "set to 'on' to enable TLS",
type: "on|off"
type: "on|off",
},
{
name: "tls_skip_verify",
label: "TLS skip verify",
tooltip:
'trust server TLS without verification, defaults to "on" (verify)',
type: "on|off"
type: "on|off",
},
{
name: "client_tls_cert",
label: "client TLS cert",
tooltip: "path to client certificate for mTLS auth",
type: "path"
type: "path",
},
{
name: "client_tls_key",
label: "client TLS key",
tooltip: "path to client key for mTLS auth",
type: "path"
type: "path",
},
{
name: "version",
label: "Version",
tooltip: "specify the version of the Kafka cluster e.g '2.2.0'",
type: "string"
type: "string",
},
...commonFields
...commonFields,
],
[notifyAmqp]: [
{
@@ -477,68 +477,68 @@ export const notificationEndpointsFields: any = {
label: "url",
tooltip:
"AMQP server endpoint e.g. `amqp://myuser:mypassword@localhost:5672`",
type: "url"
type: "url",
},
{
name: "exchange",
label: "exchange",
tooltip: "name of the AMQP exchange",
type: "string"
type: "string",
},
{
name: "exchange_type",
label: "exchange_type",
tooltip: "AMQP exchange type",
type: "string"
type: "string",
},
{
name: "routing_key",
label: "routing_key",
tooltip: "routing key for publishing",
type: "string"
type: "string",
},
{
name: "mandatory",
label: "mandatory",
tooltip:
"quietly ignore undelivered messages when set to 'off', default is 'on'",
type: "on|off"
type: "on|off",
},
{
name: "durable",
label: "durable",
tooltip:
"persist queue across broker restarts when set to 'on', default is 'off'",
type: "on|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"
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"
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"
type: "on|off",
},
{
name: "delivery_mode",
label: "delivery_mode",
tooltip: "set to '1' for non-persistent or '2' for persistent queue",
type: "number"
type: "number",
},
...commonFields
...commonFields,
],
[notifyRedis]: [
{
@@ -546,22 +546,22 @@ export const notificationEndpointsFields: any = {
required: true,
label: "address",
tooltip: "Redis server's address. For example: `localhost:6379`",
type: "address"
type: "address",
},
{
name: "key",
required: true,
label: "key",
tooltip: "Redis key to store/update events, key is auto-created",
type: "string"
type: "string",
},
{
name: "password",
label: "password",
tooltip: "Redis server password",
type: "string"
type: "string",
},
...commonFields
...commonFields,
],
[notifyMqtt]: [
{
@@ -569,46 +569,46 @@ export const notificationEndpointsFields: any = {
required: true,
label: "broker",
tooltip: "MQTT server endpoint e.g. `tcp://localhost:1883`",
type: "uri"
type: "uri",
},
{
name: "topic",
required: true,
label: "topic",
tooltip: "name of the MQTT topic to publish",
type: "string"
type: "string",
},
{
name: "username",
label: "username",
tooltip: "MQTT username",
type: "string"
type: "string",
},
{
name: "password",
label: "password",
tooltip: "MQTT password",
type: "string"
type: "string",
},
{
name: "qos",
label: "qos",
tooltip: "set the quality of service priority, defaults to '0'",
type: "number"
type: "number",
},
{
name: "keep_alive_interval",
label: "keep_alive_interval",
tooltip: "keep-alive interval for MQTT connections in s,m,h,d",
type: "duration"
type: "duration",
},
{
name: "reconnect_interval",
label: "reconnect_interval",
tooltip: "reconnect interval for MQTT connections in s,m,h,d",
type: "duration"
type: "duration",
},
...commonFields
...commonFields,
],
[notifyNats]: [
{
@@ -616,95 +616,95 @@ export const notificationEndpointsFields: any = {
required: true,
label: "address",
tooltip: "NATS server address e.g. '0.0.0.0:4222'",
type: "address"
type: "address",
},
{
name: "subject",
required: true,
label: "subject",
tooltip: "NATS subscription subject",
type: "string"
type: "string",
},
{
name: "username",
label: "username",
tooltip: "NATS username",
type: "string"
type: "string",
},
{
name: "password",
label: "password",
tooltip: "NATS password",
type: "string"
type: "string",
},
{
name: "token",
label: "token",
tooltip: "NATS token",
type: "string"
type: "string",
},
{
name: "tls",
label: "tls",
tooltip: "set to 'on' to enable TLS",
type: "on|off"
type: "on|off",
},
{
name: "tls_skip_verify",
label: "tls_skip_verify",
tooltip:
'trust server TLS without verification, defaults to "on" (verify)',
type: "on|off"
type: "on|off",
},
{
name: "ping_interval",
label: "ping_interval",
tooltip: "client ping commands interval in s,m,h,d. Disabled by default",
type: "duration"
type: "duration",
},
{
name: "streaming",
label: "streaming",
tooltip: "set to 'on', to use streaming NATS server",
type: "on|off"
type: "on|off",
},
{
name: "streaming_async",
label: "streaming_async",
tooltip: "set to 'on', to enable asynchronous publish",
type: "on|off"
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"
type: "number",
},
{
name: "streaming_cluster_id",
label: "streaming_cluster_id",
tooltip: "unique ID for NATS streaming cluster",
type: "string"
type: "string",
},
{
name: "cert_authority",
label: "cert_authority",
tooltip: "path to certificate chain of the target NATS server",
type: "string"
type: "string",
},
{
name: "client_cert",
label: "client_cert",
tooltip: "client cert for NATS mTLS auth",
type: "string"
type: "string",
},
{
name: "client_key",
label: "client_key",
tooltip: "client cert key for NATS mTLS auth",
type: "string"
type: "string",
},
...commonFields
...commonFields,
],
[notifyElasticsearch]: [
{
@@ -713,7 +713,7 @@ export const notificationEndpointsFields: any = {
label: "url",
tooltip:
"Elasticsearch server's address, with optional authentication info",
type: "url"
type: "url",
},
{
name: "index",
@@ -721,7 +721,7 @@ export const notificationEndpointsFields: any = {
label: "index",
tooltip:
"Elasticsearch index to store/update events, index is auto-created",
type: "string"
type: "string",
},
{
name: "format",
@@ -729,9 +729,9 @@ export const notificationEndpointsFields: any = {
label: "format",
tooltip:
"'namespace' reflects current bucket/object list and 'access' reflects a journal of object operations, defaults to 'namespace'",
type: "enum"
type: "enum",
},
...commonFields
...commonFields,
],
[notifyWebhooks]: [
{
@@ -740,15 +740,15 @@ export const notificationEndpointsFields: any = {
label: "endpoint",
tooltip:
"webhook server endpoint e.g. http://localhost:8080/minio/events",
type: "url"
type: "url",
},
{
name: "auth_token",
label: "auth_token",
tooltip: "opaque string or JWT authorization token",
type: "string"
type: "string",
},
...commonFields
...commonFields,
],
[notifyNsq]: [
{
@@ -756,34 +756,34 @@ export const notificationEndpointsFields: any = {
required: true,
label: "nsqd_address",
tooltip: "NSQ server address e.g. '127.0.0.1:4150'",
type: "address"
type: "address",
},
{
name: "topic",
required: true,
label: "topic",
tooltip: "NSQ topic",
type: "string"
type: "string",
},
{
name: "tls",
label: "tls",
tooltip: "set to 'on' to enable TLS",
type: "on|off"
type: "on|off",
},
{
name: "tls_skip_verify",
label: "tls_skip_verify",
tooltip:
'trust server TLS without verification, defaults to "on" (verify)',
type: "on|off"
type: "on|off",
},
...commonFields
]
...commonFields,
],
};
export const removeEmptyFields = (formFields: IElementValue[]) => {
const nonEmptyFields = formFields.filter(field => field.value !== "");
const nonEmptyFields = formFields.filter((field) => field.value !== "");
return nonEmptyFields;
};

View File

@@ -224,6 +224,7 @@ const Console = ({
console.log(err);
});
};
const allowedPages = session.pages.reduce(
(result: any, item: any, index: any) => {
result[item] = true;
@@ -278,7 +279,7 @@ const Console = ({
},
{
component: ServiceAccounts,
path: "/service_accounts",
path: "/service-accounts",
},
{
component: WebhookPanel,
@@ -290,6 +291,7 @@ const Console = ({
},
];
const allowedRoutes = routes.filter((route: any) => allowedPages[route.path]);
return (
<React.Fragment>
{session.status == "ok" ? (

View File

@@ -26,7 +26,7 @@ import { CreateIcon } from "../../../icons";
import api from "../../../common/api";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import { GroupsList } from "./types";
import { groupsSort } from "../../../utils/sortFunctions";
import { stringSort } from "../../../utils/sortFunctions";
import AddGroup from "../Groups/AddGroup";
import DeleteGroup from "./DeleteGroup";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
@@ -39,50 +39,50 @@ interface IGroupsProps {
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px"
padding: "20px",
},
errorBlock: {
color: "red"
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
paddingRight: theme.spacing(0),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
}
boxShadow: "0px 3px 6px #00000012",
},
});
const Groups = ({ classes }: IGroupsProps) => {
@@ -126,7 +126,7 @@ const Groups = ({ classes }: IGroupsProps) => {
.then((res: GroupsList) => {
let resGroups: string[] = [];
if (res.groups !== null) {
resGroups = res.groups.sort(groupsSort);
resGroups = res.groups.sort(stringSort);
}
setRecords(resGroups);
const total = !res.total ? 0 : res.total;
@@ -140,7 +140,7 @@ const Groups = ({ classes }: IGroupsProps) => {
setPage(newPage);
}
})
.catch(err => {
.catch((err) => {
setError(err);
isLoading(false);
});
@@ -162,7 +162,7 @@ const Groups = ({ classes }: IGroupsProps) => {
}
};
const filteredRecords = records.filter(elementItem =>
const filteredRecords = records.filter((elementItem) =>
elementItem.includes(filter)
);
@@ -178,7 +178,7 @@ const Groups = ({ classes }: IGroupsProps) => {
const tableActions = [
{ type: "view", onClick: viewAction },
{ type: "delete", onClick: deleteAction }
{ type: "delete", onClick: deleteAction },
];
return (
@@ -217,9 +217,9 @@ const Groups = ({ classes }: IGroupsProps) => {
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
onChange={e => {
onChange={(e) => {
setFilter(e.target.value);
}}
/>
@@ -255,11 +255,11 @@ const Groups = ({ classes }: IGroupsProps) => {
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>

View File

@@ -38,43 +38,43 @@ interface IGroupsProps {
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px"
padding: "20px",
},
errorBlock: {
color: "red"
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
paddingRight: theme.spacing(0),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
actionsTray: {
textAlign: "left",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
filterField: {
background: "#FFFFFF",
@@ -82,24 +82,24 @@ const styles = (theme: Theme) =>
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
width: "100%",
zIndex: 500
zIndex: 500,
},
noFound: {
textAlign: "center",
padding: "10px 0"
padding: "10px 0",
},
tableContainer: {
maxHeight: 250
maxHeight: 250,
},
stickyHeader: {
backgroundColor: "#fff"
}
backgroundColor: "#fff",
},
});
const UsersSelectors = ({
classes,
selectedUsers,
setSelectedUsers
setSelectedUsers,
}: IGroupsProps) => {
//Local States
const [records, setRecords] = useState<any[]>([]);
@@ -133,7 +133,7 @@ const UsersSelectors = ({
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter(element => element !== value);
elements = elements.filter((element) => element !== value);
}
setSelectedUsers(elements);
@@ -154,13 +154,13 @@ const UsersSelectors = ({
setError("");
isLoading(false);
})
.catch(err => {
.catch((err) => {
setError(err);
isLoading(false);
});
};
const filteredRecords = records.filter(elementItem =>
const filteredRecords = records.filter((elementItem) =>
elementItem.accessKey.includes(filter)
);
@@ -185,9 +185,9 @@ const UsersSelectors = ({
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
onChange={e => {
onChange={(e) => {
setFilter(e.target.value);
}}
/>

View File

@@ -33,12 +33,12 @@ export type LogActionTypes = LogMessageReceivedAction | LogResetMessagesAction;
export function logMessageReceived(message: LogMessage) {
return {
type: LOG_MESSAGE_RECEIVED,
message: message
message: message,
};
}
export function logResetMessages() {
return {
type: LOG_RESET_MESSAGES
type: LOG_RESET_MESSAGES,
};
}

View File

@@ -17,7 +17,7 @@
import {
LOG_MESSAGE_RECEIVED,
LOG_RESET_MESSAGES,
LogActionTypes
LogActionTypes,
} from "./actions";
import { LogMessage } from "./types";
@@ -26,7 +26,7 @@ export interface LogState {
}
const initialState: LogState = {
messages: []
messages: [],
};
export function logReducer(
@@ -37,12 +37,12 @@ export function logReducer(
case LOG_MESSAGE_RECEIVED:
return {
...state,
messages: [...state.messages, action.message]
messages: [...state.messages, action.message],
};
case LOG_RESET_MESSAGES:
return {
...state,
messages: []
messages: [],
};
default:
return state;

View File

@@ -17,6 +17,7 @@
import React from "react";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import RoomServiceIcon from "@material-ui/icons/RoomService";
import WebAssetIcon from "@material-ui/icons/WebAsset";
import CenterFocusWeakIcon from "@material-ui/icons/CenterFocusWeak";
import ListItemText from "@material-ui/core/ListItemText";
@@ -136,6 +137,14 @@ class Menu extends React.Component<MenuProps> {
name: "Buckets",
icon: <BucketsIcon />,
},
{
type: "item",
component: NavLink,
to: "/service-accounts",
name: "Service Accounts",
icon: <RoomServiceIcon />,
forceDisplay: true,
},
{
type: "item",
component: NavLink,

View File

@@ -31,14 +31,14 @@ import {
Radio,
RadioGroup,
Select,
TextField
TextField,
} from "@material-ui/core";
import {
createStyles,
lighten,
makeStyles,
Theme,
withStyles
withStyles,
} from "@material-ui/core/styles";
import api from "../../../common/api";
import clsx from "clsx";
@@ -61,21 +61,21 @@ const useToolbarStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(1)
paddingRight: theme.spacing(1),
},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85)
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark
backgroundColor: theme.palette.secondary.dark,
},
title: {
flex: "1 1 100%"
}
flex: "1 1 100%",
},
})
);
@@ -90,7 +90,7 @@ const EnhancedTableToolbar = (props: EnhancedTableToolbarProps) => {
return (
<Toolbar
className={clsx(classes.root, {
[classes.highlight]: numSelected > 0
[classes.highlight]: numSelected > 0,
})}
>
{numSelected > 0 ? (
@@ -122,8 +122,8 @@ const EnhancedTableToolbar = (props: EnhancedTableToolbarProps) => {
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
});
interface IAddPermissionContentProps {
@@ -164,7 +164,7 @@ class AddPermissionContent extends React.Component<
resources: [],
buckets: [],
bucketsError: "",
loadingBuckets: false
loadingBuckets: false,
};
componentDidMount(): void {
@@ -176,10 +176,10 @@ class AddPermissionContent extends React.Component<
this.setState({
loadingBuckets: false,
buckets: res.buckets,
bucketsError: ""
bucketsError: "",
});
})
.catch(err => {
.catch((err) => {
this.setState({ loadingBuckets: false, bucketsError: err });
});
});
@@ -190,8 +190,8 @@ class AddPermissionContent extends React.Component<
name: selectedPermission.name,
description: selectedPermission.description,
effect: selectedPermission.effect,
resources: selectedPermission.resources.map(r => r.bucket_name),
action: selectedPermission.actions[0].type
resources: selectedPermission.resources.map((r) => r.bucket_name),
action: selectedPermission.actions[0].type,
});
}
}
@@ -204,7 +204,7 @@ class AddPermissionContent extends React.Component<
resources,
description,
effect,
action
action,
} = this.state;
const { selectedPermission } = this.props;
if (addLoading) {
@@ -219,23 +219,23 @@ class AddPermissionContent extends React.Component<
description: description,
effect: effect,
resources: resources,
actions: [action]
actions: [action],
})
.then(res => {
.then((res) => {
this.setState(
{
addLoading: false,
addError: ""
addError: "",
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
addLoading: false,
addError: err
addError: err,
});
});
} else {
@@ -245,23 +245,23 @@ class AddPermissionContent extends React.Component<
description: description,
effect: effect,
resources: resources,
actions: [action]
actions: [action],
})
.then(res => {
.then((res) => {
this.setState(
{
addLoading: false,
addError: ""
addError: "",
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch(err => {
.catch((err) => {
this.setState({
addLoading: false,
addError: err
addError: err,
});
});
}
@@ -280,14 +280,14 @@ class AddPermissionContent extends React.Component<
name,
description,
effect,
action
action,
} = this.state;
const handleSelectAllClick = (
event: React.ChangeEvent<HTMLInputElement>
) => {
if (event.target.checked) {
const newSelecteds = buckets.map(n => n.name);
const newSelecteds = buckets.map((n) => n.name);
this.setState({ resources: newSelecteds });
return;
}
@@ -432,7 +432,7 @@ class AddPermissionContent extends React.Component<
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts"
"aria-label": "select all desserts",
}}
/>
</TableCell>
@@ -452,7 +452,9 @@ class AddPermissionContent extends React.Component<
return (
<TableRow
hover
onClick={event => handleClick(event, row.name)}
onClick={(event) =>
handleClick(event, row.name)
}
role="checkbox"
aria-checked={isItemSelected}
tabIndex={-1}
@@ -500,7 +502,7 @@ class AddPermissionContent extends React.Component<
value={action}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
action: (event.target as HTMLInputElement).value
action: (event.target as HTMLInputElement).value,
});
}}
>

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// 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
@@ -14,363 +14,100 @@
// 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 React, { useEffect, useState } from "react";
import Grid from "@material-ui/core/Grid";
import { UnControlled as CodeMirror } from "react-codemirror2";
import Typography from "@material-ui/core/Typography";
import { Button, FormControlLabel, LinearProgress } from "@material-ui/core";
import {
createStyles,
lighten,
makeStyles,
Theme,
withStyles
} from "@material-ui/core/styles";
import api from "../../../common/api";
import clsx from "clsx";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TablePagination from "@material-ui/core/TablePagination";
import TableRow from "@material-ui/core/TableRow";
import Toolbar from "@material-ui/core/Toolbar";
import Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import Tooltip from "@material-ui/core/Tooltip";
import FilterListIcon from "@material-ui/icons/FilterList";
import { Permission, PermissionList } from "../Permissions/types";
import {
NewServiceAccount,
ServiceAccount,
ServiceAccountDetails
} from "./types";
import Switch from "@material-ui/core/Switch";
import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import api from "../../../common/api";
import "codemirror/lib/codemirror.css";
import "codemirror/theme/material.css";
import { NewServiceAccount } from "./types";
const useToolbarStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(1)
},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85)
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark
},
title: {
flex: "1 1 100%"
}
})
);
interface EnhancedTableToolbarProps {
numSelected: number;
}
const EnhancedTableToolbar = (props: EnhancedTableToolbarProps) => {
const classes = useToolbarStyles();
const { numSelected } = props;
return (
<Toolbar
className={clsx(classes.root, {
[classes.highlight]: numSelected > 0
})}
>
{numSelected > 0 ? (
<Typography
className={classes.title}
color="inherit"
variant="subtitle1"
>
{numSelected} selected
</Typography>
) : (
<Typography className={classes.title} variant="h6" id="tableTitle">
Permissions
</Typography>
)}
{numSelected > 0 ? (
<span />
) : (
<Tooltip title="Filter list">
<IconButton aria-label="filter list">
<FilterListIcon />
</IconButton>
</Tooltip>
)}
</Toolbar>
);
};
require("codemirror/mode/javascript/javascript");
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
jsonPolicyEditor: {
minHeight: 400,
width: "100%",
},
codeMirror: {
fontSize: 14,
},
buttonContainer: {
textAlign: "right",
},
...modalBasic,
});
interface IAddServiceAccountContentProps {
interface IAddServiceAccountProps {
classes: any;
open: boolean;
closeModalAndRefresh: (res: NewServiceAccount | null) => void;
selectedServiceAccount: ServiceAccount | null;
}
interface IAddServiceAccountContentState {
addLoading: boolean;
addError: string;
name: string;
enabled: boolean;
selectedPermissions: Permission[];
rowsPerPage: number;
page: number;
permissions: Permission[];
permissionsError: string;
loadingPermissions: boolean;
loadingServiceAccount: boolean;
}
const AddServiceAccount = ({
classes,
open,
closeModalAndRefresh,
}: IAddServiceAccountProps) => {
const [addSending, setAddSending] = useState(false);
const [addError, setAddError] = useState("");
const [policyDefinition, setPolicyDefinition] = useState("");
class AddServiceAccountContent extends React.Component<
IAddServiceAccountContentProps,
IAddServiceAccountContentState
> {
state: IAddServiceAccountContentState = {
addLoading: false,
addError: "",
name: "",
enabled: true,
selectedPermissions: [],
rowsPerPage: 5,
page: 0,
permissions: [],
permissionsError: "",
loadingPermissions: false,
loadingServiceAccount: false
useEffect(() => {
if (addSending) {
api
.invoke("POST", "/api/v1/service-accounts", {
policy: policyDefinition,
})
.then((res) => {
setAddSending(false);
setAddError("");
closeModalAndRefresh(res);
})
.catch((err) => {
setAddSending(false);
setAddError(err);
});
}
}, [
addSending,
setAddSending,
setAddError,
policyDefinition,
closeModalAndRefresh,
]);
const addServiceAccount = (e: React.FormEvent) => {
e.preventDefault();
setAddSending(true);
};
componentDidMount(): void {
// load a list of permissions
this.setState({ loadingPermissions: true }, () => {
api
.invoke("GET", `/api/v1/permissions?limit=1000`)
.then((res: PermissionList) => {
this.setState({
loadingPermissions: false,
permissions: res.permissions,
permissionsError: ""
});
})
.catch(err => {
this.setState({ loadingPermissions: false, permissionsError: err });
});
});
const { selectedServiceAccount } = this.props;
if (selectedServiceAccount !== null) {
this.setState({ loadingServiceAccount: true }, () => {
api
.invoke(
"GET",
`/api/v1/service_accounts/${selectedServiceAccount.id}`
)
.then((res: ServiceAccountDetails) => {
console.log(res);
this.setState({
loadingServiceAccount: false,
name: selectedServiceAccount.name,
enabled: selectedServiceAccount.enabled,
selectedPermissions:
res.permissions === undefined || res.permissions === null
? []
: res.permissions
});
})
.catch(err => {
this.setState({ loadingServiceAccount: false });
});
});
}
}
saveRecord(event: React.FormEvent) {
event.preventDefault();
const { name, addLoading, selectedPermissions, enabled } = this.state;
const { selectedServiceAccount } = this.props;
if (addLoading) {
return;
}
this.setState({ addLoading: true }, () => {
if (selectedServiceAccount !== null) {
api
.invoke(
"PUT",
`/api/v1/service_accounts/${selectedServiceAccount.id}`,
{
id: selectedServiceAccount.id,
name: name,
enabled: enabled,
permission_ids: selectedPermissions.map(p => p.id)
}
)
.then(res => {
this.setState(
{
addLoading: false,
addError: ""
},
() => {
this.props.closeModalAndRefresh(null);
}
);
})
.catch(err => {
this.setState({
addLoading: false,
addError: err
});
});
} else {
api
.invoke("POST", "/api/v1/service_accounts", {
name: name,
permission_ids: selectedPermissions.map(p => p.id)
})
.then((res: NewServiceAccount) => {
this.setState(
{
addLoading: false,
addError: ""
},
() => {
this.props.closeModalAndRefresh(res);
}
);
})
.catch(err => {
this.setState({
addLoading: false,
addError: err
});
});
}
});
}
render() {
const {
classes,
selectedServiceAccount,
open,
closeModalAndRefresh
} = this.props;
const {
addLoading,
addError,
page,
rowsPerPage,
permissions,
selectedPermissions,
name,
loadingServiceAccount
} = this.state;
const handleSelectAllClick = (
event: React.ChangeEvent<HTMLInputElement>
) => {
if (event.target.checked) {
// const newSelecteds = permissions.map(n => n.name);
const newSelecteds = [...permissions];
this.setState({ selectedPermissions: newSelecteds });
return;
}
this.setState({ selectedPermissions: [] });
};
const handleClick = (
event: React.MouseEvent<unknown>,
perm: Permission
) => {
let newSelected: Permission[] = [...selectedPermissions];
if (newSelected.filter(p => p.id === perm.id).length === 0) {
newSelected.push(perm);
} else {
let selectedIndex = -1;
for (let i = 0; i < newSelected.length; i++) {
if (newSelected[i].id === perm.id) {
selectedIndex = i;
break;
}
}
if (selectedIndex >= 0) {
newSelected = [
...newSelected.slice(0, selectedIndex),
...newSelected.slice(selectedIndex + 1)
];
}
}
this.setState({ selectedPermissions: newSelected });
};
const handleChangePage = (event: unknown, newPage: number) => {
this.setState({ page: newPage });
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
this.setState({ page: 0, rowsPerPage: parseInt(event.target.value, 10) });
};
const isSelected = (perm: Permission) =>
selectedPermissions.filter(p => p.id === perm.id).length > 0;
const emptyRows =
rowsPerPage -
Math.min(rowsPerPage, permissions.length - page * rowsPerPage);
const handleChange = (name: string) => (
event: React.ChangeEvent<HTMLInputElement>
) => {
this.setState({ enabled: event.target.checked });
};
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
this.setState({ addError: "" }, () => {
closeModalAndRefresh(null);
});
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModalAndRefresh(null);
}}
title={`Create Service Account`}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
addServiceAccount(e);
}}
title={
selectedServiceAccount !== null
? "Edit Service Account"
: "Create Service Account"
}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
this.saveRecord(e);
}}
>
<Grid container>
{loadingServiceAccount && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
@@ -383,155 +120,37 @@ class AddServiceAccountContent extends React.Component<
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="service-account-name"
name="service-account-name"
label="Name"
value={name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ name: e.target.value });
<CodeMirror
className={classes.codeMirror}
options={{
mode: "javascript",
lineNumbers: true,
}}
onChange={(editor, data, value) => {
setPolicyDefinition(value);
}}
/>
</Grid>
<Grid item xs={12}>
<div className={classes.root}>
<EnhancedTableToolbar
numSelected={selectedPermissions.length}
/>
<TableContainer>
<Table
className={classes.table}
aria-labelledby="tableTitle"
size={"small"}
aria-label="enhanced table"
>
<TableHead>
<TableRow>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selectedPermissions.length > 0 &&
selectedPermissions.length < permissions.length
}
checked={
selectedPermissions.length > 0 &&
selectedPermissions.length === permissions.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts"
}}
/>
</TableCell>
<TableCell>Permission</TableCell>
<TableCell>Description</TableCell>
</TableRow>
</TableHead>
<TableBody>
{permissions
.slice(
page * rowsPerPage,
page * rowsPerPage + rowsPerPage
)
.map((row, index) => {
const isItemSelected = isSelected(row);
const labelId = `enhanced-table-checkbox-${index}`;
return (
<TableRow
hover
onClick={event => handleClick(event, row)}
role="checkbox"
aria-checked={isItemSelected}
tabIndex={-1}
key={row.name}
selected={isItemSelected}
>
<TableCell padding="checkbox">
<Checkbox
checked={isItemSelected}
inputProps={{ "aria-labelledby": labelId }}
/>
</TableCell>
<TableCell id={labelId}>{row.name}</TableCell>
<TableCell>{row.description}</TableCell>
</TableRow>
);
})}
{emptyRows > 0 && (
<TableRow style={{ height: 33 * emptyRows }}>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
component="div"
count={permissions.length}
rowsPerPage={rowsPerPage}
page={page}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
/>
</div>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<FormControlLabel
control={
<Switch onChange={handleChange("enabled")} value="checkedA" />
}
label="Enabled"
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading}
>
Save
</Button>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
}
}
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addSending}
>
Create
</Button>
</Grid>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
};
const AddServiceAccountWrapper = withStyles(styles)(AddServiceAccountContent);
interface IAddServiceAccountProps {
open: boolean;
closeModalAndRefresh: (res: NewServiceAccount | null) => void;
selectedServiceAccount: ServiceAccount | null;
}
interface IAddServiceAccountState {}
class AddServiceAccount extends React.Component<
IAddServiceAccountProps,
IAddServiceAccountState
> {
state: IAddServiceAccountState = {};
render() {
return <AddServiceAccountWrapper {...this.props} />;
}
}
export default AddServiceAccount;
export default withStyles(styles)(AddServiceAccount);

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// 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
@@ -17,20 +17,19 @@
import React from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { NewServiceAccount } from "./types";
import {
Button,
DialogActions,
DialogContent,
DialogContentText
} from "@material-ui/core";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import Grid from "@material-ui/core/Grid";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
color: "red",
},
buttonContainer: {
textAlign: "right",
},
});
interface ICredentialsPromptProps {
@@ -40,76 +39,69 @@ interface ICredentialsPromptProps {
closeModal: () => void;
}
interface ICredentialsPromptState {}
const download = (filename: string, text: string) => {
let element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text)
);
element.setAttribute("download", filename);
class CredentialsPrompt extends React.Component<
ICredentialsPromptProps,
ICredentialsPromptState
> {
state: ICredentialsPromptState = {};
element.style.display = "none";
document.body.appendChild(element);
download(filename: string, text: string) {
var element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text)
);
element.setAttribute("download", filename);
element.click();
element.style.display = "none";
document.body.appendChild(element);
document.body.removeChild(element);
};
element.click();
document.body.removeChild(element);
const CredentialsPrompt = ({
classes,
newServiceAccount,
open,
closeModal,
}: ICredentialsPromptProps) => {
if (!newServiceAccount) {
return null;
}
render() {
const { classes, open, newServiceAccount } = this.props;
if (newServiceAccount === null) {
return <div />;
}
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
this.props.closeModal();
}}
title="New Service Account"
>
<React.Fragment>
<DialogContent>
<DialogContentText id="alert-dialog-description">
A new service account has been created with the following details:
<ul>
<li>
<b>Access Key:</b>{" "}
{newServiceAccount.service_account.access_key}
</li>
<li>
<b>Secret Key:</b> {newServiceAccount.secret_key}
</li>
</ul>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
Write these down, as this is the only time the secret will be
displayed.
</Typography>
</DialogContentText>
</DialogContent>
<DialogActions>
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModal();
}}
title="New Service Account Created"
>
<React.Fragment>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
A new service account has been created with the following details:
<ul>
<li>
<b>Access Key:</b> {newServiceAccount.accessKey}
</li>
<li>
<b>Secret Key:</b> {newServiceAccount.secretKey}
</li>
</ul>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
Write these down, as this is the only time the secret will be
displayed.
</Typography>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
onClick={() => {
this.download(
download(
"credentials.json",
JSON.stringify({
access_key: newServiceAccount.service_account.access_key,
secret_key: newServiceAccount.secret_key
access_key: newServiceAccount.accessKey,
secret_key: newServiceAccount.secretKey,
})
);
}}
@@ -119,18 +111,18 @@ class CredentialsPrompt extends React.Component<
</Button>
<Button
onClick={() => {
this.props.closeModal();
closeModal();
}}
color="secondary"
autoFocus
>
Done
</Button>
</DialogActions>
</React.Fragment>
</ModalWrapper>
);
}
}
</Grid>
</Grid>
</React.Fragment>
</ModalWrapper>
);
};
export default withStyles(styles)(CredentialsPrompt);

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// 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
@@ -14,152 +14,117 @@
// 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 React from "react";
import Typography from "@material-ui/core/Typography";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@material-ui/core";
import api from "../../../common/api";
import { ServiceAccount, ServiceAccountsList } from "./types";
import Typography from "@material-ui/core/Typography";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
},
wrapText: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
}
});
createStyles({
errorBlock: {
color: "red",
},
wrapText: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word",
},
});
interface IDeleteServiceAccountProps {
classes: any;
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedServiceAccount: ServiceAccount | null;
classes: any;
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedServiceAccount: string | null;
}
interface IDeleteServiceAccountState {
deleteLoading: boolean;
deleteError: string;
}
const DeleteServiceAccount = ({
classes,
closeDeleteModalAndRefresh,
deleteOpen,
selectedServiceAccount,
}: IDeleteServiceAccountProps) => {
const [deleteLoading, setDeleteLoading] = useState(false);
const [deleteError, setDeleteError] = useState("");
class DeleteServiceAccount extends React.Component<
IDeleteServiceAccountProps,
IDeleteServiceAccountState
> {
state: IDeleteServiceAccountState = {
deleteLoading: false,
deleteError: ""
};
removeRecord() {
const { deleteLoading } = this.state;
const { selectedServiceAccount } = this.props;
if (deleteLoading) {
return;
}
if (selectedServiceAccount == null) {
return;
}
this.setState({ deleteLoading: true }, () => {
api
.invoke("DELETE", `/api/v1/service_accounts/${selectedServiceAccount.id}`, {
id: selectedServiceAccount.id
})
.then((res: ServiceAccountsList) => {
this.setState(
{
deleteLoading: false,
deleteError: ""
},
() => {
this.props.closeDeleteModalAndRefresh(true);
}
);
})
.catch(err => {
this.setState({
deleteLoading: false,
deleteError: err
});
});
useEffect(() => {
if (deleteLoading) {
api
.invoke("DELETE", `/api/v1/service-accounts/${selectedServiceAccount}`)
.then(() => {
setDeleteLoading(false);
setDeleteError("");
closeDeleteModalAndRefresh(true);
})
.catch((err) => {
setDeleteLoading(false);
setDeleteError(err);
});
}
}, [deleteLoading, closeDeleteModalAndRefresh, selectedServiceAccount]);
render() {
const { classes, deleteOpen, selectedServiceAccount } = this.props;
const { deleteLoading, deleteError } = this.state;
if (selectedServiceAccount === null) {
return <div />;
}
return (
<Dialog
open={deleteOpen}
onClose={() => {
this.setState({ deleteError: "" }, () => {
this.props.closeDeleteModalAndRefresh(false);
});
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete ServiceAccount</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete service account{" "}
<b className={classes.wrapText}>{selectedServiceAccount.name}</b>?
{deleteError !== "" && (
<React.Fragment>
<br />
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{deleteError}
</Typography>
</React.Fragment>
)}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
this.setState({ deleteError: "" }, () => {
this.props.closeDeleteModalAndRefresh(false);
});
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
onClick={() => {
this.removeRecord();
}}
color="secondary"
autoFocus
>
Delete
</Button>
</DialogActions>
</Dialog>
);
const removeRecord = () => {
if (selectedServiceAccount == null) {
return;
}
}
setDeleteLoading(true);
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
closeDeleteModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete ServiceAccount</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete service account{" "}
<b className={classes.wrapText}>{selectedServiceAccount}</b>?
{deleteError !== "" && (
<React.Fragment>
<br />
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{deleteError}
</Typography>
</React.Fragment>
)}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button onClick={removeRecord} color="secondary" autoFocus>
Delete
</Button>
</DialogActions>
</Dialog>
);
};
export default withStyles(styles)(DeleteServiceAccount);

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// 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
@@ -14,401 +14,295 @@
// 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 React, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
import api from "../../../common/api";
import {
Button,
IconButton,
LinearProgress,
TableFooter,
TablePagination
} from "@material-ui/core";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import {
NewServiceAccount,
ServiceAccount,
ServiceAccountsList
} from "./types";
import { NewServiceAccount } from "./types";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import EditIcon from "@material-ui/icons/Edit";
import AddServiceAccount from "./AddServiceAccount";
import DeleteServiceAccount from "./DeleteServiceAccount";
import CredentialsPrompt from "./CredentialsPrompt";
import { CreateIcon, DeleteIcon } from "../../../icons";
import Checkbox from "@material-ui/core/Checkbox";
import PlayArrowRoundedIcon from "@material-ui/icons/PlayArrowRounded";
import { CreateIcon } from "../../../icons";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import { stringSort } from "../../../utils/sortFunctions";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column"
flexDirection: "column",
},
addSideBar: {
width: "480px",
minWidth: "320px",
padding: "20px"
padding: "20px",
},
errorBlock: {
color: "red"
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
paddingRight: theme.spacing(0),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
imageIcon: {
height: "100%"
height: "100%",
},
iconRoot: {
textAlign: "center"
textAlign: "center",
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
}
boxShadow: "0px 3px 6px #00000012",
},
});
interface IServiceAccountsProps {
classes: any;
}
interface IServiceAccountsState {
records: ServiceAccount[];
totalRecords: number;
loading: boolean;
error: string;
deleteError: string;
addScreenOpen: boolean;
page: number;
rowsPerPage: number;
deleteOpen: boolean;
selectedServiceAccount: ServiceAccount | null;
showNewCredentials: boolean;
newServiceAccount: NewServiceAccount | null;
}
const ServiceAccounts = ({ classes }: IServiceAccountsProps) => {
const [records, setRecords] = useState<string[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [filter, setFilter] = useState<string>("");
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
const [page, setPage] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedServiceAccount, setSelectedServiceAccount] = useState<
string | null
>(null);
const [showNewCredentials, setShowNewCredentials] = useState<boolean>(false);
const [
newServiceAccount,
setNewServiceAccount,
] = useState<NewServiceAccount | null>(null);
class ServiceAccounts extends React.Component<
IServiceAccountsProps,
IServiceAccountsState
> {
state: IServiceAccountsState = {
records: [],
totalRecords: 0,
loading: false,
error: "",
deleteError: "",
addScreenOpen: false,
page: 0,
rowsPerPage: 10,
deleteOpen: false,
selectedServiceAccount: null,
showNewCredentials: false,
newServiceAccount: null
};
useEffect(() => {
fetchRecords();
}, []);
fetchRecords() {
this.setState({ loading: true }, () => {
const { page, rowsPerPage } = this.state;
const offset = page * rowsPerPage;
useEffect(() => {
if (loading) {
api
.invoke(
"GET",
`/api/v1/service_accounts?offset=${offset}&limit=${rowsPerPage}`
)
.then((res: ServiceAccountsList) => {
this.setState({
loading: false,
records: res.service_accounts,
totalRecords: res.total,
error: ""
});
.invoke("GET", `/api/v1/service-accounts`)
.then((res: string[]) => {
const serviceAccounts = res.sort(stringSort);
setLoading(false);
setRecords(serviceAccounts);
setError("");
// if we get 0 results, and page > 0 , go down 1 page
if (
(res.service_accounts === undefined ||
res.service_accounts == null ||
res.service_accounts.length === 0) &&
page > 0
) {
if ((!serviceAccounts || serviceAccounts.length === 0) && page > 0) {
const newPage = page - 1;
this.setState({ page: newPage }, () => {
this.fetchRecords();
});
setPage(newPage);
fetchRecords();
}
})
.catch(err => {
this.setState({ loading: false, error: err });
.catch((err) => {
setError(err);
setLoading(false);
});
});
}
closeAddModalAndRefresh(res: NewServiceAccount | null) {
this.setState({ addScreenOpen: false }, () => {
this.fetchRecords();
});
if (res !== null) {
this.setState({ showNewCredentials: true, newServiceAccount: res });
}
}
}, [loading, setLoading, setRecords, setError, page, setPage]);
closeDeleteModalAndRefresh(refresh: boolean) {
this.setState({ deleteOpen: false }, () => {
if (refresh) {
this.fetchRecords();
}
});
}
const fetchRecords = () => {
setLoading(true);
};
componentDidMount(): void {
this.fetchRecords();
}
const closeAddModalAndRefresh = (res: NewServiceAccount | null) => {
setAddScreenOpen(false);
fetchRecords();
closeCredentialsModal() {
this.setState({ showNewCredentials: false, newServiceAccount: null });
}
if (res !== null) {
setShowNewCredentials(true);
setNewServiceAccount(res);
}
};
render() {
const { classes } = this.props;
const {
records,
totalRecords,
addScreenOpen,
loading,
page,
rowsPerPage,
deleteOpen,
selectedServiceAccount,
showNewCredentials,
newServiceAccount
} = this.state;
const closeDeleteModalAndRefresh = (refresh: boolean) => {
setDeleteOpen(false);
const handleChangePage = (event: unknown, newPage: number) => {
this.setState({ page: newPage }, () => {
this.fetchRecords();
});
};
if (refresh) {
fetchRecords();
}
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
this.setState({ page: 0, rowsPerPage: rPP }, () => {
this.fetchRecords();
});
};
const closeCredentialsModal = () => {
setShowNewCredentials(false);
setNewServiceAccount(null);
};
const confirmDeleteServiceAccount = (
selectedServiceAccount: ServiceAccount
) => {
this.setState({
deleteOpen: true,
selectedServiceAccount: selectedServiceAccount
});
};
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const editServiceAccount = (selectedServiceAccount: ServiceAccount) => {
this.setState({
addScreenOpen: true,
selectedServiceAccount: selectedServiceAccount
});
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
return (
<React.Fragment>
{addScreenOpen && (
<AddServiceAccount
open={addScreenOpen}
selectedServiceAccount={selectedServiceAccount}
closeModalAndRefresh={(res: NewServiceAccount | null) => {
this.closeAddModalAndRefresh(res);
}}
/>
)}
{deleteOpen && (
<DeleteServiceAccount
deleteOpen={deleteOpen}
selectedServiceAccount={selectedServiceAccount}
closeDeleteModalAndRefresh={(refresh: boolean) => {
this.closeDeleteModalAndRefresh(refresh);
}}
/>
)}
{showNewCredentials && (
<CredentialsPrompt
newServiceAccount={newServiceAccount}
open={showNewCredentials}
closeModal={() => {
this.closeCredentialsModal();
}}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Service Accounts</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Service Accounts"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
this.setState({
addScreenOpen: true,
selectedServiceAccount: null
});
}}
>
Create service account
</Button>
<Button
variant="contained"
color="primary"
startIcon={<PlayArrowRoundedIcon />}
onClick={() => {
this.setState({
addScreenOpen: true
});
}}
>
Change Access
</Button>
</Grid>
setPage(0);
setRowsPerPage(rPP);
};
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Paper className={classes.paper}>
{loading && <LinearProgress />}
{records != null && records.length > 0 ? (
<Table size="medium">
<TableHead className={classes.minTableHeader}>
<TableRow>
<TableCell>Select</TableCell>
<TableCell>Name</TableCell>
<TableCell>Access Key</TableCell>
<TableCell>Status</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.map(row => (
<TableRow key={row.name}>
<TableCell padding="checkbox">
<Checkbox
value="secondary"
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
/>
</TableCell>
<TableCell className={classes.wrapCell}>
{row.name}
</TableCell>
<TableCell>{row.access_key}</TableCell>
<TableCell>
{row.enabled ? "enabled" : "disabled"}
</TableCell>
<TableCell align="right">
<IconButton
aria-label="edit"
onClick={() => {
editServiceAccount(row);
}}
>
<EditIcon />
</IconButton>
<IconButton
aria-label="delete"
onClick={() => {
confirmDeleteServiceAccount(row);
}}
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
colSpan={4}
count={totalRecords}
rowsPerPage={rowsPerPage}
page={page}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true
}}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
ActionsComponent={MinTablePaginationActions}
/>
</TableRow>
</TableFooter>
</Table>
) : (
<div>No Service Accounts</div>
)}
</Paper>
</Grid>
const confirmDeleteServiceAccount = (selectedServiceAccount: string) => {
setSelectedServiceAccount(selectedServiceAccount);
setDeleteOpen(true);
};
const tableActions = [
{ type: "delete", onClick: confirmDeleteServiceAccount },
];
const filteredRecords = records.filter((elementItem) =>
elementItem.toLowerCase().includes(filter.toLowerCase())
);
const beginRecord = page * rowsPerPage;
const endRecords = beginRecord + rowsPerPage;
const paginatedRecords = filteredRecords.slice(beginRecord, endRecords);
return (
<React.Fragment>
{addScreenOpen && (
<AddServiceAccount
open={addScreenOpen}
closeModalAndRefresh={(res: NewServiceAccount | null) => {
closeAddModalAndRefresh(res);
}}
/>
)}
{deleteOpen && (
<DeleteServiceAccount
deleteOpen={deleteOpen}
selectedServiceAccount={selectedServiceAccount}
closeDeleteModalAndRefresh={(refresh: boolean) => {
closeDeleteModalAndRefresh(refresh);
}}
/>
)}
{showNewCredentials && (
<CredentialsPrompt
newServiceAccount={newServiceAccount}
open={showNewCredentials}
closeModal={() => {
closeCredentialsModal();
}}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Service Accounts</Typography>
</Grid>
</React.Fragment>
);
}
}
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Service Accounts"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
onChange={(e) => {
setFilter(e.target.value);
setPage(0);
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
setSelectedServiceAccount(null);
}}
>
Create service account
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{error !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{error}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<TableWrapper
isLoading={loading}
records={paginatedRecords}
entityName={"Service Accounts"}
idField={""}
columns={[{ label: "Service Account", elementKey: "" }]}
itemActions={tableActions}
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 4,
count: records.length,
rowsPerPage: rowsPerPage,
page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(ServiceAccounts);

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// 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
@@ -14,27 +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 {Permission} from "../Permissions/types";
export interface ServiceAccount {
id: string;
name: string;
slug: string;
access_key: string;
enabled: boolean;
}
export interface ServiceAccountsList {
service_accounts: ServiceAccount[];
total:number;
service_accounts: string[];
total: number;
}
export interface NewServiceAccount {
service_account: ServiceAccount,
secret_key:string,
}
export interface ServiceAccountDetails {
service_account: ServiceAccount,
permissions: Permission[],
accessKey: string;
secretKey: string;
}

View File

@@ -31,15 +31,15 @@ const styles = (theme: Theme) =>
overflow: "auto",
"& ul": {
margin: "4px",
padding: "0px"
padding: "0px",
},
"& ul li": {
listStyle: "none",
margin: "0px",
padding: "0px",
borderBottom: "1px solid #dedede"
}
}
borderBottom: "1px solid #dedede",
},
},
});
interface ITrace {
@@ -53,7 +53,7 @@ const Trace = ({
classes,
traceMessageReceived,
traceResetMessages,
messages
messages,
}: ITrace) => {
useEffect(() => {
traceResetMessages();
@@ -96,7 +96,7 @@ const Trace = ({
<h1>Trace</h1>
<div className={classes.logList}>
<ul>
{messages.map(m => {
{messages.map((m) => {
return (
<li key={m.key}>
{timeFromDate(m.time)} - {m.api}[{m.statusCode} {m.statusMsg}]{" "}
@@ -113,12 +113,12 @@ const Trace = ({
};
const mapState = (state: AppState) => ({
messages: state.trace.messages
messages: state.trace.messages,
});
const connector = connect(mapState, {
traceMessageReceived: traceMessageReceived,
traceResetMessages: traceResetMessages
traceResetMessages: traceResetMessages,
});
export default connector(withStyles(styles)(Trace));

View File

@@ -35,12 +35,12 @@ export type TraceActionTypes =
export function traceMessageReceived(message: TraceMessage) {
return {
type: TRACE_MESSAGE_RECEIVED,
message: message
message: message,
};
}
export function traceResetMessages() {
return {
type: TRACE_RESET_MESSAGES
type: TRACE_RESET_MESSAGES,
};
}

View File

@@ -17,7 +17,7 @@
import {
TRACE_MESSAGE_RECEIVED,
TRACE_RESET_MESSAGES,
TraceActionTypes
TraceActionTypes,
} from "./actions";
import { TraceMessage } from "./types";
@@ -26,7 +26,7 @@ export interface TraceState {
}
const initialState: TraceState = {
messages: []
messages: [],
};
export function traceReducer(
@@ -37,12 +37,12 @@ export function traceReducer(
case TRACE_MESSAGE_RECEIVED:
return {
...state,
messages: [...state.messages, action.message]
messages: [...state.messages, action.message],
};
case TRACE_RESET_MESSAGES:
return {
...state,
messages: []
messages: [],
};
default:
return state;

View File

@@ -24,7 +24,7 @@ import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import TextField from "@material-ui/core/TextField";
import api from "../../../common/api";
import { groupsSort } from "../../../utils/sortFunctions";
import { stringSort } from "../../../utils/sortFunctions";
import { GroupsList } from "../Groups/types";
import get from "lodash/get";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
@@ -38,43 +38,43 @@ interface IGroupsProps {
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px"
padding: "20px",
},
errorBlock: {
color: "red"
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
paddingRight: theme.spacing(0),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
actionsTray: {
textAlign: "left",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
filterField: {
background: "#FFFFFF",
@@ -82,24 +82,24 @@ const styles = (theme: Theme) =>
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
width: "100%",
zIndex: 500
zIndex: 500,
},
noFound: {
textAlign: "center",
padding: "10px 0"
padding: "10px 0",
},
tableContainer: {
maxHeight: 200
maxHeight: 200,
},
stickyHeader: {
backgroundColor: "#fff"
}
backgroundColor: "#fff",
},
});
const GroupsSelectors = ({
classes,
selectedGroups,
setSelectedGroups
setSelectedGroups,
}: IGroupsProps) => {
// Local State
const [records, setRecords] = useState<any[]>([]);
@@ -129,11 +129,11 @@ const GroupsSelectors = ({
if (!groups) {
groups = [];
}
setRecords(groups.sort(groupsSort));
setRecords(groups.sort(stringSort));
setError("");
isLoading(false);
})
.catch(err => {
.catch((err) => {
setError(err);
isLoading(false);
});
@@ -151,14 +151,14 @@ const GroupsSelectors = ({
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter(element => element !== value);
elements = elements.filter((element) => element !== value);
}
setSelectedGroups(elements);
return elements;
};
const filteredRecords = records.filter(elementItem =>
const filteredRecords = records.filter((elementItem) =>
elementItem.includes(filter)
);
@@ -183,9 +183,9 @@ const GroupsSelectors = ({
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
onChange={e => {
onChange={(e) => {
setFilter(e.target.value);
}}
/>

View File

@@ -22,7 +22,7 @@ import {
Grid,
Typography,
TextField,
InputAdornment
InputAdornment,
} from "@material-ui/core";
import SearchIcon from "@material-ui/icons/Search";
import GroupIcon from "@material-ui/icons/Group";
@@ -38,50 +38,50 @@ import TableWrapper from "../Common/TableWrapper/TableWrapper";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px"
padding: "20px",
},
errorBlock: {
color: "red"
color: "red",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
paddingRight: theme.spacing(0),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
fontWeight: "bold",
},
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
marginLeft: 10,
},
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
}
boxShadow: "0px 3px 6px #00000012",
},
});
interface IUsersProps {
@@ -118,7 +118,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
selectedUser: null,
addGroupOpen: false,
filter: "",
checkedUsers: []
checkedUsers: [],
};
fetchRecords() {
@@ -133,7 +133,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
loading: false,
records: users.sort(usersSort),
totalRecords: users.length,
error: ""
error: "",
});
// if we get 0 results, and page > 0 , go down 1 page
if ((!users || users.length === 0) && page > 0) {
@@ -143,7 +143,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
});
}
})
.catch(err => {
.catch((err) => {
this.setState({ loading: false, error: err });
});
});
@@ -191,7 +191,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
selectedUser,
filter,
checkedUsers,
addGroupOpen
addGroupOpen,
} = this.state;
const handleChangePage = (event: unknown, newPage: number) => {
@@ -205,7 +205,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
this.setState({ page: 0, rowsPerPage: rPP });
};
const filteredRecords = records.filter(elementItem =>
const filteredRecords = records.filter((elementItem) =>
elementItem.accessKey.includes(filter)
);
@@ -226,11 +226,11 @@ class Users extends React.Component<IUsersProps, IUsersState> {
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter(element => element !== value);
elements = elements.filter((element) => element !== value);
}
this.setState({
checkedUsers: elements
checkedUsers: elements,
});
return elements;
@@ -239,20 +239,20 @@ class Users extends React.Component<IUsersProps, IUsersState> {
const viewAction = (selectionElement: any): void => {
this.setState({
addScreenOpen: true,
selectedUser: selectionElement
selectedUser: selectionElement,
});
};
const deleteAction = (selectionElement: any): void => {
this.setState({
deleteOpen: true,
selectedUser: selectionElement
selectedUser: selectionElement,
});
};
const tableActions = [
{ type: "view", onClick: viewAction },
{ type: "delete", onClick: deleteAction }
{ type: "delete", onClick: deleteAction },
];
return (
@@ -304,9 +304,9 @@ class Users extends React.Component<IUsersProps, IUsersState> {
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
),
}}
onChange={e => {
onChange={(e) => {
this.setState({ filter: e.target.value, page: 0 });
}}
/>
@@ -318,7 +318,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
onClick={() => {
if (checkedUsers.length > 0) {
this.setState({
addGroupOpen: true
addGroupOpen: true,
});
}
}}
@@ -332,7 +332,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
onClick={() => {
this.setState({
addScreenOpen: true,
selectedUser: null
selectedUser: null,
});
}}
>
@@ -361,11 +361,11 @@ class Users extends React.Component<IUsersProps, IUsersState> {
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>

View File

@@ -14,12 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect, useState } from "react";
import {
Button,
Grid,
Typography,
TextField
} from "@material-ui/core";
import { Button, Grid, Typography, TextField } from "@material-ui/core";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import { AppState } from "../../../store";
import { connect } from "react-redux";
@@ -29,11 +24,7 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { niceBytes, timeFromDate } from "../../../common/utils";
import { wsProtocol } from "../../../utils/wsUtils";
import api from "../../../common/api";
import {
FormControl,
MenuItem,
Select,
} from "@material-ui/core";
import { FormControl, MenuItem, Select } from "@material-ui/core";
const styles = (theme: Theme) =>
createStyles({
@@ -43,27 +34,27 @@ const styles = (theme: Theme) =>
overflow: "auto",
"& ul": {
margin: "4px",
padding: "0px"
padding: "0px",
},
"& ul li": {
listStyle: "none",
margin: "0px",
padding: "0px",
borderBottom: "1px solid #dedede"
}
borderBottom: "1px solid #dedede",
},
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
}
},
},
inputField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
marginLeft: 10,
boxShadow: "0px 3px 6px #00000012"
boxShadow: "0px 3px 6px #00000012",
},
fieldContainer: {
background: "#FFFFFF",
@@ -72,8 +63,8 @@ const styles = (theme: Theme) =>
marginLeft: 10,
textAlign: "left",
minWidth: "206px",
boxShadow: "0px 3px 6px #00000012"
}
boxShadow: "0px 3px 6px #00000012",
},
});
interface IWatch {
@@ -87,7 +78,7 @@ const Watch = ({
classes,
watchMessageReceived,
watchResetMessages,
messages
messages,
}: IWatch) => {
const [start, setStart] = useState(false);
const [bucketName, setBucketName] = useState("Select Bucket");
@@ -108,7 +99,7 @@ const Watch = ({
.catch((err: any) => {
console.log(err);
});
}
};
useEffect(() => {
fetchBucketList();
}, []);
@@ -116,13 +107,15 @@ const Watch = ({
useEffect(() => {
watchResetMessages();
// begin watch if bucketName in bucketList and start pressed
if (start && bucketList.some(bucket => bucket.name === bucketName)) {
if (start && bucketList.some((bucket) => bucket.name === bucketName)) {
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;
const wsProt = wsProtocol(url.protocol);
const c = new W3CWebSocket(`${wsProt}://${url.hostname}:${port}/ws/watch/${bucketName}?prefix=${prefix}&suffix=${suffix}`);
const c = new W3CWebSocket(
`${wsProt}://${url.hostname}:${port}/ws/watch/${bucketName}?prefix=${prefix}&suffix=${suffix}`
);
let interval: any | null = null;
if (c !== null) {
@@ -156,9 +149,9 @@ const Watch = ({
}
}, [watchMessageReceived, start]);
const bucketNames = bucketList.map(bucketName => ({
const bucketNames = bucketList.map((bucketName) => ({
label: bucketName.name,
value: bucketName.name
value: bucketName.name,
}));
return (
<React.Fragment>
@@ -175,7 +168,9 @@ const Watch = ({
id="bucket-name"
name="bucket-name"
value={bucketName}
onChange={(e) => { setBucketName(e.target.value as string) }}
onChange={(e) => {
setBucketName(e.target.value as string);
}}
className={classes.fieldContainer}
disabled={start}
>
@@ -185,8 +180,8 @@ const Watch = ({
disabled={true}
>
Select Bucket
</MenuItem>
{bucketNames.map(option => (
</MenuItem>
{bucketNames.map((option) => (
<MenuItem
value={option.value}
key={`select-bucket-name-${option.label}`}
@@ -205,7 +200,9 @@ const Watch = ({
InputProps={{
disableUnderline: true,
}}
onChange={(e) => { setPrefix(e.target.value) }}
onChange={(e) => {
setPrefix(e.target.value);
}}
/>
<TextField
placeholder="Suffix"
@@ -216,7 +213,9 @@ const Watch = ({
InputProps={{
disableUnderline: true,
}}
onChange={(e) => { setSuffix(e.target.value) }}
onChange={(e) => {
setSuffix(e.target.value);
}}
/>
<Button
type="submit"
@@ -226,7 +225,7 @@ const Watch = ({
onClick={() => setStart(true)}
>
Start
</Button>
</Button>
</Grid>
<Grid item xs={12}>
<br />
@@ -234,10 +233,11 @@ const Watch = ({
</Grid>
<div className={classes.watchList}>
<ul>
{messages.map(m => {
{messages.map((m) => {
return (
<li key={m.key}>
{timeFromDate(m.Time)} - {niceBytes(m.Size + "")} - {m.Type} - {m.Path}
{timeFromDate(m.Time)} - {niceBytes(m.Size + "")} - {m.Type} -{" "}
{m.Path}
</li>
);
})}
@@ -248,12 +248,12 @@ const Watch = ({
};
const mapState = (state: AppState) => ({
messages: state.watch.messages
messages: state.watch.messages,
});
const connector = connect(mapState, {
watchMessageReceived: watchMessageReceived,
watchResetMessages: watchResetMessages
watchResetMessages: watchResetMessages,
});
export default connector(withStyles(styles)(Watch));

View File

@@ -28,7 +28,6 @@ interface WatchResetMessagesAction {
type: typeof WATCH_RESET_MESSAGES;
}
export type WatchActionTypes =
| WatchMessageReceivedAction
| WatchResetMessagesAction;
@@ -36,12 +35,12 @@ export type WatchActionTypes =
export function watchMessageReceived(message: EventInfo) {
return {
type: WATCH_MESSAGE_RECEIVED,
message: message
message: message,
};
}
export function watchResetMessages() {
return {
type: WATCH_RESET_MESSAGES
type: WATCH_RESET_MESSAGES,
};
}

View File

@@ -17,7 +17,7 @@
import {
WATCH_MESSAGE_RECEIVED,
WATCH_RESET_MESSAGES,
WatchActionTypes
WatchActionTypes,
} from "./actions";
import { EventInfo } from "./types";
@@ -26,7 +26,7 @@ export interface WatchState {
}
const initialState: WatchState = {
messages: []
messages: [],
};
export function watchReducer(
@@ -37,12 +37,12 @@ export function watchReducer(
case WATCH_MESSAGE_RECEIVED:
return {
...state,
messages: [...state.messages, action.message]
messages: [...state.messages, action.message],
};
case WATCH_RESET_MESSAGES:
return {
...state,
messages: []
messages: [],
};
default:
return state;

View File

@@ -35,8 +35,8 @@ const styles = (theme: Theme) =>
createStyles({
"@global": {
body: {
backgroundColor: "#F4F4F4"
}
backgroundColor: "#F4F4F4",
},
},
paper: {
marginTop: theme.spacing(16),
@@ -45,48 +45,48 @@ const styles = (theme: Theme) =>
flexDirection: "column",
alignItems: "center",
width: "800px",
margin: "auto"
margin: "auto",
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main
backgroundColor: theme.palette.secondary.main,
},
form: {
width: "100%", // Fix IE 11 issue.
marginTop: theme.spacing(3)
marginTop: theme.spacing(3),
},
submit: {
margin: theme.spacing(3, 0, 2)
margin: theme.spacing(3, 0, 2),
},
errorBlock: {
color: "red"
color: "red",
},
mainContainer: {
borderRadius: "3px"
borderRadius: "3px",
},
theOcean: {
borderTopLeftRadius: "3px",
borderBottomLeftRadius: "3px",
background:
"transparent linear-gradient(333deg, #281B6F 1%, #271260 13%, #120D53 83%) 0% 0% no-repeat padding-box;"
"transparent linear-gradient(333deg, #281B6F 1%, #271260 13%, #120D53 83%) 0% 0% no-repeat padding-box;",
},
oceanBg: {
backgroundImage: "url(/images/BG_Illustration.svg)",
backgroundRepeat: "no-repeat",
backgroundPosition: "bottom left",
height: "100%",
width: "100%"
width: "100%",
},
theLogin: {
padding: "76px 62px 20px 62px"
padding: "76px 62px 20px 62px",
},
loadingLoginStrategy: {
textAlign: "center"
}
textAlign: "center",
},
});
const mapState = (state: SystemState) => ({
loggedIn: state.loggedIn
loggedIn: state.loggedIn,
});
const connector = connect(mapState, { userLoggedIn });
@@ -117,8 +117,8 @@ class Login extends React.Component<ILoginProps, ILoginState> {
loading: false,
loginStrategy: {
loginStrategy: "",
redirect: ""
}
redirect: "",
},
};
fetchConfiguration() {
@@ -127,12 +127,12 @@ class Login extends React.Component<ILoginProps, ILoginState> {
.invoke("GET", "/api/v1/login")
.then((loginDetails: ILoginDetails) => {
this.setState({
loading: false
loading: false,
});
this.setState({
loading: false,
loginStrategy: loginDetails,
error: ""
error: "",
});
})
.catch((err: any) => {
@@ -169,7 +169,7 @@ class Login extends React.Component<ILoginProps, ILoginState> {
// therefore after login we need to use window.location redirect
window.location.href = "/";
})
.catch(err => {
.catch((err) => {
this.setState({ error: `${err}` });
});
};

View File

@@ -29,7 +29,7 @@ export const usersSort = (a: userInterface, b: userInterface) => {
return 0;
};
export const groupsSort = (a: string, b: string) => {
export const stringSort = (a: string, b: string) => {
if (a > b) {
return 1;
}