feat: instances list actions
This commit is contained in:
parent
7536cdfc85
commit
6f10428b0f
@ -23,8 +23,8 @@
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"dev": "redrun -p dev:*",
|
||||
"dev:ui-toolkit": "lerna run watch --scope joyent-ui-toolkit",
|
||||
"dev:cp-frontend": "lerna run start --scope joyent-cp-frontend",
|
||||
"dev:gql-mock-server": "lerna run dev --scope joyent-cp-gql-mock-server",
|
||||
"dev:my-joy-beta": "lerna run dev --scope my-joy-beta",
|
||||
"dev:cloudapi-gql": "lerna run dev --scope cloudapi-gql",
|
||||
"commitmsg": "commitlint -e",
|
||||
"precommit": "cross-env CI=1 redrun -s lint-staged format-staged",
|
||||
"postinstall": "lerna run prepublish",
|
||||
|
@ -35,8 +35,9 @@ module.exports.start = uuid => request('startMachine', uuid);
|
||||
module.exports.startFromSnapshot = ctx =>
|
||||
request('startMachineFromSnapshot', ctx);
|
||||
module.exports.reboot = ctx => request('rebootMachine', ctx);
|
||||
module.exports.resize = ctx => request('', ctx);
|
||||
module.exports.rename = ctx => request('', ctx);
|
||||
(module.exports.resize = ({ id, package }) =>
|
||||
request.fetch(`/:login/machines/${id}?action=resize?package=${package}`)),
|
||||
(module.exports.rename = ctx => request('', ctx));
|
||||
module.exports.destroy = ctx => request('deleteMachine', ctx);
|
||||
module.exports.audit = ({ id }) => request('machineAudit', id);
|
||||
|
||||
|
@ -4,25 +4,35 @@ const api = require('../api');
|
||||
const resolvers = {
|
||||
Query: {
|
||||
account: () => api.account.get(),
|
||||
|
||||
keys: (root, { login, name }) =>
|
||||
name
|
||||
? api.keys.get({ login, name }).then(key => [key])
|
||||
: api.keys.list({ login, name }),
|
||||
|
||||
key: (root, { login, name }) => api.keys.get({ login, name }),
|
||||
|
||||
users: (root, { id }) =>
|
||||
id ? api.users.get({ id }).then(user => [user]) : api.users.list(),
|
||||
|
||||
user: (root, { id }) => api.users.get({ id }),
|
||||
|
||||
roles: (root, { id, name }) =>
|
||||
id || name
|
||||
? api.roles.get({ id, name }).then(role => [role])
|
||||
: api.roles.list(),
|
||||
|
||||
role: (root, { id, name }) => api.roles.get({ id, name }),
|
||||
|
||||
policies: (root, { id }) =>
|
||||
id
|
||||
? api.policies.get({ id }).then(policy => [policy])
|
||||
: api.policies.list(),
|
||||
|
||||
policy: (root, { id }) => api.policies.get({ id }),
|
||||
|
||||
config: () => api.config().then(toKeyValue),
|
||||
|
||||
datacenters: () =>
|
||||
api.datacenters().then(dcs =>
|
||||
Object.keys(dcs).map(name => ({
|
||||
@ -30,17 +40,23 @@ const resolvers = {
|
||||
url: dcs[name]
|
||||
}))
|
||||
),
|
||||
|
||||
services: () => api.services().then(toKeyValue),
|
||||
|
||||
images: (root, { id, ...rest }) =>
|
||||
id
|
||||
? api.images.get({ id }).then(image => [image])
|
||||
: api.images.list(rest),
|
||||
|
||||
image: (root, { id }) => api.images.get({ id }),
|
||||
|
||||
packages: (root, { id, ...rest }) =>
|
||||
id
|
||||
? api.packages.get({ id }).then(pkg => [pkg])
|
||||
: api.packages.list(rest),
|
||||
|
||||
package: (root, { id, name }) => api.packages.get({ id, name }),
|
||||
|
||||
machines: (root, { id, brand, state, tags, ...rest }) =>
|
||||
id
|
||||
? api.machines.get({ id }).then(machine => [machine])
|
||||
@ -51,36 +67,45 @@ const resolvers = {
|
||||
tags: fromKeyValue(tags)
|
||||
})
|
||||
),
|
||||
|
||||
machine: (root, { id }) => api.machines.get({ id }),
|
||||
|
||||
snapshots: (root, { name, machine }) =>
|
||||
name
|
||||
? api.machines.snapshots
|
||||
.get({ id: machine, name })
|
||||
.then(snapshot => [snapshot])
|
||||
: api.machines.snapshots.list({ id: machine }),
|
||||
|
||||
snapshot: (root, { name, machine }) =>
|
||||
api.machines.snapshots.get({ name, id: machine }),
|
||||
|
||||
metadata: (root, { machine, name, ...rest }) =>
|
||||
name
|
||||
? api.machines.metadata
|
||||
.get(Object.assign(rest, { id: machine, key: name }))
|
||||
.then(value => toKeyValue({ [name]: value }))
|
||||
: api.machines.metadata.list({ id: machine }).then(toKeyValue),
|
||||
|
||||
metadataValue: (root, { name, machine }) =>
|
||||
api.machines.metadata
|
||||
.get({ key: name, id: machine })
|
||||
.then(value => toKeyValue({ [name]: value }).shift()),
|
||||
|
||||
tags: (root, { machine, name }) =>
|
||||
name
|
||||
? api.machines.tags
|
||||
.get({ id: machine, tag: name })
|
||||
.then(value => toKeyValue({ [name]: value }))
|
||||
: api.machines.tags.list({ id: machine }).then(toKeyValue),
|
||||
|
||||
tag: (root, { machine, name }) =>
|
||||
api.machines.tags
|
||||
.get({ id: machine, tag: name })
|
||||
.then(value => toKeyValue({ [name]: value }).shift()),
|
||||
|
||||
actions: (root, { machine }) => api.machines.audit({ id: machine }),
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
firewall_rules: (root, { machine, id }) =>
|
||||
id
|
||||
@ -88,15 +113,22 @@ const resolvers = {
|
||||
: machine
|
||||
? api.firewall.listByMachine({ id: machine })
|
||||
: api.firewall.list(),
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
firewall_rule: (root, { id }) => api.firewall.get({ id }),
|
||||
|
||||
vlans: (root, { id }) => (id ? api.vlans.get({ id }) : api.vlans.list()),
|
||||
|
||||
vlan: (root, { id }) => api.vlans.get({ id }),
|
||||
|
||||
networks: (root, { id, vlan }) =>
|
||||
id ? api.networks.get({ id, vlan }) : api.networks.list({ vlan }),
|
||||
|
||||
network: (root, { id, vlan }) => api.networks.get({ id, vlan }),
|
||||
|
||||
nics: (root, { machine, mac }) =>
|
||||
mac ? api.nics.get({ machine, mac }) : api.nics.list({ machine }),
|
||||
|
||||
nic: (root, { machine, mac }) => api.nics.get({ machine, mac })
|
||||
},
|
||||
User: {
|
||||
@ -104,36 +136,50 @@ const resolvers = {
|
||||
},
|
||||
Machine: {
|
||||
brand: ({ brand }) => (brand ? brand.toUpperCase() : brand),
|
||||
|
||||
state: ({ state }) => (state ? state.toUpperCase() : state),
|
||||
|
||||
image: ({ image }) => resolvers.Query.image(null, { id: image }),
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
primary_ip: ({ primaryIp }) => primaryIp,
|
||||
|
||||
tags: ({ id }, { name }) =>
|
||||
resolvers.Query.tags(null, { machine: id, name }),
|
||||
|
||||
metadata: ({ id }, { name }) =>
|
||||
resolvers.Query.metadata(null, { machine: id, name }),
|
||||
|
||||
networks: ({ networks }) =>
|
||||
Promise.all(networks.map(id => resolvers.Query.network(null, { id }))),
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
package: root => resolvers.Query.package(null, { name: root.package }),
|
||||
|
||||
snapshots: ({ id }, { name }) =>
|
||||
resolvers.Query.snapshots(null, { machine: id, name }),
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
firewall_rules: ({ id: machine }, { id }) =>
|
||||
resolvers.Query.firewall_rules(null, { machine, id }),
|
||||
|
||||
actions: ({ id }) => resolvers.Query.actions(null, { machine: id })
|
||||
},
|
||||
Image: {
|
||||
os: ({ os }) => (os ? os.toUpperCase() : os),
|
||||
|
||||
state: ({ state }) => (state ? state.toUpperCase() : state),
|
||||
|
||||
type: ({ type }) => (type ? type.toUpperCase() : type)
|
||||
},
|
||||
Action: {
|
||||
name: ({ action }) => action,
|
||||
|
||||
parameters: ({ parameters }) => toKeyValue(parameters)
|
||||
},
|
||||
Caller: {
|
||||
type: ({ type }) => (type ? type.toUpperCase() : type),
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
key_id: ({ keyId }) => keyId
|
||||
},
|
||||
@ -151,8 +197,39 @@ const resolvers = {
|
||||
compression ? compression.toUpperCase() : compression
|
||||
},
|
||||
Mutation: {
|
||||
stopMachine: (root, { id }) =>
|
||||
api.machines.stop(id).then(() => resolvers.Query.machine(null, { id })),
|
||||
|
||||
startMachine: (root, { id }) =>
|
||||
api.machines.start(id).then(() => resolvers.Query.machine(null, { id })),
|
||||
|
||||
rebootMachine: (root, { id }) =>
|
||||
api.machines.reboot(id).then(() => resolvers.Query.machine(null, { id }))
|
||||
api.machines.reboot(id).then(() => resolvers.Query.machine(null, { id })),
|
||||
|
||||
resizeMachine: (root, { id, ...args }) =>
|
||||
api.machines
|
||||
.resize({ id, package: args.package })
|
||||
.then(() => resolvers.Query.machine(null, { id })),
|
||||
|
||||
enableMachineFirewall: (root, { id }) =>
|
||||
api.machines.firewall
|
||||
.enable(id)
|
||||
.then(() => resolvers.Query.machine(null, { id })),
|
||||
|
||||
disableMachineFirewall: (root, { id }) =>
|
||||
api.machines.firewall
|
||||
.disable(id)
|
||||
.then(() => resolvers.Query.machine(null, { id })),
|
||||
|
||||
createMachineSnapshot: (root, { id, name }) =>
|
||||
api.machines.snapshots
|
||||
.create({ id, name })
|
||||
.then(() => resolvers.Query.snapshots(null, { machine: id, name })),
|
||||
|
||||
startMachineFromSnapshot: (root, { id, name }) =>
|
||||
api.machines.snapshots
|
||||
.startFromSnapshot({ id, name })
|
||||
.then(() => resolvers.Query.machine(null, { id }))
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -31,7 +31,7 @@
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-styled-flexboxgrid": "^2.0.3",
|
||||
"redux": "^3.7.2",
|
||||
"redux-form": "^7.1.0",
|
||||
"redux-form": "^7.1.1",
|
||||
"remcalc": "^1.0.9",
|
||||
"styled-components": "^2.2.1",
|
||||
"styled-is": "^1.1.0"
|
||||
@ -51,7 +51,7 @@
|
||||
"jest-snapshot": "^21.2.1",
|
||||
"jest-styled-components": "^4.7.0",
|
||||
"jest-transform-graphql": "^2.1.0",
|
||||
"joyent-react-scripts": "^2.1.1",
|
||||
"joyent-react-scripts": "^2.2.1",
|
||||
"react-test-renderer": "^16.0.0",
|
||||
"redrun": "^5.9.18",
|
||||
"stylelint": "^8.2.0",
|
||||
|
@ -37,7 +37,7 @@
|
||||
"react-router-dom": "^4.2.2",
|
||||
"redux": "^3.7.2",
|
||||
"redux-actions": "^2.2.1",
|
||||
"redux-form": "^7.1.0",
|
||||
"redux-form": "^7.1.1",
|
||||
"remcalc": "^1.0.9",
|
||||
"styled-components": "^2.2.1",
|
||||
"title-case": "^2.1.1"
|
||||
@ -57,7 +57,7 @@
|
||||
"jest-snapshot": "^21.2.1",
|
||||
"jest-styled-components": "^4.7.0",
|
||||
"jest-transform-graphql": "^2.1.0",
|
||||
"joyent-react-scripts": "^2.1.1",
|
||||
"joyent-react-scripts": "^2.2.1",
|
||||
"react-test-renderer": "^16.0.0",
|
||||
"redrun": "^5.9.18",
|
||||
"serve": "^6.2.0",
|
||||
|
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FormGroup,
|
||||
FormLabel,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
MessageTitle,
|
||||
MessageDescription
|
||||
} from 'joyent-ui-toolkit';
|
||||
|
||||
export default ({
|
||||
submitting = false,
|
||||
error,
|
||||
handleSubmit = () => {},
|
||||
onCancel = () => {}
|
||||
}) => {
|
||||
const _error = error &&
|
||||
!submitting && (
|
||||
<Message error>
|
||||
<MessageTitle>Ooops!</MessageTitle>
|
||||
<MessageDescription>{error}</MessageDescription>
|
||||
</Message>
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
{_error}
|
||||
<FormGroup name="name" reduxForm>
|
||||
<FormLabel>Name (optional)</FormLabel>
|
||||
<Input placeholder="Snapshot name" />
|
||||
</FormGroup>
|
||||
<Button type="button" disabled={submitting} onClick={onCancel} secondary>
|
||||
Back
|
||||
</Button>
|
||||
<Button type="submit" disabled={submitting} loading={submitting}>
|
||||
Create
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
@ -3,3 +3,5 @@ export { default as List } from './list';
|
||||
export { default as KeyValue } from './key-value';
|
||||
export { default as Network } from './network';
|
||||
export { default as FirewallRule } from './firewall-rule';
|
||||
export { default as Resize } from './resize';
|
||||
export { default as CreateSnapshot } from './create-snapshot';
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Row, Col } from 'react-styled-flexboxgrid';
|
||||
import forceArray from 'force-array';
|
||||
import get from 'lodash.get';
|
||||
|
||||
import {
|
||||
FormGroup,
|
||||
@ -14,13 +15,34 @@ import {
|
||||
import Item from './item';
|
||||
|
||||
export default ({
|
||||
instances,
|
||||
instances = [],
|
||||
selected = [],
|
||||
loading,
|
||||
handleChange = () => null,
|
||||
onAction = () => null,
|
||||
handleSubmit,
|
||||
...rest
|
||||
}) => {
|
||||
const allowedActions = {
|
||||
stop: selected.some(({ state }) => state === 'RUNNING'),
|
||||
start: selected.some(({ state }) => state !== 'RUNNING'),
|
||||
reboot: true,
|
||||
resize:
|
||||
selected.length === 1 && selected.every(({ brand }) => brand === 'KVM'),
|
||||
enableFw: selected.some(({ firewall_enabled }) => !firewall_enabled),
|
||||
disableFw: selected.some(({ firewall_enabled }) => firewall_enabled),
|
||||
createSnap: true,
|
||||
startSnap:
|
||||
selected.length === 1 &&
|
||||
selected.every(({ snapshots = [] }) => snapshots.length)
|
||||
};
|
||||
|
||||
const handleActions = ({ target }) =>
|
||||
onAction({
|
||||
name: target.value,
|
||||
items: selected
|
||||
});
|
||||
|
||||
const _instances = forceArray(instances);
|
||||
|
||||
const items = _instances.map((instance, i, all) => (
|
||||
@ -83,21 +105,46 @@ export default ({
|
||||
<FormLabel>⁣</FormLabel>
|
||||
<Select
|
||||
value="actions"
|
||||
disabled={!items.length}
|
||||
onChange={({ target }) => onAction(target.value)}
|
||||
disabled={!items.length || !selected.length}
|
||||
onChange={handleActions}
|
||||
fluid
|
||||
>
|
||||
<option value="actions" selected disabled>
|
||||
≡
|
||||
</option>
|
||||
<option value="stop">Stop</option>
|
||||
<option value="start">Start</option>
|
||||
<option value="reboot">Reboot</option>
|
||||
<option value="resize">Resize</option>
|
||||
<option value="enable-fw">Enable Firewall</option>
|
||||
<option value="disable-fw">Disable Firewall</option>
|
||||
<option value="create-snap">Create Snapshot</option>
|
||||
<option value="start-snap">Start from Snapshot</option>
|
||||
<option value="stop" disabled={!allowedActions.stop}>
|
||||
Stop
|
||||
</option>
|
||||
<option value="start" disabled={!allowedActions.start}>
|
||||
Start
|
||||
</option>
|
||||
<option value="reboot" disabled={!allowedActions.reboot}>
|
||||
Reboot
|
||||
</option>
|
||||
<option value="resize" disabled={!allowedActions.resize}>
|
||||
Resize
|
||||
</option>
|
||||
<option value="enableFw" disabled={!allowedActions.enableFw}>
|
||||
Enable Firewall
|
||||
</option>
|
||||
<option
|
||||
value="disableFw"
|
||||
disabled={!allowedActions.disableFw}
|
||||
>
|
||||
Disable Firewall
|
||||
</option>
|
||||
<option
|
||||
value="createSnap"
|
||||
disabled={!allowedActions.createSnap}
|
||||
>
|
||||
Create Snapshot
|
||||
</option>
|
||||
<option
|
||||
value="startSnap"
|
||||
disabled={!allowedActions.startSnap}
|
||||
>
|
||||
Start from Snapshot
|
||||
</option>
|
||||
</Select>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
|
8
packages/my-joy-beta/src/components/instances/resize.js
Normal file
8
packages/my-joy-beta/src/components/instances/resize.js
Normal file
@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import ReactJson from 'react-json-view';
|
||||
|
||||
export default ({ instance, packages, handleSubmit }) => (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<ReactJson src={{ instance, packages }} />
|
||||
</form>
|
||||
);
|
@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { reduxForm, SubmissionError } from 'redux-form';
|
||||
import { connect } from 'react-redux';
|
||||
import { compose, graphql } from 'react-apollo';
|
||||
import find from 'lodash.find';
|
||||
import get from 'lodash.get';
|
||||
|
||||
import {
|
||||
Title,
|
||||
ViewContainer,
|
||||
StatusLoader,
|
||||
Message,
|
||||
MessageTitle,
|
||||
MessageDescription
|
||||
} from 'joyent-ui-toolkit';
|
||||
|
||||
import { CreateSnapshot as InstanceCreateSnapshot } from '@components/instances';
|
||||
import CreateSnapshotMutation from '@graphql/create-snapshot.gql';
|
||||
import GetInstance from '@graphql/get-instance.gql';
|
||||
|
||||
const CreateSnapshot = ({
|
||||
match,
|
||||
instance,
|
||||
loading,
|
||||
error,
|
||||
handleSubmit,
|
||||
handleCancel
|
||||
}) => {
|
||||
const _title = <Title>Create Snapshot</Title>;
|
||||
const _loading = !(loading && !instance) ? null : <StatusLoader />;
|
||||
|
||||
const CreateSnapshotForm = reduxForm({
|
||||
form: `create-snapshot-${match.params.instance}`
|
||||
})(InstanceCreateSnapshot);
|
||||
|
||||
const _error = error &&
|
||||
!instance &&
|
||||
!_loading && (
|
||||
<Message error>
|
||||
<MessageTitle>Ooops!</MessageTitle>
|
||||
<MessageDescription>
|
||||
An error occurred while loading your instance
|
||||
</MessageDescription>
|
||||
</Message>
|
||||
);
|
||||
|
||||
const _form = !loading &&
|
||||
!_error && (
|
||||
<CreateSnapshotForm onSubmit={handleSubmit} onCancel={handleCancel} />
|
||||
);
|
||||
|
||||
return (
|
||||
<ViewContainer center={Boolean(_loading)} main>
|
||||
{_title}
|
||||
{_loading}
|
||||
{_error}
|
||||
{_form}
|
||||
</ViewContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(
|
||||
graphql(CreateSnapshotMutation, { name: 'createSnapshot' }),
|
||||
graphql(GetInstance, {
|
||||
options: ({ match }) => ({
|
||||
variables: {
|
||||
name: get(match, 'params.instance')
|
||||
}
|
||||
}),
|
||||
props: ({ data: { loading, error, variables, ...rest } }) => ({
|
||||
instance: find(get(rest, 'machines', []), ['name', variables.name]),
|
||||
loading,
|
||||
error
|
||||
})
|
||||
}),
|
||||
connect(
|
||||
null,
|
||||
(dispatch, { history, location, instance, createSnapshot }) => ({
|
||||
handleCancel: () => history.push(location.pathname.split(/\/\~/).shift()),
|
||||
handleSubmit: ({ name }) =>
|
||||
createSnapshot({
|
||||
variables: { name, id: instance.id }
|
||||
})
|
||||
.catch(error => {
|
||||
throw new SubmissionError({
|
||||
_error: error.graphQLErrors
|
||||
.map(({ message }) => message)
|
||||
.join('\n')
|
||||
});
|
||||
})
|
||||
.then(() => history.push(`/instances/${instance.name}/snapshots`))
|
||||
})
|
||||
)
|
||||
)(CreateSnapshot);
|
@ -5,3 +5,5 @@ export { default as Metadata } from './metadata';
|
||||
export { default as Networks } from './networks';
|
||||
export { default as Firewall } from './firewall';
|
||||
export { default as Snapshots } from './snapshots';
|
||||
export { default as Resize } from './resize';
|
||||
export { default as CreateSnapshot } from './create-snapshot';
|
||||
|
@ -57,8 +57,6 @@ const List = ({
|
||||
</Message>
|
||||
) : null;
|
||||
|
||||
const handleAction = name => onAction({ name, ids: selected });
|
||||
|
||||
return (
|
||||
<ViewContainer main>
|
||||
{_title}
|
||||
@ -66,7 +64,8 @@ const List = ({
|
||||
<InstanceListForm
|
||||
instances={_instances}
|
||||
loading={loading}
|
||||
onAction={handleAction}
|
||||
onAction={onAction}
|
||||
selected={selected}
|
||||
/>
|
||||
</ViewContainer>
|
||||
);
|
||||
@ -121,7 +120,8 @@ export default compose(
|
||||
const selected = Object.keys(form)
|
||||
.map(name => find(values, ['name', name]))
|
||||
.filter(Boolean)
|
||||
.map(({ id }) => id);
|
||||
.map(({ id }) => find(instances, ['id', id]))
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
...rest,
|
||||
@ -129,27 +129,39 @@ export default compose(
|
||||
selected
|
||||
};
|
||||
},
|
||||
(dispatch, { instances, ...ownProps }) => ({
|
||||
onAction: ({ name, ids = [] }) => {
|
||||
(dispatch, { stop, start, reboot, history, location }) => ({
|
||||
onAction: ({ name, items = [] }) => {
|
||||
const types = {
|
||||
stop: () =>
|
||||
Promise.all(ids.map(id => ownProps.stop({ variables: { id } }))),
|
||||
Promise.all(items.map(({ id }) => stop({ variables: { id } }))),
|
||||
start: () =>
|
||||
Promise.all(ids.map(id => ownProps.start({ variables: { id } }))),
|
||||
Promise.all(items.map(({ id }) => start({ variables: { id } }))),
|
||||
reboot: () =>
|
||||
Promise.all(ids.map(id => ownProps.reboot({ variables: { id } }))),
|
||||
resize: () => null,
|
||||
'enable-fw': () => null,
|
||||
'disable-fw': () => null,
|
||||
'create-snap': () => null,
|
||||
'start-snap': () => null
|
||||
Promise.all(items.map(({ id }) => reboot({ variables: { id } }))),
|
||||
resize: () =>
|
||||
Promise.resolve(
|
||||
history.push(`/instances/~resize/${items.shift().name}`)
|
||||
),
|
||||
enableFw: () =>
|
||||
Promise.all(items.map(({ id }) => enableFw({ variables: { id } }))),
|
||||
disableFw: () =>
|
||||
Promise.all(
|
||||
items.map(({ id }) => disableFw({ variables: { id } }))
|
||||
),
|
||||
createSnap: () =>
|
||||
Promise.resolve(
|
||||
history.push(`/instances/~create-snapshot/${items.shift().name}`)
|
||||
),
|
||||
startSnap: () =>
|
||||
Promise.resolve(
|
||||
history.push(`/instances/${items.shift().name}/snapshots`)
|
||||
)
|
||||
};
|
||||
|
||||
const clearSelected = () =>
|
||||
dispatch(
|
||||
ids.map(id => {
|
||||
items.map(({ name: field }) => {
|
||||
const form = 'instance-list';
|
||||
const field = get(find(instances, ['id', id]), 'name');
|
||||
const value = false;
|
||||
|
||||
if (!field) {
|
||||
|
77
packages/my-joy-beta/src/containers/instances/resize.js
Normal file
77
packages/my-joy-beta/src/containers/instances/resize.js
Normal file
@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import { reduxForm } from 'redux-form';
|
||||
import { compose, graphql } from 'react-apollo';
|
||||
import forceArray from 'force-array';
|
||||
import find from 'lodash.find';
|
||||
import get from 'lodash.get';
|
||||
|
||||
import {
|
||||
StatusLoader,
|
||||
Title,
|
||||
ViewContainer,
|
||||
Message,
|
||||
MessageTitle,
|
||||
MessageDescription
|
||||
} from 'joyent-ui-toolkit';
|
||||
|
||||
import { Resize as InstanceResize } from '@components/instances';
|
||||
|
||||
import ListPackages from '@graphql/list-packages.gql';
|
||||
import ListInstances from '@graphql/list-instances.gql';
|
||||
import GetInstance from '@graphql/get-instance.gql';
|
||||
|
||||
const Resize = ({ match, loading, error, instance, packages }) => {
|
||||
const ResizeForm = reduxForm({
|
||||
form: `resize-instance-${match.params.instance}`
|
||||
})(InstanceResize);
|
||||
|
||||
const _packages = forceArray(packages);
|
||||
|
||||
const _title = <Title>Resize</Title>;
|
||||
const _loading = !(loading && (!_packages.length || !instance)) ? null : (
|
||||
<StatusLoader />
|
||||
);
|
||||
|
||||
const _error = !(error && !_loading) ? null : (
|
||||
<Message error>
|
||||
<MessageTitle>Ooops!</MessageTitle>
|
||||
<MessageDescription>An error occurred</MessageDescription>
|
||||
</Message>
|
||||
);
|
||||
|
||||
const _resize =
|
||||
_loading || _error ? null : (
|
||||
<ResizeForm packages={packages} instance={instance} />
|
||||
);
|
||||
|
||||
return (
|
||||
<ViewContainer center={Boolean(_loading)} main>
|
||||
{_title}
|
||||
{_loading}
|
||||
{_error}
|
||||
{_resize}
|
||||
</ViewContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(
|
||||
graphql(GetInstance, {
|
||||
options: ({ match }) => ({
|
||||
variables: {
|
||||
name: get(match, 'params.instance')
|
||||
}
|
||||
}),
|
||||
props: ({ data: { loading, error, variables, ...rest } }) => ({
|
||||
instance: find(get(rest, 'machines', []), ['name', variables.name]),
|
||||
loading,
|
||||
error
|
||||
})
|
||||
}),
|
||||
graphql(ListPackages, {
|
||||
props: ({ data: { packages, loading, error } }) => ({
|
||||
packages: forceArray(packages),
|
||||
loading,
|
||||
error
|
||||
})
|
||||
})
|
||||
)(Resize);
|
@ -1,3 +1,5 @@
|
||||
mutation createInstanceSnapshot($id: ID!, $name: String) {
|
||||
createMachineSnapshot(id: $id, name: $name)
|
||||
createMachineSnapshot(id: $id, name: $name) {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
query instance($name: String!) {
|
||||
query instance($name: String) {
|
||||
machines(name: $name) {
|
||||
id
|
||||
name
|
||||
@ -8,5 +8,8 @@ query instance($name: String!) {
|
||||
created
|
||||
updated
|
||||
firewall_enabled
|
||||
package {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
packages/my-joy-beta/src/graphql/get-package.gql
Normal file
14
packages/my-joy-beta/src/graphql/get-package.gql
Normal file
@ -0,0 +1,14 @@
|
||||
query packages($id: ID) {
|
||||
packages(id: $id) {
|
||||
id
|
||||
name
|
||||
memory
|
||||
disk
|
||||
swap
|
||||
lwps
|
||||
vcpus
|
||||
version
|
||||
group
|
||||
description
|
||||
}
|
||||
}
|
@ -13,5 +13,8 @@ query instances {
|
||||
package {
|
||||
name
|
||||
}
|
||||
snapshots {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
6
packages/my-joy-beta/src/graphql/list-packages.gql
Normal file
6
packages/my-joy-beta/src/graphql/list-packages.gql
Normal file
@ -0,0 +1,6 @@
|
||||
query packages {
|
||||
packages {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
@ -14,61 +14,109 @@ import {
|
||||
Metadata as InstanceMetadata,
|
||||
Networks as InstanceNetworks,
|
||||
Firewall as InstanceFirewall,
|
||||
Snapshots as InstanceSnapshots
|
||||
Snapshots as InstanceSnapshots,
|
||||
Resize as InstanceResize,
|
||||
CreateSnapshot as InstanceCreateSnapshot
|
||||
} from '@containers/instances';
|
||||
|
||||
export default () => (
|
||||
<BrowserRouter>
|
||||
<PageContainer>
|
||||
{/* Header */}
|
||||
<Route path="*" component={Header} />
|
||||
|
||||
{/* Breadcrumb */}
|
||||
<Switch>
|
||||
<Route path="/instances" exact component={Breadcrumb} />
|
||||
<Route path="/instances/:instance" component={Breadcrumb} />
|
||||
<Route
|
||||
path="/instances/~:action/:instance?"
|
||||
exact
|
||||
component={Breadcrumb}
|
||||
/>
|
||||
<Route path="/instances/:instance?" component={Breadcrumb} />
|
||||
</Switch>
|
||||
|
||||
{/* Menu */}
|
||||
<Switch>
|
||||
<Route path="/instances" exact component={Menu} />
|
||||
<Route path="/instances/:instance/:section" component={Menu} />
|
||||
<Route path="/instances/~:action/:id?" exact component={Menu} />
|
||||
<Route path="/instances/:instance?/:section?" component={Menu} />
|
||||
</Switch>
|
||||
|
||||
<Route path="/instances" exact component={Instances} />
|
||||
{/* Instances List */}
|
||||
<Switch>
|
||||
<Route path="/instances" exact component={Instances} />
|
||||
</Switch>
|
||||
|
||||
<Route
|
||||
path="/instances/:instance/summary"
|
||||
exact
|
||||
component={InstanceSummary}
|
||||
/>
|
||||
<Route path="/instances/:instance/tags" exact component={InstanceTags} />
|
||||
<Route
|
||||
path="/instances/:instance/metadata"
|
||||
exact
|
||||
component={InstanceMetadata}
|
||||
/>
|
||||
<Route
|
||||
path="/instances/:instance/networks"
|
||||
exact
|
||||
component={InstanceNetworks}
|
||||
/>
|
||||
<Route
|
||||
path="/instances/:instance/firewall"
|
||||
exact
|
||||
component={InstanceFirewall}
|
||||
/>
|
||||
<Route
|
||||
path="/instances/:instance/snapshots"
|
||||
exact
|
||||
component={InstanceSnapshots}
|
||||
/>
|
||||
<Route
|
||||
path="/instances/:instance"
|
||||
exact
|
||||
component={({ match }) => (
|
||||
<Redirect
|
||||
to={`/instances/${get(match, 'params.instance')}/summary`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{/* Instance Sections */}
|
||||
<Switch>
|
||||
<Route path="/instances/~:action" component={() => null} />
|
||||
<Route
|
||||
path="/instances/:instance/:section?/~create-snapshot"
|
||||
component={() => null}
|
||||
/>
|
||||
<Route
|
||||
path="/instances/:instance/summary"
|
||||
exact
|
||||
component={InstanceSummary}
|
||||
/>
|
||||
<Route
|
||||
path="/instances/:instance/tags"
|
||||
exact
|
||||
component={InstanceTags}
|
||||
/>
|
||||
<Route
|
||||
path="/instances/:instance/metadata"
|
||||
exact
|
||||
component={InstanceMetadata}
|
||||
/>
|
||||
<Route
|
||||
path="/instances/:instance/networks"
|
||||
exact
|
||||
component={InstanceNetworks}
|
||||
/>
|
||||
<Route
|
||||
path="/instances/:instance/firewall"
|
||||
exact
|
||||
component={InstanceFirewall}
|
||||
/>
|
||||
<Route
|
||||
path="/instances/:instance/snapshots"
|
||||
exact
|
||||
component={InstanceSnapshots}
|
||||
/>
|
||||
<Route
|
||||
path="/instances/:instance"
|
||||
exact
|
||||
component={({ match }) => (
|
||||
<Redirect
|
||||
to={`/instances/${get(match, 'params.instance')}/summary`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
|
||||
{/* Actions */}
|
||||
<Switch>
|
||||
<Route
|
||||
path="/instances/~resize/:instance"
|
||||
exact
|
||||
component={InstanceResize}
|
||||
/>
|
||||
<Route
|
||||
path="/instances/:instance/:section?/~resize"
|
||||
exact
|
||||
component={InstanceResize}
|
||||
/>
|
||||
<Route
|
||||
path="/instances/~create-snapshot/:instance"
|
||||
exact
|
||||
component={InstanceCreateSnapshot}
|
||||
/>
|
||||
<Route
|
||||
path="/instances/:instance/:section?/~create-snapshot"
|
||||
exact
|
||||
component={InstanceCreateSnapshot}
|
||||
/>
|
||||
</Switch>
|
||||
|
||||
<Route path="/" exact component={() => <Redirect to="/instances" />} />
|
||||
</PageContainer>
|
||||
|
@ -31,7 +31,7 @@
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-styled-flexboxgrid": "^2.0.3",
|
||||
"redux": "^3.7.2",
|
||||
"redux-form": "^7.1.0",
|
||||
"redux-form": "^7.1.1",
|
||||
"remcalc": "^1.0.9",
|
||||
"styled-components": "^2.2.1",
|
||||
"styled-is": "^1.1.0",
|
||||
|
@ -46,17 +46,18 @@
|
||||
"disable-scroll": "^0.3.0",
|
||||
"file-loader": "^1.1.5",
|
||||
"fontfaceobserver": "^2.0.13",
|
||||
"joy-react-broadcast": "^0.6.9",
|
||||
"joyent-manifest-editor": "^1.4.0",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"lodash.differenceby": "^4.8.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.isequalwith": "^4.4.0",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"moment": "^2.19.0",
|
||||
"normalized-styled-components": "^1.0.17",
|
||||
"pascal-case": "^2.0.1",
|
||||
"polished": "^1.8.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"joy-react-broadcast": "^0.6.9",
|
||||
"react-bundle": "^1.0.4",
|
||||
"react-input-range": "^1.2.1",
|
||||
"react-responsive": "^2.0.0",
|
||||
@ -100,10 +101,10 @@
|
||||
"react-redux": "^5.0.6",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-scripts": "^1.0.14",
|
||||
"react-styleguidist": "^6.0.28",
|
||||
"react-styleguidist": "^6.0.29",
|
||||
"react-test-renderer": "^16.0.0",
|
||||
"redux": "^3.7.2",
|
||||
"redux-form": "^7.1.0",
|
||||
"redux-form": "^7.1.1",
|
||||
"serve-static": "^1.13.1",
|
||||
"snapguidist": "^2.1.0",
|
||||
"style-loader": "^0.19.0",
|
||||
@ -121,6 +122,6 @@
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"redux-form": "^7.1.0"
|
||||
"redux-form": "^7.1.1"
|
||||
}
|
||||
}
|
||||
|
@ -27,9 +27,13 @@ const style = css`
|
||||
`};
|
||||
`;
|
||||
|
||||
const StyledAnchor = A.extend`${style};`;
|
||||
const StyledAnchor = A.extend`
|
||||
${style};
|
||||
`;
|
||||
|
||||
const StyledLink = styled(BaseLink)`${style};`;
|
||||
const StyledLink = styled(BaseLink)`
|
||||
${style};
|
||||
`;
|
||||
|
||||
/**
|
||||
* @example ./usage.md
|
||||
|
@ -11,9 +11,19 @@ export const tooltipShadow = `0 ${remcalc(2)} ${remcalc(6)} ${remcalc(
|
||||
export const modalShadow = `0 0 ${remcalc(6)} ${remcalc(1)} rgba(0, 0, 0, 0.1)`;
|
||||
|
||||
export const border = {
|
||||
checked: css`${remcalc(1)} solid ${props => props.theme.primary};`,
|
||||
unchecked: css`${remcalc(1)} solid ${props => props.theme.grey};`,
|
||||
confirmed: css`${remcalc(1)} solid ${props => props.theme.grey};`,
|
||||
error: css`${remcalc(1)} solid ${props => props.theme.red};`,
|
||||
secondary: css`${remcalc(1)} solid ${props => props.theme.secondaryActive};`
|
||||
checked: css`
|
||||
${remcalc(1)} solid ${props => props.theme.primary};
|
||||
`,
|
||||
unchecked: css`
|
||||
${remcalc(1)} solid ${props => props.theme.grey};
|
||||
`,
|
||||
confirmed: css`
|
||||
${remcalc(1)} solid ${props => props.theme.grey};
|
||||
`,
|
||||
error: css`
|
||||
${remcalc(1)} solid ${props => props.theme.red};
|
||||
`,
|
||||
secondary: css`
|
||||
${remcalc(1)} solid ${props => props.theme.secondaryActive};
|
||||
`
|
||||
};
|
||||
|
@ -202,7 +202,7 @@ const Button = props => {
|
||||
const View = Views.reduce((sel, view) => (sel ? sel : view()), null);
|
||||
|
||||
const children = loading ? (
|
||||
<StatusLoader secondary={!secondary} small/>
|
||||
<StatusLoader secondary={!secondary} small />
|
||||
) : (
|
||||
props.children
|
||||
);
|
||||
|
@ -61,12 +61,9 @@ const GraphNodeInfo = ({ data, pos }) => {
|
||||
const healthy = (
|
||||
<HealthyIcon
|
||||
healthy={
|
||||
instancesHealthy &&
|
||||
instancesHealthy.total === instancesHealthy.healthy ? (
|
||||
'HEALTHY'
|
||||
) : (
|
||||
'UNHEALTHY'
|
||||
)
|
||||
instancesHealthy && instancesHealthy.total === instancesHealthy.healthy
|
||||
? 'HEALTHY'
|
||||
: 'UNHEALTHY'
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
32
yarn.lock
32
yarn.lock
@ -392,7 +392,7 @@ apollo-server-core@^1.1.6:
|
||||
dependencies:
|
||||
apollo-tracing "^0.0.7"
|
||||
|
||||
apollo-server-hapi@^1.1.3:
|
||||
apollo-server-hapi@^1.1.6:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/apollo-server-hapi/-/apollo-server-hapi-1.1.6.tgz#97bdc483afe908e28aa0ae9a3ee7744d581bc3bf"
|
||||
dependencies:
|
||||
@ -1767,7 +1767,7 @@ babel-preset-jest@^21.2.0:
|
||||
babel-plugin-jest-hoist "^21.2.0"
|
||||
babel-plugin-syntax-object-rest-spread "^6.13.0"
|
||||
|
||||
babel-preset-joyent-portal@^3.0.1:
|
||||
babel-preset-joyent-portal@^3.1.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-preset-joyent-portal/-/babel-preset-joyent-portal-3.2.0.tgz#0801746916568886beba5c2911ce1c55ec142320"
|
||||
dependencies:
|
||||
@ -4363,11 +4363,7 @@ escope@^3.6.0:
|
||||
esrecurse "^4.1.0"
|
||||
estraverse "^4.1.1"
|
||||
|
||||
eslint-config-joyent-portal@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-joyent-portal/-/eslint-config-joyent-portal-3.0.0.tgz#269e3e0b88abba96adc3a6dc0bbf604a6ae89356"
|
||||
|
||||
eslint-config-joyent-portal@^3.0.0:
|
||||
eslint-config-joyent-portal@3.1.0, eslint-config-joyent-portal@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-joyent-portal/-/eslint-config-joyent-portal-3.1.0.tgz#7a77270d118627e59461db29d35639cf146d1dfc"
|
||||
|
||||
@ -5832,7 +5828,7 @@ graphql-tools@^1.1.0:
|
||||
optionalDependencies:
|
||||
"@types/graphql" "^0.9.0"
|
||||
|
||||
graphql-tools@^2.2.1:
|
||||
graphql-tools@^2.3.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-2.4.0.tgz#183d7e509e1ebd07d51db05fdeb181e7126f7ecb"
|
||||
dependencies:
|
||||
@ -7492,7 +7488,7 @@ joy-react-broadcast@^0.6.9:
|
||||
prop-types "^15.5.6"
|
||||
warning "^3.0.0"
|
||||
|
||||
joyent-manifest-editor@^1.3.0:
|
||||
joyent-manifest-editor@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/joyent-manifest-editor/-/joyent-manifest-editor-1.4.0.tgz#0c02efe6c02b0386a5b209ae4ddcc3492b9c22ac"
|
||||
dependencies:
|
||||
@ -7500,7 +7496,7 @@ joyent-manifest-editor@^1.3.0:
|
||||
prop-types "^15.6.0"
|
||||
react-codemirror "^1.0.0"
|
||||
|
||||
joyent-react-scripts@^2.0.2:
|
||||
joyent-react-scripts@^2.2.1:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/joyent-react-scripts/-/joyent-react-scripts-2.3.0.tgz#9e48f93d67284b8149dc73b35ccdfc11d27131d9"
|
||||
dependencies:
|
||||
@ -8913,7 +8909,7 @@ normalize-url@^1.4.0:
|
||||
query-string "^4.1.0"
|
||||
sort-keys "^1.0.0"
|
||||
|
||||
normalized-styled-components@^1.0.14:
|
||||
normalized-styled-components@^1.0.17:
|
||||
version "1.0.17"
|
||||
resolved "https://registry.yarnpkg.com/normalized-styled-components/-/normalized-styled-components-1.0.17.tgz#fd3a82e00b87d0c89d973f795cdaa7b5025ebb8a"
|
||||
dependencies:
|
||||
@ -10462,7 +10458,7 @@ react-styled-flexboxgrid@^2.0.3:
|
||||
dependencies:
|
||||
lodash.isinteger "^4.0.4"
|
||||
|
||||
react-styleguidist@^6.0.28:
|
||||
react-styleguidist@^6.0.29:
|
||||
version "6.0.30"
|
||||
resolved "https://registry.yarnpkg.com/react-styleguidist/-/react-styleguidist-6.0.30.tgz#988a09282f8af43749e44602349ec524dc1f07a0"
|
||||
dependencies:
|
||||
@ -10780,7 +10776,7 @@ reduce-css-calc@^1.2.6:
|
||||
math-expression-evaluator "^1.2.14"
|
||||
reduce-function-call "^1.0.1"
|
||||
|
||||
reduce-css-calc@^2.0.5:
|
||||
reduce-css-calc@^2.1.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.1.tgz#f4ecd7a00ec3e5683773f208067ad7da117b9db0"
|
||||
dependencies:
|
||||
@ -10806,7 +10802,7 @@ redux-actions@^2.2.1:
|
||||
lodash-es "^4.17.4"
|
||||
reduce-reducers "^0.1.0"
|
||||
|
||||
redux-form@^7.1.0:
|
||||
redux-form@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/redux-form/-/redux-form-7.1.1.tgz#4d9ab1d9c03beb3a8b5f8e5d0f398cff4209081f"
|
||||
dependencies:
|
||||
@ -11375,7 +11371,7 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||
hash-base "^2.0.0"
|
||||
inherits "^2.0.1"
|
||||
|
||||
rnd-id@^1.0.8:
|
||||
rnd-id@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/rnd-id/-/rnd-id-1.1.1.tgz#aaa11c650cf4105eeb1025eecf185db89071afb6"
|
||||
dependencies:
|
||||
@ -12225,11 +12221,11 @@ styled-components@^2.2.1:
|
||||
stylis "^3.2.1"
|
||||
supports-color "^3.2.3"
|
||||
|
||||
styled-is@^1.0.15:
|
||||
styled-is@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/styled-is/-/styled-is-1.1.0.tgz#0cf8d32098fe6559eb0ec889790cc6c84f1f497f"
|
||||
|
||||
stylelint-config-joyent-portal@^2.0.0:
|
||||
stylelint-config-joyent-portal@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/stylelint-config-joyent-portal/-/stylelint-config-joyent-portal-2.0.1.tgz#9d9242807749db394b9b9c3da7bc48b9b818a16e"
|
||||
dependencies:
|
||||
@ -13019,7 +13015,7 @@ unist-util-visit@^1.0.0, unist-util-visit@^1.1.0, unist-util-visit@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.1.3.tgz#ec268e731b9d277a79a5b5aa0643990e405d600b"
|
||||
|
||||
unitcalc@^1.1.0:
|
||||
unitcalc@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/unitcalc/-/unitcalc-1.1.1.tgz#a57e1c9dd61f251d2fad0c1d19f8577255cf080a"
|
||||
dependencies:
|
||||
|
Loading…
Reference in New Issue
Block a user