From 54b4631a9b7db64b97dd1310df6d9a1f0a0bbe7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Thu, 27 Jul 2023 13:48:29 +0200 Subject: [PATCH 01/45] Add Object Storage Tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an "Object Storage" tab for managing object endpoints. Signed-off-by: Moritz Röhrich --- src/index.js | 2 + src/models/dashboard.js | 0 src/models/engineimage.js | 0 src/models/eventlog.js | 0 src/models/objectendpoint.js | 44 +++++++++ src/models/volume.js | 0 src/publicPath.js | 2 + src/router.js | 8 +- .../objectEndpoint/CreateObjectEndpoint.js | 65 +++++++++++++ .../objectEndpoint/ObjectEndpointActions.js | 49 ++++++++++ .../ObjectEndpointBulkActions.js | 50 ++++++++++ .../objectEndpoint/ObjectEndpointList.js | 71 ++++++++++++++ src/routes/objectEndpoint/index.js | 97 +++++++++++++++++++ src/services/backup.js | 0 src/services/dashboard.js | 0 src/services/engineimage.js | 0 src/services/eventlog.js | 0 src/services/host.js | 0 src/services/objectendpoint.js | 30 ++++++ src/services/volume.js | 0 src/utils/dataDependency.js | 9 ++ src/utils/menu.js | 5 + 22 files changed, 431 insertions(+), 1 deletion(-) mode change 100755 => 100644 src/models/dashboard.js mode change 100755 => 100644 src/models/engineimage.js mode change 100755 => 100644 src/models/eventlog.js create mode 100644 src/models/objectendpoint.js mode change 100755 => 100644 src/models/volume.js create mode 100644 src/routes/objectEndpoint/CreateObjectEndpoint.js create mode 100644 src/routes/objectEndpoint/ObjectEndpointActions.js create mode 100644 src/routes/objectEndpoint/ObjectEndpointBulkActions.js create mode 100644 src/routes/objectEndpoint/ObjectEndpointList.js create mode 100644 src/routes/objectEndpoint/index.js mode change 100755 => 100644 src/services/backup.js mode change 100755 => 100644 src/services/dashboard.js mode change 100755 => 100644 src/services/engineimage.js mode change 100755 => 100644 src/services/eventlog.js mode change 100755 => 100644 src/services/host.js create mode 100644 src/services/objectendpoint.js mode change 100755 => 100644 src/services/volume.js diff --git a/src/index.js b/src/index.js index bfd614fab..a393190ba 100755 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ import recurringJob from './models/recurringJob' import instanceManager from './models/instanceManager' import orphanedData from './models/orphanedData' import systemBackups from './models/systemBackups' +import objectEndpoints from './models/objectendpoint' // import assets import './assets/iconfont/iconfont.eot' @@ -40,6 +41,7 @@ app.model(recurringJob) app.model(instanceManager) app.model(orphanedData) app.model(systemBackups) +app.model(objectEndpoints) // 3. Router app.router(routerConfig) diff --git a/src/models/dashboard.js b/src/models/dashboard.js old mode 100755 new mode 100644 diff --git a/src/models/engineimage.js b/src/models/engineimage.js old mode 100755 new mode 100644 diff --git a/src/models/eventlog.js b/src/models/eventlog.js old mode 100755 new mode 100644 diff --git a/src/models/objectendpoint.js b/src/models/objectendpoint.js new file mode 100644 index 000000000..ecfab9475 --- /dev/null +++ b/src/models/objectendpoint.js @@ -0,0 +1,44 @@ +import { listObjectEndpoints, getObjectEndpoint, createObjectEndpoint, deleteObjectEndpoint } from '../services/objectendpoint' + +export default { + namespace: 'objectstorage', + state: { + data: [], + selected: {}, + resourceType: 'objectEndpoint', + socketStatus: 'closed', + }, + subscriptions: {}, + effects: { + *list({ + payload, + }, { call, get }) { + const data = yield call(listObjectEndpoints, payload) + yield get({ type: 'listObjectEndpoints', payload: { ...data } }) + }, + *get({ + payload, + }, { call, get }) { + const data = yield call(getObjectEndpoint, payload) + yield get({ type: 'getObjectEndpoint', payload: { ...data } }) + }, + *create({ + payload, + }, { call }) { + yield call(createObjectEndpoint, payload) + }, + *delete({ + payload, + }, { call }) { + yield call(deleteObjectEndpoint, payload) + }, + }, + reducers: { + listObjectEndpoints(state, action) { + return { + ...state, + ...action.payload, + } + }, + }, +} diff --git a/src/models/volume.js b/src/models/volume.js old mode 100755 new mode 100644 diff --git a/src/publicPath.js b/src/publicPath.js index 4b489075b..7828a7a9e 100644 --- a/src/publicPath.js +++ b/src/publicPath.js @@ -18,6 +18,7 @@ let columnArr = [ 'lastBackupAt', 'actualSize', 'backendStoreDriver', + 'objectEndpoint', ] let pageSizeCollectionObject = { volumePageSize: 10, @@ -26,6 +27,7 @@ let pageSizeCollectionObject = { hostPageSize: 10, instanceManagerSize: 10, orphanedDataSize: 10, + objectEndpointSize: 10, } if (column) { columnArr = JSON.parse(column) diff --git a/src/router.js b/src/router.js index 7b48bdfde..835861922 100755 --- a/src/router.js +++ b/src/router.js @@ -18,6 +18,7 @@ import recurringJobComponent from './routes/recurringJob/' import orphanedDataComponent from './routes/orphanedData/' import engineimageDetailComponent from './routes/engineimage/detail' import systemBackupsComponent from './routes/systemBackups/' +import objectEndpointComponent from './routes/objectEndpoint/' const Routers = function ({ history, app }) { const App = dynamic({ @@ -95,6 +96,11 @@ const Routers = function ({ history, app }) { component: () => systemBackupsComponent, }) + const objectEndpoints = dynamic({ + app, + component: () => objectEndpointComponent, + }) + const path = '/' return ( @@ -114,10 +120,10 @@ const Routers = function ({ history, app }) { - + diff --git a/src/routes/objectEndpoint/CreateObjectEndpoint.js b/src/routes/objectEndpoint/CreateObjectEndpoint.js new file mode 100644 index 000000000..80e857c58 --- /dev/null +++ b/src/routes/objectEndpoint/CreateObjectEndpoint.js @@ -0,0 +1,65 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Form, Input } from 'antd' +import { ModalBlur } from '../../components' + +const FormItem = Form.Item + +const formItemLayout = { + labelCol: { + span: 4, + }, + wrapperCol: { + span: 17, + }, +} + +const modal = ({ + form: { + getFieldDecorator, + }, + item, + visible, + onCancel, + onOk, +}) => { + function handleOk() { + onOk({}) + } + + const modalOpts = { + visible, + onCancel, + width: 800, + onOk: handleOk, + style: { top: 0 }, + } + + return ( + +
+ + {getFieldDecorator('name', { + initialValue: item.name, + rules: [ + { + required: true, + message: 'Please input Object Endpoint name', + }, + ], + })()} + +
+
+ ) +} + +modal.propTypes = { + form: PropTypes.object.isRequired, + item: PropTypes.object, + visible: PropTypes.bool, + onCancel: PropTypes.func, + onOk: PropTypes.func, +} + +export default Form.create()(modal) diff --git a/src/routes/objectEndpoint/ObjectEndpointActions.js b/src/routes/objectEndpoint/ObjectEndpointActions.js new file mode 100644 index 000000000..8cd5d101d --- /dev/null +++ b/src/routes/objectEndpoint/ObjectEndpointActions.js @@ -0,0 +1,49 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Modal, Alert } from 'antd' +import { DropOption } from '../../components' + +const confirm = Modal.confirm + +function actions({ selected, deleteObjectEndpoint, editObjectEndpoint }) { + const handleMenuClick = (event, record) => { + switch (event.key) { + case 'delete': + confirm({ + title: `Are you sure you want to delete Object Endpoint ${record.name} ?`, + content: , + width: 760, + onOk() { + deleteObjectEndpoint(record) + }, + }) + break + case 'edit': + editObjectEndpoint(record) + break + default: + } + } + + const availableActions = [ + { key: 'delete', name: 'Delete' }, + { key: 'edit', name: 'Edit' }, + ] + + return ( + handleMenuClick(e, selected)} + /> + ) +} + +actions.propTypes = { + selected: PropTypes.object, + deleteRecurringJob: PropTypes.func, + editRecurringJob: PropTypes.func, +} + +export default actions diff --git a/src/routes/objectEndpoint/ObjectEndpointBulkActions.js b/src/routes/objectEndpoint/ObjectEndpointBulkActions.js new file mode 100644 index 000000000..bf026485a --- /dev/null +++ b/src/routes/objectEndpoint/ObjectEndpointBulkActions.js @@ -0,0 +1,50 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Button, Modal, Alert } from 'antd' + +const confirm = Modal.confirm + +function bulkActions({ selectedRows, deleteObjectEndpoint }) { + const handleClick = (action) => { + switch (action) { + case 'delete': + confirm({ + title: `Are you sure you want to delete Object Endpoint ${selectedRows.map(item => item.name).join(',')} ?`, + content: , + width: 760, + onOk() { + deleteObjectEndpoint(selectedRows) + }, + }) + break + default: + } + } + + const allActions = [ + { key: 'delete', name: 'Delete', disabled() { return selectedRows.length === 0 } }, + ] + + return ( +
+ { allActions.map(item => { + return ( +
+   + +
+ ) + }) } +
+ ) +} + +bulkActions.propTypes = { + selectedRows: PropTypes.array, + deleteObjectEndpoint: PropTypes.func, +} + +export default bulkActions diff --git a/src/routes/objectEndpoint/ObjectEndpointList.js b/src/routes/objectEndpoint/ObjectEndpointList.js new file mode 100644 index 000000000..ff1368a84 --- /dev/null +++ b/src/routes/objectEndpoint/ObjectEndpointList.js @@ -0,0 +1,71 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Table } from 'antd' +import { pagination } from '../../utils/page' +import ObjectEndpointActions from './ObjectEndpointActions' + +function list({ loading, dataSource, rowSelection, height, deleteObjectEndpoint }) { + const objectEndpointActionsProps = { + deleteObjectEndpoint, + } + + const columns = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + render: (text, record) => { + return ( +
{record.name}
+ ) + }, + }, + { + title: 'Endpoint', + dataIndex: 'endpoint', + key: 'endpoint', + render: (text, record) => { + return ( +
{record.endpoint}
+ ) + }, + }, + { + title: 'Operation', + key: 'operation', + width: 120, + render: (text, record) => { + return ( + + ) + }, + }, + ] + + return ( +
+ record.id} + scroll={{ x: 970, y: dataSource.length > 0 ? height : 1 }} + /> + + ) +} + +list.propTypes = { + loading: PropTypes.bool, + dataSource: PropTypes.array, + rowSelection: PropTypes.object, + heigth: PropTypes.number, + deleteObjectEndpoint: PropTypes.func, +} + +export default list diff --git a/src/routes/objectEndpoint/index.js b/src/routes/objectEndpoint/index.js new file mode 100644 index 000000000..70e91ef85 --- /dev/null +++ b/src/routes/objectEndpoint/index.js @@ -0,0 +1,97 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'dva' +import { Row, Col, Button } from 'antd' +import { Filter } from '../../components/index' +import CreateObjectEndpoint from './CreateObjectEndpoint' +import ObjectEndpointList from './ObjectEndpointList' +import ObjectEndpointBulkActions from './ObjectEndpointBulkActions' + +class ObjectEndpoint extends React.Component { + constructor(props) { + super(props) + this.state = { + selectedRows: [], + createObjectEndpointModalVisible: false, + createObjectEndpointModalKey: Math.random(), + } + } + + showCreateObjectEndpointModal = () => { + this.setState({ + ...this.state, + selected: {}, + createObjectEndpointModalVisible: true, + createObjectEndpointModalKey: Math.random(), + }) + } + + render() { + const me = this + const { dispatch, loading, location } = this.props + + const objectEndpointListProps = { + loading, + dataSource: [], + } + + const createObjectEndpointModalProps = { + item: {}, + visible: this.state.createObjectEndpointModalVisible, + onCancel() { + me.setState({ + ...this.state, + createObjectEndpointModalVisible: false, + }) + }, + } + + const objectEndpointFilterProps = { + location, + fieldOption: [], + } + + const objectEndpointBulkActionsProps = { + selectedRows: this.state.selectedRows, + deleteObjectEndpoint(record) { + dispatch({ + type: 'objectEndpoint/bulkDelete', + payload: record, + callback: () => { + this.setState({ + ...this.state, + selectedRows: [], + }) + }, + }) + }, + } + + return ( +
+ +
+ + + + + + + + {this.state.createObjectEndpointModalVisible && } + + + ) + } +} + +ObjectEndpoint.propTypes = { + objectendpoint: PropTypes.object, + loading: PropTypes.bool, + location: PropTypes.object, + dispatch: PropTypes.func, +} + +export default connect( + ({ objectendpoint, loading }) => ({ objectendpoint, loading: loading.models.objectEndpoint }) +)(ObjectEndpoint) diff --git a/src/services/backup.js b/src/services/backup.js old mode 100755 new mode 100644 diff --git a/src/services/dashboard.js b/src/services/dashboard.js old mode 100755 new mode 100644 diff --git a/src/services/engineimage.js b/src/services/engineimage.js old mode 100755 new mode 100644 diff --git a/src/services/eventlog.js b/src/services/eventlog.js old mode 100755 new mode 100644 diff --git a/src/services/host.js b/src/services/host.js old mode 100755 new mode 100644 diff --git a/src/services/objectendpoint.js b/src/services/objectendpoint.js new file mode 100644 index 000000000..8d28661c0 --- /dev/null +++ b/src/services/objectendpoint.js @@ -0,0 +1,30 @@ +import { request } from '../utils' + +export async function listObjectEndpoints() { + return request({ + url: '/v1/objectendpoints', + method: 'get', + }) +} + +export async function getObjectEndpoint(name) { + return request({ + url: `/v1/objectendpoints/${name}`, + method: 'get', + }) +} + +export async function createObjectEndpoint(params) { + return request({ + url: '/v1/objectendpoints', + method: 'put', + data: params, + }) +} + +export async function deleteObjectEndpoint(name) { + return request({ + url: `/v1/objectendpoints/${name}`, + method: 'delete', + }) +} diff --git a/src/services/volume.js b/src/services/volume.js old mode 100755 new mode 100644 diff --git a/src/utils/dataDependency.js b/src/utils/dataDependency.js index 725b2f353..7bed4cb20 100644 --- a/src/utils/dataDependency.js +++ b/src/utils/dataDependency.js @@ -97,6 +97,15 @@ const dependency = { key: 'backups', }], }, + objectEndpoint: { + path: '/objectendpoint', + runWs: [ + { + ns: 'objectendpoint', + key: 'objectendpoints', + }, + ], + }, instanceManager: { path: '/instanceManager', runWs: [{ diff --git a/src/utils/menu.js b/src/utils/menu.js index d8eae50e6..932d8696e 100755 --- a/src/utils/menu.js +++ b/src/utils/menu.js @@ -30,6 +30,11 @@ module.exports = [ name: 'Backup', icon: 'copy', }, + { + key: 'objectendpoint', + name: 'Object Storage', + icon: 'file', + }, { key: 'setting', name: 'Setting', From 234d67fe2ca0db3abec7cd707d0e2d90f34972a6 Mon Sep 17 00:00:00 2001 From: Siye Wang Date: Tue, 22 Aug 2023 15:32:44 +0800 Subject: [PATCH 02/45] feat(volume) Add volume scheduling failure message Signed-off-by: Siye Wang --- src/routes/volume/detail/VolumeInfo.js | 27 +++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/routes/volume/detail/VolumeInfo.js b/src/routes/volume/detail/VolumeInfo.js index dae65e27d..841fb3429 100644 --- a/src/routes/volume/detail/VolumeInfo.js +++ b/src/routes/volume/detail/VolumeInfo.js @@ -33,15 +33,24 @@ function VolumeInfo({ selectedVolume, snapshotModalState, engineImages, hosts, c }) if (isSchedulingFailure(selectedVolume)) { - errorMsg = ( - - ) + const scheduledConditions = selectedVolume?.conditions?.scheduled + if (scheduledConditions) { + const { reason, message } = scheduledConditions + errorMsg = ( + + {reason &&
{reason.replace(/([A-Z])/g, ' $1')}
} + {message &&
{`Error Message: ${message}`}
} + + } + type="warning" + className="ant-alert-error" + showIcon + /> + ) + } } const computeActualSize = selectedVolume && selectedVolume.controllers && selectedVolume.controllers[0] && selectedVolume.controllers[0].actualSize ? selectedVolume.controllers[0].actualSize : '' const defaultImage = engineImages.find(image => image.default === true) From 607c149c837a9f24bf8be6a780f1063068d067b0 Mon Sep 17 00:00:00 2001 From: Siye Wang Date: Mon, 24 Jul 2023 15:30:52 +0800 Subject: [PATCH 03/45] fix(volume) Add tooltip for volumes in maintenance mode Signed-off-by: Siye Wang --- src/routes/volume/detail/VolumeInfo.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/volume/detail/VolumeInfo.js b/src/routes/volume/detail/VolumeInfo.js index 841fb3429..117673a0d 100644 --- a/src/routes/volume/detail/VolumeInfo.js +++ b/src/routes/volume/detail/VolumeInfo.js @@ -285,7 +285,9 @@ function VolumeInfo({ selectedVolume, snapshotModalState, engineImages, hosts, c
Frontend: - {(frontends.find(item => item.value === selectedVolume.frontend) || '').label} + {selectedVolume.disableFrontend ? + + : (frontends.find(item => item.value === selectedVolume.frontend) || '').label}
Backend Data Engine: From bece27709810bf1a79aeb9132af1bd16129b90bf Mon Sep 17 00:00:00 2001 From: James Lu Date: Wed, 23 Aug 2023 11:22:12 +0800 Subject: [PATCH 04/45] fix: permission denied when running ui image It has a permission denied when running UI image to create tempfs directory, copy nginx config files to tempfs directory and touch pid file and change pid file owner. Ref: 6430 Signed-off-by: James Lu --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index ddb8d9588..4766fc2b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,8 @@ EXPOSE 8000 ENV LONGHORN_MANAGER_IP http://localhost:9500 ENV LONGHORN_UI_PORT 8000 +RUN mkdir -p /var/config/ && touch /var/run/nginx.pid && chown -R 499 /var/config /var/run/nginx.pid + # Use the uid of the default user (nginx) from the installed nginx package USER 499 From ccc88e0ceff4c3083732fe7b96a189920a99d0a6 Mon Sep 17 00:00:00 2001 From: Yarden Shoham Date: Mon, 11 Sep 2023 21:47:08 +0300 Subject: [PATCH 05/45] Make some of the delete confirmation dialogs nicer Mostly removed unnecessary spaces before question marks. Signed-off-by: Yarden Shoham --- src/components/Replica/Replica.js | 2 +- src/routes/backingImage/BackingImageActions.js | 2 +- src/routes/engineimage/EngineImageActions.js | 2 +- src/routes/host/DiskActions.js | 2 +- src/routes/host/HostActions.js | 2 +- src/routes/host/ReplicaList.js | 2 +- src/routes/orphanedData/orphanedDataActions.js | 2 +- src/routes/recurringJob/RecurringJobActions.js | 2 +- src/routes/systemBackups/systemBackupsAction.js | 2 +- src/routes/volume/VolumeActions.js | 4 ++-- src/routes/volume/detail/RecurringJobActions.js | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/Replica/Replica.js b/src/components/Replica/Replica.js index 871998263..a6d08b92d 100644 --- a/src/components/Replica/Replica.js +++ b/src/components/Replica/Replica.js @@ -19,7 +19,7 @@ class Replica extends React.Component { switch (event.key) { case 'delete': confirm({ - title: `Are you sure you want to delete replica ${record.name} ?`, + title: `Are you sure you want to delete replica ${record.name}?`, onOk() { deleteReplicas([record]) }, diff --git a/src/routes/backingImage/BackingImageActions.js b/src/routes/backingImage/BackingImageActions.js index 004f783ce..66f83149d 100644 --- a/src/routes/backingImage/BackingImageActions.js +++ b/src/routes/backingImage/BackingImageActions.js @@ -9,7 +9,7 @@ function actions({ selected, deleteBackingImage, cleanUpDiskMap, downloadBacking switch (event.key) { case 'delete': confirm({ - title: `Are you sure you want to delete backing image ${record.name} ?`, + title: `Are you sure you want to delete backing image ${record.name}?`, onOk() { deleteBackingImage(record) }, diff --git a/src/routes/engineimage/EngineImageActions.js b/src/routes/engineimage/EngineImageActions.js index ba24cde32..16a6f7333 100644 --- a/src/routes/engineimage/EngineImageActions.js +++ b/src/routes/engineimage/EngineImageActions.js @@ -9,7 +9,7 @@ function actions({ selected, deleteEngineImage }) { switch (event.key) { case 'delete': confirm({ - title: `Are you sure you want to delete engine image ${record.name} ?`, + title: `Are you sure you want to delete engine image ${record.name}?`, onOk() { deleteEngineImage(record) }, diff --git a/src/routes/host/DiskActions.js b/src/routes/host/DiskActions.js index 9e13fd5c7..c1ae5e092 100644 --- a/src/routes/host/DiskActions.js +++ b/src/routes/host/DiskActions.js @@ -16,7 +16,7 @@ function actions({ node, selected, updateDisk }) { } case 'delete': confirm({ - title: `Are you sure you want to delete disk which mounted on ${record.path}`, + title: `Are you sure you want to delete the disk that's mounted on ${record.path}?`, onOk() { const disks = Object.keys(node.disks) .filter(id => record.id !== id) diff --git a/src/routes/host/HostActions.js b/src/routes/host/HostActions.js index 316e81583..d3182bd60 100644 --- a/src/routes/host/HostActions.js +++ b/src/routes/host/HostActions.js @@ -13,7 +13,7 @@ function actions({ selected, showEditDisksModal, deleteHost }) { break case 'deleteHost': confirm({ - title: `Are you sure you want to delete node ${selected.name} ?`, + title: `Are you sure you want to delete node ${selected.name}?`, onOk() { deleteHost(record) }, diff --git a/src/routes/host/ReplicaList.js b/src/routes/host/ReplicaList.js index 27505f7b2..aabf1dcf9 100644 --- a/src/routes/host/ReplicaList.js +++ b/src/routes/host/ReplicaList.js @@ -9,7 +9,7 @@ function list({ dataSource, deleteReplicas, rowSelection }) { switch (event.key) { case 'delete': confirm({ - title: `Are you sure you want to delete replica ${record.name} ?`, + title: `Are you sure you want to delete replica ${record.name}?`, onOk() { deleteReplicas([record]) }, diff --git a/src/routes/orphanedData/orphanedDataActions.js b/src/routes/orphanedData/orphanedDataActions.js index 11eb7059a..7c98acd5e 100644 --- a/src/routes/orphanedData/orphanedDataActions.js +++ b/src/routes/orphanedData/orphanedDataActions.js @@ -9,7 +9,7 @@ function actions({ selected, deleteOrphanedData }) { switch (event.key) { case 'delete': confirm({ - title: `Are you sure you want to delete ${record.name} ?`, + title: `Are you sure you want to delete ${record.name}?`, width: 560, onOk() { deleteOrphanedData(record) diff --git a/src/routes/recurringJob/RecurringJobActions.js b/src/routes/recurringJob/RecurringJobActions.js index 3838ac386..77f2f1965 100644 --- a/src/routes/recurringJob/RecurringJobActions.js +++ b/src/routes/recurringJob/RecurringJobActions.js @@ -9,7 +9,7 @@ function actions({ selected, deleteRecurringJob, editRecurringJob }) { switch (event.key) { case 'delete': confirm({ - title: `Are you sure you want to delete Recurring Job ${record.name} ?`, + title: `Are you sure you want to delete Recurring Job ${record.name}?`, content: Date: Tue, 24 Oct 2023 09:56:19 +0000 Subject: [PATCH 06/45] Apply suggestions from code review Co-authored-by: Volker Theile Signed-off-by: Yarden Shoham --- src/routes/host/DiskActions.js | 2 +- src/routes/volume/VolumeActions.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/host/DiskActions.js b/src/routes/host/DiskActions.js index c1ae5e092..652c0edff 100644 --- a/src/routes/host/DiskActions.js +++ b/src/routes/host/DiskActions.js @@ -16,7 +16,7 @@ function actions({ node, selected, updateDisk }) { } case 'delete': confirm({ - title: `Are you sure you want to delete the disk that's mounted on ${record.path}?`, + title: `Are you sure you want to delete the disk that is mounted on ${record.path}?`, onOk() { const disks = Object.keys(node.disks) .filter(id => record.id !== id) diff --git a/src/routes/volume/VolumeActions.js b/src/routes/volume/VolumeActions.js index dd9a876c9..238062e60 100644 --- a/src/routes/volume/VolumeActions.js +++ b/src/routes/volume/VolumeActions.js @@ -141,7 +141,7 @@ function actions({ break case 'trimFilesystem': confirm({ - title: 'Are you sure you want to trim the fileystem?', + title: 'Are you sure you want to trim the filesystem?', onOk() { trimFilesystem(record) }, From bf7821ae5b4f3de8f09fe83f0facb02af197e726 Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Fri, 20 Oct 2023 09:23:37 +0200 Subject: [PATCH 07/45] Replace spec.engineImage field in volume, engine and replica CRDs with spec.image Fixes: https://github.com/longhorn/longhorn/issues/6685 Fixes: https://github.com/longhorn/longhorn/issues/6841 Signed-off-by: Volker Theile --- .gitignore | 3 +++ src/models/host.js | 2 +- src/routes/volume/EngineUpgrade.js | 4 ++-- src/routes/volume/VolumeActions.js | 8 ++++---- src/routes/volume/VolumeBulkActions.js | 8 ++++---- src/routes/volume/detail/Snapshots.js | 2 +- src/routes/volume/detail/VolumeInfo.js | 2 +- src/routes/volume/helper/index.js | 2 +- src/utils/status.js | 4 ++-- 9 files changed, 19 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 816e46c72..f2be6fecf 100755 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ bin/ yarn.lock npm-debug.log *.DS_Store + +# IDEs and editors +/.idea diff --git a/src/models/host.js b/src/models/host.js index 1fbd94b3d..2273de992 100644 --- a/src/models/host.js +++ b/src/models/host.js @@ -127,7 +127,7 @@ export default { }) engineImageData.forEach((item) => { replicaData.forEach((ele) => { - if (item.engineImage === ele.engineImage) { + if (item.image === ele.image) { item.replicaCurrentState = ele.currentState } }) diff --git a/src/routes/volume/EngineUpgrade.js b/src/routes/volume/EngineUpgrade.js index d506ce169..66fb59140 100644 --- a/src/routes/volume/EngineUpgrade.js +++ b/src/routes/volume/EngineUpgrade.js @@ -51,9 +51,9 @@ const modal = ({ const options = engineImages.filter((engineImage) => { return items.findIndex((item) => { if (engineUpgradePerNodeLimit && engineUpgradePerNodeLimit.value !== '0') { - return item.engineImage === engineImage.image || !engineImage.default + return item.image === engineImage.image || !engineImage.default } - return item.engineImage === engineImage.image + return item.image === engineImage.image }) === -1 }).map(engineImage => {engineImage.image}) diff --git a/src/routes/volume/VolumeActions.js b/src/routes/volume/VolumeActions.js index 238062e60..044965c55 100644 --- a/src/routes/volume/VolumeActions.js +++ b/src/routes/volume/VolumeActions.js @@ -151,7 +151,7 @@ function actions({ } } const toggleRollbackAndUpgradeAction = (currentActions) => { - if (selected.currentImage === selected.engineImage) { + if (selected.currentImage === selected.image) { const rollbackActionIndex = currentActions.findIndex(item => item.key === 'rollback') if (rollbackActionIndex > -1) { const upgradeAction = { key: 'engineUpgrade', name: 'Upgrade' } @@ -186,20 +186,20 @@ function actions({ if (engineUpgradePerNodeLimit && engineUpgradePerNodeLimit.value !== '0') { let defaultEngineImage = engineImages.find(engineImage => engineImage.default) if (defaultEngineImage) { - return selected.engineImage === defaultEngineImage.image + return selected.image === defaultEngineImage.image } return true } return false } - const upgradingEngine = () => selected.currentImage !== selected.engineImage + const upgradingEngine = () => selected.currentImage !== selected.image const allActions = [ { key: 'attach', name: 'Attach', disabled: !attachable(selected) }, { key: 'detach', name: 'Detach', disabled: !detachable(selected), tooltip: isRwxVolumeWithWorkload() ? 'The volume access mode is `ReadWriteMany`, Please ensure that the workloads are scaled down before trying to detach the volume' : '' }, { key: 'salvage', name: 'Salvage', disabled: isRestoring(selected) }, - { key: 'engineUpgrade', name: 'Upgrade Engine', disabled: isAutomaticallyUpgradeEngine() || (engineImages.findIndex(engineImage => selected.engineImage !== engineImage.image) === -1) || isRestoring(selected) || (selected.state !== 'detached' && selected.state !== 'attached') }, + { key: 'engineUpgrade', name: 'Upgrade Engine', disabled: isAutomaticallyUpgradeEngine() || (engineImages.findIndex(engineImage => selected.image !== engineImage.image) === -1) || isRestoring(selected) || (selected.state !== 'detached' && selected.state !== 'attached') }, { key: 'updateReplicaCount', name: 'Update Replicas Count', disabled: selected.state !== 'attached' || isRestoring(selected) || selected.standby || upgradingEngine() }, { key: 'updateDataLocality', name: 'Update Data Locality', disabled: !canUpdateDataLocality() || upgradingEngine() }, { key: 'updateSnapshotDataIntegrity', name: 'Snapshot Data Integrity', disabled: false }, diff --git a/src/routes/volume/VolumeBulkActions.js b/src/routes/volume/VolumeBulkActions.js index b7f2600d7..3285192fd 100644 --- a/src/routes/volume/VolumeBulkActions.js +++ b/src/routes/volume/VolumeBulkActions.js @@ -144,7 +144,7 @@ function bulkActions({ } } const hasAction = action => selectedRows.every(item => Object.keys(item.actions).includes(action)) - const hasDoingState = (exclusions = []) => selectedRows.some(item => (item.state.endsWith('ing') && !exclusions.includes(item.state)) || item.currentImage !== item.engineImage) + const hasDoingState = (exclusions = []) => selectedRows.some(item => (item.state.endsWith('ing') && !exclusions.includes(item.state)) || item.currentImage !== item.image) const isSnapshotDisabled = () => selectedRows.every(item => !item.actions || !item.actions.snapshotCreate) const disableUpdateBulkReplicaCount = () => selectedRows.some(item => !item.actions || !item.actions.updateReplicaCount) const disableUpdateBulkDataLocality = () => selectedRows.some(item => !item.actions || !item.actions.updateDataLocality) @@ -162,7 +162,7 @@ function bulkActions({ if (engineUpgradePerNodeLimit && engineUpgradePerNodeLimit.value !== '0') { let defaultEngineImage = engineImages.find(engineImage => engineImage.default) if (defaultEngineImage) { - return item.engineImage === defaultEngineImage.image + return item.image === defaultEngineImage.image } return true } @@ -170,12 +170,12 @@ function bulkActions({ }) } const conditionsScheduled = () => selectedRows.some(item => item.conditions && item.conditions.scheduled && item.conditions.scheduled.status && item.conditions.scheduled.status.toLowerCase() === 'true') - const upgradingEngine = () => selectedRows.some((item) => item.currentImage !== item.engineImage) + const upgradingEngine = () => selectedRows.some((item) => item.currentImage !== item.image) const notAttached = () => selectedRows.some(item => item.state !== 'attached') /* * PV/PVC decides whether to disable it */ - const hasMoreOptions = () => engineImages.findIndex(engineImage => selectedRows.findIndex(item => item.engineImage === engineImage.image) === -1) === -1 + const hasMoreOptions = () => engineImages.findIndex(engineImage => selectedRows.findIndex(item => item.image === engineImage.image) === -1) === -1 const allActions = [ { key: 'delete', name: 'Delete', disabled() { return selectedRows.length === 0 } }, { key: 'attach', name: 'Attach', disabled() { return selectedRows.length === 0 || selectedRows.some((item) => !attachable(item)) } }, diff --git a/src/routes/volume/detail/Snapshots.js b/src/routes/volume/detail/Snapshots.js index c050a2d9b..fb39d3709 100644 --- a/src/routes/volume/detail/Snapshots.js +++ b/src/routes/volume/detail/Snapshots.js @@ -220,7 +220,7 @@ class Snapshots extends React.Component { return false } } - const upgradingEngine = () => this.props.volume.currentImage !== this.props.volume.engineImage + const upgradingEngine = () => this.props.volume.currentImage !== this.props.volume.image const disableBackup = !this.props.volume.actions || !this.props.volume.actions.snapshotCreate || !this.props.state || this.props.volume.standby || isRestoring() || upgradingEngine() || !this.props.backupTargetAvailable diff --git a/src/routes/volume/detail/VolumeInfo.js b/src/routes/volume/detail/VolumeInfo.js index 117673a0d..3d2803c34 100644 --- a/src/routes/volume/detail/VolumeInfo.js +++ b/src/routes/volume/detail/VolumeInfo.js @@ -335,7 +335,7 @@ function VolumeInfo({ selectedVolume, snapshotModalState, engineImages, hosts, c Engine Image: - {selectedVolume.engineImage} + {selectedVolume.image}
Created: diff --git a/src/routes/volume/helper/index.js b/src/routes/volume/helper/index.js index bd0052450..b9f69dacc 100644 --- a/src/routes/volume/helper/index.js +++ b/src/routes/volume/helper/index.js @@ -564,7 +564,7 @@ export const frontends = [ ] export function disabledSnapshotAction(volume, modelState) { - return !volume.actions || !volume.actions.snapshotCreate || !modelState || volume.currentImage !== volume.engineImage || volume.standby + return !volume.actions || !volume.actions.snapshotCreate || !modelState || volume.currentImage !== volume.image || volume.standby } export function extractImageVersion(image) { diff --git a/src/utils/status.js b/src/utils/status.js index faf93f0b9..7e1e02661 100644 --- a/src/utils/status.js +++ b/src/utils/status.js @@ -2,8 +2,8 @@ import React from 'react' import { Tooltip, Icon } from 'antd' export function statusUpgradingEngine(volume) { - if (volume && volume.engineImage && volume.currentImage && volume.engineImage !== volume.currentImage) { - return () + if (volume && volume.image && volume.currentImage && volume.image !== volume.currentImage) { + return () } return '' } From 63b0d38946b79682878f168bf93f899a13697f94 Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Mon, 30 Oct 2023 12:41:40 +0100 Subject: [PATCH 08/45] Duplicate MIME type "text/html" in /var/config/nginx/nginx.conf According to the nginx docs (http://nginx.org/en/docs/http/ngx_http_gzip_module.html#gzip_types) the MIME type `text/html` does not need to be mentioned. Fixes: https://github.com/longhorn/longhorn/issues/7002 Signed-off-by: Volker Theile --- nginx.conf.template | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nginx.conf.template b/nginx.conf.template index 29bf56080..4a6acfa51 100644 --- a/nginx.conf.template +++ b/nginx.conf.template @@ -4,7 +4,7 @@ http { gzip on; gzip_min_length 1k; gzip_comp_level 2; - gzip_types text/html text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png image/svg+xml; + gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png image/svg+xml; gzip_vary on; gzip_disable "MSIE [1-6]\."; include /etc/nginx/mime.types; @@ -42,7 +42,7 @@ http { } location ~ ^/.(images|javascript|js|css|flash|media|static)/ { - root /web/dist; + root /web/dist; } } From c07d6bbafe44327ce2ff5efb7b22233ccc029d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Thu, 27 Jul 2023 13:48:29 +0200 Subject: [PATCH 09/45] Add Object Storage Tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an "Object Storage" tab for managing object endpoints. Signed-off-by: Moritz Röhrich --- src/models/objectendpoint.js | 15 ++++++++++++++- src/utils/dataDependency.js | 6 +++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/models/objectendpoint.js b/src/models/objectendpoint.js index ecfab9475..a1bd7cbbd 100644 --- a/src/models/objectendpoint.js +++ b/src/models/objectendpoint.js @@ -1,4 +1,6 @@ import { listObjectEndpoints, getObjectEndpoint, createObjectEndpoint, deleteObjectEndpoint } from '../services/objectendpoint' +import queryString from 'query-string' +import { enableQueryData } from '../utils/dataDependency' export default { namespace: 'objectstorage', @@ -8,7 +10,18 @@ export default { resourceType: 'objectEndpoint', socketStatus: 'closed', }, - subscriptions: {}, + subscriptions: { + setup({ dispatch, history }) { + history.listen(location => { + if (enableQueryData(location.pathname, 'objectendpoint')) { + dispatch({ + type: 'listObjectEndpoints', + payload: location.pathname.startsWith('/objectendpoint') ? queryString.parse(location.search) : {}, + }) + } + }) + }, + }, effects: { *list({ payload, diff --git a/src/utils/dataDependency.js b/src/utils/dataDependency.js index 7bed4cb20..de10fc864 100644 --- a/src/utils/dataDependency.js +++ b/src/utils/dataDependency.js @@ -101,7 +101,7 @@ const dependency = { path: '/objectendpoint', runWs: [ { - ns: 'objectendpoint', + ns: 'objectstorage', key: 'objectendpoints', }, ], @@ -158,6 +158,9 @@ const allWs = [{ }, { ns: 'systemBackups', key: 'systemrestores', +}, { + ns: 'objectstorage', + key: 'objectendpoints', }] const httpDataDependency = { @@ -172,6 +175,7 @@ const httpDataDependency = { '/instanceManager': ['volume', 'instanceManager'], '/orphanedData': ['orphanedData'], '/systemBackups': ['systemBackups'], + '/objectendpoint': ['objectstorage'], } export function getDataDependency(pathName) { From ead6c7fb6983e4d6afb120d104fdce2bd89a5f52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Thu, 27 Jul 2023 14:43:02 +0200 Subject: [PATCH 10/45] Object Endpoints: fill out list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fill out the object endpoint list view with the data received. Signed-off-by: Moritz Röhrich --- src/routes/objectEndpoint/index.js | 32 ++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/routes/objectEndpoint/index.js b/src/routes/objectEndpoint/index.js index 70e91ef85..45cc424af 100644 --- a/src/routes/objectEndpoint/index.js +++ b/src/routes/objectEndpoint/index.js @@ -1,5 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' +import queryString from 'query-string' import { connect } from 'dva' import { Row, Col, Button } from 'antd' import { Filter } from '../../components/index' @@ -29,10 +30,18 @@ class ObjectEndpoint extends React.Component { render() { const me = this const { dispatch, loading, location } = this.props + const { data } = this.props.objectendpoint + const { field, value } = queryString.parse(this.props.location.search) - const objectEndpointListProps = { - loading, - dataSource: [], + let objectendpoints = data.filter((item) => { + if (field === 'name') { + return item[field] && item[field].indexOf(value.trim()) > -1 + } + return true + }) + + if (objectendpoints && objectendpoints.length > 0) { + objectendpoints.sort((a, b) => a.name.localeCompare(b.name)) } const createObjectEndpointModalProps = { @@ -46,6 +55,21 @@ class ObjectEndpoint extends React.Component { }, } + const objectEndpointListProps = { + dataSource: objectendpoints, + height: this.state.height, + loading, + rowSelection: { + selectedRowKeys: this.state.selectedRows.map(item => item.id), + onChange(_, records) { + me.setState({ + ...me.state, + selectedRows: records, + }) + }, + }, + } + const objectEndpointFilterProps = { location, fieldOption: [], @@ -58,7 +82,7 @@ class ObjectEndpoint extends React.Component { type: 'objectEndpoint/bulkDelete', payload: record, callback: () => { - this.setState({ + me.setState({ ...this.state, selectedRows: [], }) From f22f867485bf222745778f523f5fcf3a0265e46c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Thu, 27 Jul 2023 20:01:14 +0200 Subject: [PATCH 11/45] Object Endpoint: Fill data into the list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fill data into the list and add trivial filter. Signed-off-by: Moritz Röhrich --- src/components/Layout/Footer.js | 6 ++- src/index.js | 4 +- src/models/objectendpoint.js | 44 ++++++++++++++++--- src/router.js | 4 +- .../objectEndpoint/ObjectEndpointList.js | 6 +-- src/routes/objectEndpoint/index.js | 35 ++++++++++++--- src/utils/dataDependency.js | 4 +- src/utils/menu.js | 2 +- 8 files changed, 81 insertions(+), 24 deletions(-) diff --git a/src/components/Layout/Footer.js b/src/components/Layout/Footer.js index c76ddccf5..7992277f1 100644 --- a/src/components/Layout/Footer.js +++ b/src/components/Layout/Footer.js @@ -10,7 +10,7 @@ import semver from 'semver' import BundlesModel from './BundlesModel' import StableLonghornVersions from './StableLonghornVersions' -function Footer({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, backup, systemBackups, dispatch }) { +function Footer({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectEndpoint, backup, systemBackups, dispatch }) { const { bundlesropsVisible, bundlesropsKey, stableLonghornVersionslVisible, stableLonghornVersionsKey, okText, modalButtonDisabled, progressPercentage } = app const currentVersion = config.version === '${VERSION}' ? 'dev' : config.version // eslint-disable-line no-template-curly-in-string const issueHref = 'https://github.com/longhorn/longhorn/issues/new/choose' @@ -128,6 +128,7 @@ function Footer({ app, host, volume, setting, engineimage, eventlog, backingImag {getStatusIcon(eventlog)} {getStatusIcon(backingImage)} {getStatusIcon(recurringJob)} + {getStatusIcon(objectEndpoint)} {getBackupStatusIcon(backup, 'backupVolumes')} {getBackupStatusIcon(backup, 'backups')} {getSystemBackupStatusIcon(systemBackups, 'systemBackup')} @@ -148,10 +149,11 @@ Footer.propTypes = { eventlog: PropTypes.object, backingImage: PropTypes.object, recurringJob: PropTypes.object, + objectEndpoint: PropTypes.object, app: PropTypes.object, backup: PropTypes.object, systemBackups: PropTypes.object, dispatch: PropTypes.func, } -export default connect(({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, backup, systemBackups }) => ({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, backup, systemBackups }))(Footer) +export default connect(({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectEndpoint, backup, systemBackups }) => ({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectEndpoint, backup, systemBackups }))(Footer) diff --git a/src/index.js b/src/index.js index a393190ba..696340047 100755 --- a/src/index.js +++ b/src/index.js @@ -12,7 +12,7 @@ import recurringJob from './models/recurringJob' import instanceManager from './models/instanceManager' import orphanedData from './models/orphanedData' import systemBackups from './models/systemBackups' -import objectEndpoints from './models/objectendpoint' +import objectEndpoint from './models/objectendpoint' // import assets import './assets/iconfont/iconfont.eot' @@ -41,7 +41,7 @@ app.model(recurringJob) app.model(instanceManager) app.model(orphanedData) app.model(systemBackups) -app.model(objectEndpoints) +app.model(objectEndpoint) // 3. Router app.router(routerConfig) diff --git a/src/models/objectendpoint.js b/src/models/objectendpoint.js index a1bd7cbbd..0e70481b5 100644 --- a/src/models/objectendpoint.js +++ b/src/models/objectendpoint.js @@ -1,10 +1,12 @@ import { listObjectEndpoints, getObjectEndpoint, createObjectEndpoint, deleteObjectEndpoint } from '../services/objectendpoint' +import { wsChanges, updateState } from '../utils/websocket' import queryString from 'query-string' import { enableQueryData } from '../utils/dataDependency' export default { - namespace: 'objectstorage', + namespace: 'objectEndpoint', state: { + ws: null, data: [], selected: {}, resourceType: 'objectEndpoint', @@ -13,21 +15,21 @@ export default { subscriptions: { setup({ dispatch, history }) { history.listen(location => { - if (enableQueryData(location.pathname, 'objectendpoint')) { + if (enableQueryData(location.pathname, 'objectEndpoint')) { dispatch({ - type: 'listObjectEndpoints', - payload: location.pathname.startsWith('/objectendpoint') ? queryString.parse(location.search) : {}, + type: 'query', + payload: location.pathname.startsWith('/objectEndpoint') ? queryString.parse(location.search) : {}, }) } }) }, }, effects: { - *list({ + *query({ payload, - }, { call, get }) { + }, { call, put }) { const data = yield call(listObjectEndpoints, payload) - yield get({ type: 'listObjectEndpoints', payload: { ...data } }) + yield put({ type: 'listObjectEndpoints', payload: { ...data } }) }, *get({ payload, @@ -45,6 +47,25 @@ export default { }, { call }) { yield call(deleteObjectEndpoint, payload) }, + *startWS({ + payload, + }, { select }) { + let ws = yield select(state => state.objectEndpoint.ws) + if (ws) { + ws.open() + } else { + wsChanges(payload.dispatch, payload.type, '1s', payload.ns) + } + }, + *stopWS({ + // eslint-disable-next-line no-unused-vars + payload, + }, { select }) { + let ws = yield select(state => state.objectEndpoint.ws) + if (ws) { + ws.close(1000) + } + }, }, reducers: { listObjectEndpoints(state, action) { @@ -53,5 +74,14 @@ export default { ...action.payload, } }, + updateBackground(state, action) { + return updateState(state, action) + }, + updateSocketStatus(state, action) { + return { ...state, socketStatus: action.payload } + }, + updateWs(state, action) { + return { ...state, ws: action.payload } + }, }, } diff --git a/src/router.js b/src/router.js index 835861922..3ec20787f 100755 --- a/src/router.js +++ b/src/router.js @@ -96,7 +96,7 @@ const Routers = function ({ history, app }) { component: () => systemBackupsComponent, }) - const objectEndpoints = dynamic({ + const objectEndpoint = dynamic({ app, component: () => objectEndpointComponent, }) @@ -123,7 +123,7 @@ const Routers = function ({ history, app }) { - + diff --git a/src/routes/objectEndpoint/ObjectEndpointList.js b/src/routes/objectEndpoint/ObjectEndpointList.js index ff1368a84..0649904f0 100644 --- a/src/routes/objectEndpoint/ObjectEndpointList.js +++ b/src/routes/objectEndpoint/ObjectEndpointList.js @@ -4,7 +4,7 @@ import { Table } from 'antd' import { pagination } from '../../utils/page' import ObjectEndpointActions from './ObjectEndpointActions' -function list({ loading, dataSource, rowSelection, height, deleteObjectEndpoint }) { +function list({ dataSource, height, loading, rowSelection, deleteObjectEndpoint }) { const objectEndpointActionsProps = { deleteObjectEndpoint, } @@ -61,10 +61,10 @@ function list({ loading, dataSource, rowSelection, height, deleteObjectEndpoint } list.propTypes = { - loading: PropTypes.bool, dataSource: PropTypes.array, - rowSelection: PropTypes.object, heigth: PropTypes.number, + loading: PropTypes.bool, + rowSelection: PropTypes.object, deleteObjectEndpoint: PropTypes.func, } diff --git a/src/routes/objectEndpoint/index.js b/src/routes/objectEndpoint/index.js index 45cc424af..f4eff701d 100644 --- a/src/routes/objectEndpoint/index.js +++ b/src/routes/objectEndpoint/index.js @@ -2,6 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import queryString from 'query-string' import { connect } from 'dva' +import { routerRedux } from 'dva/router' import { Row, Col, Button } from 'antd' import { Filter } from '../../components/index' import CreateObjectEndpoint from './CreateObjectEndpoint' @@ -12,6 +13,7 @@ class ObjectEndpoint extends React.Component { constructor(props) { super(props) this.state = { + height: 300, selectedRows: [], createObjectEndpointModalVisible: false, createObjectEndpointModalKey: Math.random(), @@ -30,7 +32,7 @@ class ObjectEndpoint extends React.Component { render() { const me = this const { dispatch, loading, location } = this.props - const { data } = this.props.objectendpoint + const { data } = this.props.objectEndpoint const { field, value } = queryString.parse(this.props.location.search) let objectendpoints = data.filter((item) => { @@ -60,7 +62,7 @@ class ObjectEndpoint extends React.Component { height: this.state.height, loading, rowSelection: { - selectedRowKeys: this.state.selectedRows.map(item => item.id), + selectedRowKeys: this.state.selectedRows.map(item => item.name), onChange(_, records) { me.setState({ ...me.state, @@ -68,11 +70,34 @@ class ObjectEndpoint extends React.Component { }) }, }, + deleteObjectEndpoint(record) { + dispatch({ + type: 'objectEndpoint/delete', + payload: record, + }) + }, } const objectEndpointFilterProps = { location, - fieldOption: [], + defaultField: 'name', + fieldOption: [ + { value: 'name', name: 'Name' }, + ], + onSearch(filter) { + const { field: filterField, value: filterValue } = filter + filterField && filterValue ? dispatch(routerRedux.push({ + pathname: '/objectEndpoint', + search: queryString.stringify({ + ...queryString.parse(location.search), + field: filterField, + value: filterValue, + }), + })) : dispatch(routerRedux.push({ + pathname: '/objectEndpoint', + search: queryString.stringify({}), + })) + }, } const objectEndpointBulkActionsProps = { @@ -110,12 +135,12 @@ class ObjectEndpoint extends React.Component { } ObjectEndpoint.propTypes = { - objectendpoint: PropTypes.object, + objectEndpoint: PropTypes.object, loading: PropTypes.bool, location: PropTypes.object, dispatch: PropTypes.func, } export default connect( - ({ objectendpoint, loading }) => ({ objectendpoint, loading: loading.models.objectEndpoint }) + ({ objectEndpoint, loading }) => ({ objectEndpoint, loading: loading.models.objectEndpoint }) )(ObjectEndpoint) diff --git a/src/utils/dataDependency.js b/src/utils/dataDependency.js index de10fc864..d4cfc1755 100644 --- a/src/utils/dataDependency.js +++ b/src/utils/dataDependency.js @@ -98,7 +98,7 @@ const dependency = { }], }, objectEndpoint: { - path: '/objectendpoint', + path: '/objectEndpoint', runWs: [ { ns: 'objectstorage', @@ -175,7 +175,7 @@ const httpDataDependency = { '/instanceManager': ['volume', 'instanceManager'], '/orphanedData': ['orphanedData'], '/systemBackups': ['systemBackups'], - '/objectendpoint': ['objectstorage'], + '/objectEndpoint': ['objectEndpoint'], } export function getDataDependency(pathName) { diff --git a/src/utils/menu.js b/src/utils/menu.js index 932d8696e..38aab4c1c 100755 --- a/src/utils/menu.js +++ b/src/utils/menu.js @@ -31,7 +31,7 @@ module.exports = [ icon: 'copy', }, { - key: 'objectendpoint', + key: 'objectEndpoint', name: 'Object Storage', icon: 'file', }, From c17f42670809bf8bc46072372068ab1e183236a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Fri, 28 Jul 2023 09:42:02 +0200 Subject: [PATCH 12/45] Object Endpoint: Make websockets work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make updates of the object endpoint list via websockets actually work. Signed-off-by: Moritz Röhrich --- src/models/objectendpoint.js | 26 +++------- .../objectEndpoint/CreateObjectEndpoint.js | 49 ++++++++++++++++++- .../objectEndpoint/ObjectEndpointList.js | 11 +++++ src/routes/objectEndpoint/index.js | 12 ++++- src/utils/dataDependency.js | 4 +- 5 files changed, 78 insertions(+), 24 deletions(-) diff --git a/src/models/objectendpoint.js b/src/models/objectendpoint.js index 0e70481b5..869beab2c 100644 --- a/src/models/objectendpoint.js +++ b/src/models/objectendpoint.js @@ -25,31 +25,21 @@ export default { }, }, effects: { - *query({ - payload, - }, { call, put }) { + *query({ payload }, { call, put }) { const data = yield call(listObjectEndpoints, payload) yield put({ type: 'listObjectEndpoints', payload: { ...data } }) }, - *get({ - payload, - }, { call, get }) { + *get({ payload }, { call, get }) { const data = yield call(getObjectEndpoint, payload) yield get({ type: 'getObjectEndpoint', payload: { ...data } }) }, - *create({ - payload, - }, { call }) { + *create({ payload }, { call }) { yield call(createObjectEndpoint, payload) }, - *delete({ - payload, - }, { call }) { + *delete({ payload }, { call }) { yield call(deleteObjectEndpoint, payload) }, - *startWS({ - payload, - }, { select }) { + *startWS({ payload }, { select }) { let ws = yield select(state => state.objectEndpoint.ws) if (ws) { ws.open() @@ -57,10 +47,8 @@ export default { wsChanges(payload.dispatch, payload.type, '1s', payload.ns) } }, - *stopWS({ - // eslint-disable-next-line no-unused-vars - payload, - }, { select }) { + // eslint-disable-next-line no-unused-vars + *stopWS({ payload }, { select }) { let ws = yield select(state => state.objectEndpoint.ws) if (ws) { ws.close(1000) diff --git a/src/routes/objectEndpoint/CreateObjectEndpoint.js b/src/routes/objectEndpoint/CreateObjectEndpoint.js index 80e857c58..c19639cd6 100644 --- a/src/routes/objectEndpoint/CreateObjectEndpoint.js +++ b/src/routes/objectEndpoint/CreateObjectEndpoint.js @@ -1,9 +1,10 @@ import React from 'react' import PropTypes from 'prop-types' -import { Form, Input } from 'antd' +import { Form, Input, InputNumber, Select } from 'antd' import { ModalBlur } from '../../components' const FormItem = Form.Item +const Option = Select.Option const formItemLayout = { labelCol: { @@ -17,6 +18,7 @@ const formItemLayout = { const modal = ({ form: { getFieldDecorator, + getFieldsValue, }, item, visible, @@ -24,7 +26,10 @@ const modal = ({ onOk, }) => { function handleOk() { - onOk({}) + const data = { + ...getFieldsValue(), + } + onOk(data) } const modalOpts = { @@ -49,6 +54,46 @@ const modal = ({ ], })()} +
+ + {getFieldDecorator('size', { + initialValue: item.size, + rules: [ + { + required: true, + message: 'Please input size', + }, + { + validator: (rule, value, callback) => { + if (value === '' || typeof value !== 'number') { + callback() + return + } + if (value < 0 || value > 65536) { + callback('The value should be between 0 and 65536') + } + }, + }, + ], + })()} + + + {getFieldDecorator('unit', { + initialValue: item.unit, + rules: [ + { + required: true, + message: 'Please input unit', + }, + ], + })( + + )} + +
) diff --git a/src/routes/objectEndpoint/ObjectEndpointList.js b/src/routes/objectEndpoint/ObjectEndpointList.js index 0649904f0..32506b372 100644 --- a/src/routes/objectEndpoint/ObjectEndpointList.js +++ b/src/routes/objectEndpoint/ObjectEndpointList.js @@ -10,6 +10,17 @@ function list({ dataSource, height, loading, rowSelection, deleteObjectEndpoint } const columns = [ + { + title: 'State', + dataIndex: 'state', + key: 'state', + width: 160, + render: (text, record) => { + return ( +
{record.state}
+ ) + }, + }, { title: 'Name', dataIndex: 'name', diff --git a/src/routes/objectEndpoint/index.js b/src/routes/objectEndpoint/index.js index f4eff701d..84072c9cd 100644 --- a/src/routes/objectEndpoint/index.js +++ b/src/routes/objectEndpoint/index.js @@ -51,10 +51,20 @@ class ObjectEndpoint extends React.Component { visible: this.state.createObjectEndpointModalVisible, onCancel() { me.setState({ - ...this.state, + ...me.state, createObjectEndpointModalVisible: false, }) }, + onOk(newObjectEndpoint) { + me.setState({ + ...me.state, + createObjectEndpointModalVisible: false, + }) + dispatch({ + type: 'objectEndpoint/create', + payload: newObjectEndpoint, + }) + }, } const objectEndpointListProps = { diff --git a/src/utils/dataDependency.js b/src/utils/dataDependency.js index d4cfc1755..1fc0f1324 100644 --- a/src/utils/dataDependency.js +++ b/src/utils/dataDependency.js @@ -101,7 +101,7 @@ const dependency = { path: '/objectEndpoint', runWs: [ { - ns: 'objectstorage', + ns: 'objectEndpoint', key: 'objectendpoints', }, ], @@ -159,7 +159,7 @@ const allWs = [{ ns: 'systemBackups', key: 'systemrestores', }, { - ns: 'objectstorage', + ns: 'objectEndpoint', key: 'objectendpoints', }] From 78fd559a6e099067554b7bae2d654b6e3af13a07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Fri, 28 Jul 2023 11:50:36 +0200 Subject: [PATCH 13/45] Oject Endpoint: Separate Size Input from Volume MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Separate out the size input elements from the create volume dialogue and re-use them in the create object endpoint dialogue. Signed-off-by: Moritz Röhrich --- src/components/SizeInput/SizeInput.js | 95 +++++++++++++++++++ src/components/index.js | 2 + .../objectEndpoint/CreateObjectEndpoint.js | 72 +++++--------- src/routes/volume/CreateVolume.js | 68 ++----------- 4 files changed, 131 insertions(+), 106 deletions(-) create mode 100644 src/components/SizeInput/SizeInput.js diff --git a/src/components/SizeInput/SizeInput.js b/src/components/SizeInput/SizeInput.js new file mode 100644 index 000000000..53f07105c --- /dev/null +++ b/src/components/SizeInput/SizeInput.js @@ -0,0 +1,95 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Form, Select, InputNumber } from 'antd' + +const FormItem = Form.Item +const Option = Select.Option + +class SizeInput extends React.Component { + state = { + size: 0, + unit: 'Gi', + } + + render() { + const { getFieldDecorator, getFieldsValue, setFieldsValue } = this.props + + function unitChange(value) { + const unitmap = new Map([ + ['Mi', 0], + ['Gi', 1], + ['Ti', 2], + ]) + + let currentSize = getFieldsValue().size + let newUnit = unitmap.get(value) + let currentUnit = unitmap.get(getFieldsValue().unit) + + if (newUnit > currentUnit) { + currentSize /= 1024 ** (newUnit - currentUnit) + } else { + currentSize *= 1024 ** (currentUnit - newUnit) + } + setFieldsValue({ + ...getFieldsValue(), + unit: value, + size: currentSize, + }) + } + + return ( +
+ + {getFieldDecorator('size', { + initialValue: this.state.size, + rules: [ + { + required: true, + message: 'Please input size', + }, { + validator: (rule, value, callback) => { + if (value === '' || typeof value !== 'number') { + callback() + return + } + if (value < 0 || value > 65536) { + callback('The value should be between 0 and 65535') + } else if (!/^\d+([.]\d{1,2})?$/.test(value)) { + callback('This value should have at most two decimal places') + } else if (value < 10 && getFieldsValue().unit === 'Mi') { + callback('The volume size must be greater than 10 Mi') + } else if (value % 1 !== 0 && getFieldsValue().unit === 'Mi') { + callback('Decimals are not allowed') + } else { + callback() + } + }, + }, + ], + })()} + + + {getFieldDecorator('unit', { + initialValue: this.state.unit, + rules: [{ required: true, message: 'Please select your unit!' }], + })( + , + )} + +
+ ) + } +} + +SizeInput.propTypes = { + state: PropTypes.object, + getFieldDecorator: PropTypes.func, + getFieldsValue: PropTypes.func, + setFieldsValue: PropTypes.func, +} + +export default SizeInput diff --git a/src/components/index.js b/src/components/index.js index baaa82406..47ca4fb4b 100755 --- a/src/components/index.js +++ b/src/components/index.js @@ -16,6 +16,7 @@ import BackupLabelInput from './BackupLabelInput/BackupLabelInput' import BackupLabelInputForRecurring from './BackupLabelInputForRecurring/BackupLabelInputForRecurring' import ExpansionErrorDetail from './ExpansionErrorDetail/ExpansionErrorDetail' import AutoComplete from './AutoComplete/AutoComplete' +import SizeInput from './SizeInput/SizeInput' export { DropOption, @@ -36,4 +37,5 @@ export { BackupLabelInputForRecurring, ExpansionErrorDetail, AutoComplete, + SizeInput, } diff --git a/src/routes/objectEndpoint/CreateObjectEndpoint.js b/src/routes/objectEndpoint/CreateObjectEndpoint.js index c19639cd6..8b9558258 100644 --- a/src/routes/objectEndpoint/CreateObjectEndpoint.js +++ b/src/routes/objectEndpoint/CreateObjectEndpoint.js @@ -1,10 +1,9 @@ import React from 'react' import PropTypes from 'prop-types' -import { Form, Input, InputNumber, Select } from 'antd' -import { ModalBlur } from '../../components' +import { Form, Input } from 'antd' +import { ModalBlur, SizeInput } from '../../components' const FormItem = Form.Item -const Option = Select.Option const formItemLayout = { labelCol: { @@ -18,7 +17,9 @@ const formItemLayout = { const modal = ({ form: { getFieldDecorator, + validateFields, getFieldsValue, + setFieldsValue, }, item, visible, @@ -26,10 +27,18 @@ const modal = ({ onOk, }) => { function handleOk() { - const data = { - ...getFieldsValue(), - } - onOk(data) + validateFields((errors) => { + if (errors) { return } + + const data = { + ...getFieldsValue(), + size: `${getFieldsValue().size}${getFieldsValue().unit}`, + } + + if (data.unit) { delete data.unit } + + onOk(data) + }) } const modalOpts = { @@ -40,6 +49,13 @@ const modal = ({ style: { top: 0 }, } + const sizeInputProps = { + state: item, + getFieldDecorator, + getFieldsValue, + setFieldsValue, + } + return (
@@ -54,46 +70,8 @@ const modal = ({ ], })()} -
- - {getFieldDecorator('size', { - initialValue: item.size, - rules: [ - { - required: true, - message: 'Please input size', - }, - { - validator: (rule, value, callback) => { - if (value === '' || typeof value !== 'number') { - callback() - return - } - if (value < 0 || value > 65536) { - callback('The value should be between 0 and 65536') - } - }, - }, - ], - })()} - - - {getFieldDecorator('unit', { - initialValue: item.unit, - rules: [ - { - required: true, - message: 'Please input unit', - }, - ], - })( - - )} - -
+ +
) diff --git a/src/routes/volume/CreateVolume.js b/src/routes/volume/CreateVolume.js index 9199e5dbf..ead1c6cd3 100644 --- a/src/routes/volume/CreateVolume.js +++ b/src/routes/volume/CreateVolume.js @@ -1,7 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' import { Form, Input, InputNumber, Select, Checkbox, Spin, Collapse } from 'antd' -import { ModalBlur } from '../../components' +import { ModalBlur, SizeInput } from '../../components' import { frontends } from './helper/index' const FormItem = Form.Item const { Panel } = Collapse @@ -73,19 +73,11 @@ const modal = ({ style: { top: 0 }, } - function unitChange(value) { - let currentSize = getFieldsValue().size - - if (value === 'Gi') { - currentSize /= 1024 - } else { - currentSize *= 1024 - } - setFieldsValue({ - ...getFieldsValue(), - unit: value, - size: currentSize, - }) + const sizeInputProps = { + state: item, + getFieldDecorator, + getFieldsValue, + setFieldsValue, } return ( @@ -102,52 +94,10 @@ const modal = ({ ], })()} -
- - {getFieldDecorator('size', { - initialValue: item.size, - rules: [ - { - required: true, - message: 'Please input volume size', - }, { - validator: (rule, value, callback) => { - if (value === '' || typeof value !== 'number') { - callback() - return - } - if (value < 0 || value > 65536) { - callback('The value should be between 0 and 65535') - } else if (!/^\d+([.]\d{1,2})?$/.test(value)) { - callback('This value should have at most two decimal places') - } else if (value < 10 && getFieldsValue().unit === 'Mi') { - callback('The volume size must be greater than 10 Mi') - } else if (value % 1 !== 0 && getFieldsValue().unit === 'Mi') { - callback('Decimals are not allowed') - } else { - callback() - } - }, - }, - ], - })()} - - - {getFieldDecorator('unit', { - initialValue: item.unit, - rules: [{ required: true, message: 'Please select your unit!' }], - })( - , - )} - +
+ +
- {getFieldDecorator('numberOfReplicas', { initialValue: item.numberOfReplicas, From b23cf53d5214f36dbbf507036811daae81d8d53a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Fri, 28 Jul 2023 13:29:00 +0200 Subject: [PATCH 14/45] Object Endpoint: Delete action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hook up the Delete action to the internal API Signed-off-by: Moritz Röhrich --- src/models/objectendpoint.js | 15 +++++++++++++-- src/services/objectendpoint.js | 14 ++++++++------ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/models/objectendpoint.js b/src/models/objectendpoint.js index 869beab2c..717f99f6a 100644 --- a/src/models/objectendpoint.js +++ b/src/models/objectendpoint.js @@ -33,11 +33,22 @@ export default { const data = yield call(getObjectEndpoint, payload) yield get({ type: 'getObjectEndpoint', payload: { ...data } }) }, - *create({ payload }, { call }) { + *create({ payload, callback }, { call, put }) { yield call(createObjectEndpoint, payload) + if (callback) callback() + yield put({ type: 'quey' }) }, - *delete({ payload }, { call }) { + *delete({ payload, callback }, { call, put }) { yield call(deleteObjectEndpoint, payload) + if (callback) callback() + yield put({ type: 'query' }) + }, + *bulkDelete({ payload, callback }, { call, put }) { + if (payload && payload.length > 0) { + yield payload.map(item => call(deleteObjectEndpoint, item)) + } + if (callback) callback() + yield put({ type: 'query' }) }, *startWS({ payload }, { select }) { let ws = yield select(state => state.objectEndpoint.ws) diff --git a/src/services/objectendpoint.js b/src/services/objectendpoint.js index 8d28661c0..fbc7ad613 100644 --- a/src/services/objectendpoint.js +++ b/src/services/objectendpoint.js @@ -17,14 +17,16 @@ export async function getObjectEndpoint(name) { export async function createObjectEndpoint(params) { return request({ url: '/v1/objectendpoints', - method: 'put', + method: 'post', data: params, }) } -export async function deleteObjectEndpoint(name) { - return request({ - url: `/v1/objectendpoints/${name}`, - method: 'delete', - }) +export async function deleteObjectEndpoint(params) { + if (params.name) { + return request({ + url: `/v1/objectendpoints/${params.name}`, + method: 'delete', + }) + } } From 5fa06b422a35a5d534e29fd7b7a0bec1a2f56d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Fri, 28 Jul 2023 14:34:35 +0200 Subject: [PATCH 15/45] Object Endpoint: Default Values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the default value of the SizeInput element sensible Add defaults for access/secret key for the object endpoint creation dialogue Signed-off-by: Moritz Röhrich --- src/components/SizeInput/SizeInput.js | 2 +- .../objectEndpoint/CreateObjectEndpoint.js | 29 ++++++++++++++++++- src/routes/objectEndpoint/index.js | 5 +++- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/components/SizeInput/SizeInput.js b/src/components/SizeInput/SizeInput.js index 53f07105c..ac426c5a5 100644 --- a/src/components/SizeInput/SizeInput.js +++ b/src/components/SizeInput/SizeInput.js @@ -7,7 +7,7 @@ const Option = Select.Option class SizeInput extends React.Component { state = { - size: 0, + size: 1, unit: 'Gi', } diff --git a/src/routes/objectEndpoint/CreateObjectEndpoint.js b/src/routes/objectEndpoint/CreateObjectEndpoint.js index 8b9558258..09ab9f875 100644 --- a/src/routes/objectEndpoint/CreateObjectEndpoint.js +++ b/src/routes/objectEndpoint/CreateObjectEndpoint.js @@ -1,9 +1,10 @@ import React from 'react' import PropTypes from 'prop-types' -import { Form, Input } from 'antd' +import { Form, Input, Collapse } from 'antd' import { ModalBlur, SizeInput } from '../../components' const FormItem = Form.Item +const Panel = Collapse.Panel const formItemLayout = { labelCol: { @@ -72,6 +73,32 @@ const modal = ({ + + {getFieldDecorator('accesskey', { + initialValue: item.accesskey, + rules: [ + { + required: true, + message: 'Please input an access key', + }, + ], + })()} + + + {getFieldDecorator('secretkey', { + initialValue: item.secretkey, + rules: [ + { + required: true, + message: 'Please input a secret key', + }, + ], + })()} + + + + + ) diff --git a/src/routes/objectEndpoint/index.js b/src/routes/objectEndpoint/index.js index 84072c9cd..14b86bef9 100644 --- a/src/routes/objectEndpoint/index.js +++ b/src/routes/objectEndpoint/index.js @@ -47,7 +47,10 @@ class ObjectEndpoint extends React.Component { } const createObjectEndpointModalProps = { - item: {}, + item: { + accesskey: Math.random().toString(36).substr(2, 6), + secretkey: Math.random().toString(36).substr(2, 6), + }, visible: this.state.createObjectEndpointModalVisible, onCancel() { me.setState({ From ccd67a97df0b4d38d2ba8b86b46110a9e86d51cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Tue, 1 Aug 2023 11:53:57 +0200 Subject: [PATCH 16/45] Object Endpoint: Add color/style to the state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add colors and styling to the state indicator in the object endpoint list Signed-off-by: Moritz Röhrich --- .../objectEndpoint/ObjectEndpointList.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/routes/objectEndpoint/ObjectEndpointList.js b/src/routes/objectEndpoint/ObjectEndpointList.js index 32506b372..9bf11a7fd 100644 --- a/src/routes/objectEndpoint/ObjectEndpointList.js +++ b/src/routes/objectEndpoint/ObjectEndpointList.js @@ -1,6 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' -import { Table } from 'antd' +import { Table, Tooltip } from 'antd' import { pagination } from '../../utils/page' import ObjectEndpointActions from './ObjectEndpointActions' @@ -9,6 +9,14 @@ function list({ dataSource, height, loading, rowSelection, deleteObjectEndpoint deleteObjectEndpoint, } + const endpointStateColorMap = { + Unknown: { color: '#F15354', bg: 'rgba(241,83,84,.05)' }, + Starting: { color: '#F1C40F', bg: 'rgba(241,196,15,.05)' }, + Running: { color: '#27AE5F', bg: 'rgba(39,174,95,.05)' }, + Stopping: { color: '#DEE1E3', bg: 'rgba(222,225,227,.05)' }, + Error: { color: '#F15354', bg: 'rgba(241,83,84,.1)' }, + } + const columns = [ { title: 'State', @@ -16,8 +24,14 @@ function list({ dataSource, height, loading, rowSelection, deleteObjectEndpoint key: 'state', width: 160, render: (text, record) => { + const tooltip = `Endpoint ${record.name} is ${record.state}` + const colormap = endpointStateColorMap[record.state] || { color: '', bg: '' } return ( -
{record.state}
+ +
+ {record.state} +
+
) }, }, From c2b591adc01f1318305c9de1eedb8ab728384918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Fri, 4 Aug 2023 13:54:13 +0200 Subject: [PATCH 17/45] object endpoint: make storageclass selectable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make storageclass selectable via dropdown menu. Signed-off-by: Moritz Röhrich --- src/components/Layout/Footer.js | 6 +- .../StorageClassInput/StorageClassInput.js | 54 +++++++++++++++ src/components/index.js | 2 + src/models/objectendpoint.js | 10 +++ src/models/storageclass.js | 65 +++++++++++++++++++ .../objectEndpoint/CreateObjectEndpoint.js | 19 ++++-- src/routes/objectEndpoint/index.js | 3 +- src/services/storageclass.js | 8 +++ src/utils/dataDependency.js | 14 +++- 9 files changed, 170 insertions(+), 11 deletions(-) create mode 100644 src/components/StorageClassInput/StorageClassInput.js create mode 100644 src/models/storageclass.js create mode 100644 src/services/storageclass.js diff --git a/src/components/Layout/Footer.js b/src/components/Layout/Footer.js index 7992277f1..d6c16dfe5 100644 --- a/src/components/Layout/Footer.js +++ b/src/components/Layout/Footer.js @@ -10,7 +10,7 @@ import semver from 'semver' import BundlesModel from './BundlesModel' import StableLonghornVersions from './StableLonghornVersions' -function Footer({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectEndpoint, backup, systemBackups, dispatch }) { +function Footer({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectEndpoint, storageClass, backup, systemBackups, dispatch }) { const { bundlesropsVisible, bundlesropsKey, stableLonghornVersionslVisible, stableLonghornVersionsKey, okText, modalButtonDisabled, progressPercentage } = app const currentVersion = config.version === '${VERSION}' ? 'dev' : config.version // eslint-disable-line no-template-curly-in-string const issueHref = 'https://github.com/longhorn/longhorn/issues/new/choose' @@ -129,6 +129,7 @@ function Footer({ app, host, volume, setting, engineimage, eventlog, backingImag {getStatusIcon(backingImage)} {getStatusIcon(recurringJob)} {getStatusIcon(objectEndpoint)} + {getStatusIcon(storageClass)} {getBackupStatusIcon(backup, 'backupVolumes')} {getBackupStatusIcon(backup, 'backups')} {getSystemBackupStatusIcon(systemBackups, 'systemBackup')} @@ -150,10 +151,11 @@ Footer.propTypes = { backingImage: PropTypes.object, recurringJob: PropTypes.object, objectEndpoint: PropTypes.object, + storageClass: PropTypes.object, app: PropTypes.object, backup: PropTypes.object, systemBackups: PropTypes.object, dispatch: PropTypes.func, } -export default connect(({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectEndpoint, backup, systemBackups }) => ({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectEndpoint, backup, systemBackups }))(Footer) +export default connect(({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectEndpoint, storageClass, backup, systemBackups }) => ({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectEndpoint, storageClass, backup, systemBackups }))(Footer) diff --git a/src/components/StorageClassInput/StorageClassInput.js b/src/components/StorageClassInput/StorageClassInput.js new file mode 100644 index 000000000..43ba2fd71 --- /dev/null +++ b/src/components/StorageClassInput/StorageClassInput.js @@ -0,0 +1,54 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Form, Select } from 'antd' + +const FormItem = Form.Item +const Option = Select.Option + +class StorageClassInput extends React.Component { + state = { + storageclass: 'longhorn', + } + + render() { + const { getFieldDecorator, getFieldsValue, setFieldsValue } = this.props + + function classChange(value) { + setFieldsValue({ + ...getFieldsValue(), + storageclass: value, + }) + } + + return ( +
+ + {getFieldDecorator('storageclass', { + initialValue: this.state.storageclass, + rules: [{ required: true, message: 'Please select a storage class' }], + })( + + )} + +
+ ) + } +} + +StorageClassInput.propTypes = { + classes: PropTypes.array, + state: PropTypes.object, + getFieldDecorator: PropTypes.func, + getFieldsValue: PropTypes.func, + setFieldsValue: PropTypes.func, +} + +export default StorageClassInput diff --git a/src/components/index.js b/src/components/index.js index 47ca4fb4b..1ea544717 100755 --- a/src/components/index.js +++ b/src/components/index.js @@ -17,6 +17,7 @@ import BackupLabelInputForRecurring from './BackupLabelInputForRecurring/BackupL import ExpansionErrorDetail from './ExpansionErrorDetail/ExpansionErrorDetail' import AutoComplete from './AutoComplete/AutoComplete' import SizeInput from './SizeInput/SizeInput' +import StorageClassInput from './StorageClassInput/StorageClassInput' export { DropOption, @@ -38,4 +39,5 @@ export { ExpansionErrorDetail, AutoComplete, SizeInput, + StorageClassInput, } diff --git a/src/models/objectendpoint.js b/src/models/objectendpoint.js index 717f99f6a..5fdb4ca01 100644 --- a/src/models/objectendpoint.js +++ b/src/models/objectendpoint.js @@ -1,4 +1,5 @@ import { listObjectEndpoints, getObjectEndpoint, createObjectEndpoint, deleteObjectEndpoint } from '../services/objectendpoint' +import { listStorageClasses } from '../services/storageclass' import { wsChanges, updateState } from '../utils/websocket' import queryString from 'query-string' import { enableQueryData } from '../utils/dataDependency' @@ -8,6 +9,7 @@ export default { state: { ws: null, data: [], + storageclasses: [], selected: {}, resourceType: 'objectEndpoint', socketStatus: 'closed', @@ -27,7 +29,9 @@ export default { effects: { *query({ payload }, { call, put }) { const data = yield call(listObjectEndpoints, payload) + const classes = yield call(listStorageClasses, payload) yield put({ type: 'listObjectEndpoints', payload: { ...data } }) + yield put({ type: 'listStorageClasses', payload: { ...classes } }) }, *get({ payload }, { call, get }) { const data = yield call(getObjectEndpoint, payload) @@ -73,6 +77,12 @@ export default { ...action.payload, } }, + listStorageClasses(state, action) { + return { + ...state, + storageclasses: action.payload.data, + } + }, updateBackground(state, action) { return updateState(state, action) }, diff --git a/src/models/storageclass.js b/src/models/storageclass.js new file mode 100644 index 000000000..93389b2c7 --- /dev/null +++ b/src/models/storageclass.js @@ -0,0 +1,65 @@ +import { listStorageClasses } from '../services/storageclass' +import { wsChanges, updateState } from '../utils/websocket' +import { enableQueryData } from '../utils/dataDependency' +import queryString from 'query-string' + +export default { + namespace: 'storageClass', + state: { + ws: null, + data: [], + selected: {}, + resourceType: 'storageClass', + socketStatus: 'closed', + }, + subscriptions: { + setup({ dispatch, history }) { + history.listen(location => { + if (enableQueryData(location.pathname, 'storageClass')) { + dispatch({ + type: 'query', + payload: location.pathname.startsWith('/storageClass') ? queryString.parse(location.search) : {}, + }) + } + }) + }, + }, + effects: { + *query({ payload }, { call, put }) { + const data = yield call(listStorageClasses, payload) + yield put({ type: 'listStorageClasses', payload: { ...data } }) + }, + *startWS({ payload }, { select }) { + let ws = yield select(state => state.objectEndpoint.ws) + if (ws) { + ws.open() + } else { + wsChanges(payload.dispatch, payload.type, '1s', payload.ns) + } + }, + // eslint-disable-next-line no-unused-vars + *stopWS({ payload }, { select }) { + let ws = yield select(state => state.objectEndpoint.ws) + if (ws) { + ws.close(1000) + } + }, + }, + reducers: { + listStorageClasses(state, action) { + return { + ...state, + ...action.payload, + } + }, + updateBackground(state, action) { + return updateState(state, action) + }, + updateSocketStatus(state, action) { + return { ...state, socketStatus: action.payload } + }, + updateWs(state, action) { + return { ...state, ws: action.payload } + }, + }, +} diff --git a/src/routes/objectEndpoint/CreateObjectEndpoint.js b/src/routes/objectEndpoint/CreateObjectEndpoint.js index 09ab9f875..163daf581 100644 --- a/src/routes/objectEndpoint/CreateObjectEndpoint.js +++ b/src/routes/objectEndpoint/CreateObjectEndpoint.js @@ -1,10 +1,9 @@ import React from 'react' import PropTypes from 'prop-types' -import { Form, Input, Collapse } from 'antd' -import { ModalBlur, SizeInput } from '../../components' +import { Form, Input } from 'antd' +import { ModalBlur, SizeInput, StorageClassInput } from '../../components' const FormItem = Form.Item -const Panel = Collapse.Panel const formItemLayout = { labelCol: { @@ -57,6 +56,14 @@ const modal = ({ setFieldsValue, } + const storageclassInputProps = { + classes: item.storageclasses, + state: item, + getFieldDecorator, + getFieldsValue, + setFieldsValue, + } + return (
@@ -73,6 +80,8 @@ const modal = ({ + + {getFieldDecorator('accesskey', { initialValue: item.accesskey, @@ -95,10 +104,6 @@ const modal = ({ ], })()} - - - -
) diff --git a/src/routes/objectEndpoint/index.js b/src/routes/objectEndpoint/index.js index 14b86bef9..911c985a1 100644 --- a/src/routes/objectEndpoint/index.js +++ b/src/routes/objectEndpoint/index.js @@ -32,7 +32,7 @@ class ObjectEndpoint extends React.Component { render() { const me = this const { dispatch, loading, location } = this.props - const { data } = this.props.objectEndpoint + const { data, storageclasses } = this.props.objectEndpoint const { field, value } = queryString.parse(this.props.location.search) let objectendpoints = data.filter((item) => { @@ -48,6 +48,7 @@ class ObjectEndpoint extends React.Component { const createObjectEndpointModalProps = { item: { + storageclasses, accesskey: Math.random().toString(36).substr(2, 6), secretkey: Math.random().toString(36).substr(2, 6), }, diff --git a/src/services/storageclass.js b/src/services/storageclass.js new file mode 100644 index 000000000..c8a3eea17 --- /dev/null +++ b/src/services/storageclass.js @@ -0,0 +1,8 @@ +import { request } from '../utils' + +export async function listStorageClasses() { + return request({ + url: '/v1/storageclasses', + method: 'get', + }) +} diff --git a/src/utils/dataDependency.js b/src/utils/dataDependency.js index 1fc0f1324..7a787a071 100644 --- a/src/utils/dataDependency.js +++ b/src/utils/dataDependency.js @@ -124,6 +124,15 @@ const dependency = { key: 'systemBackups', }], }, + storageClass: { + path: '/storageClass', + runWs: [ + { + ns: 'storageClass', + key: 'storageclasses', + }, + ], + }, } const allWs = [{ ns: 'volume', @@ -161,6 +170,9 @@ const allWs = [{ }, { ns: 'objectEndpoint', key: 'objectendpoints', +}, { + ns: 'storageClass', + key: 'storageclasses', }] const httpDataDependency = { @@ -175,7 +187,7 @@ const httpDataDependency = { '/instanceManager': ['volume', 'instanceManager'], '/orphanedData': ['orphanedData'], '/systemBackups': ['systemBackups'], - '/objectEndpoint': ['objectEndpoint'], + '/objectEndpoint': ['objectEndpoint', 'storageClass'], } export function getDataDependency(pathName) { From eddc893abc219cf84a6ad99aad7fa6e92273cc46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Wed, 16 Aug 2023 09:28:26 +0200 Subject: [PATCH 18/45] Object Endpoint: Create Stub Edit dialogue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create a stub dialogue for editing object endpoints. Signed-off-by: Moritz Röhrich --- .../objectEndpoint/CreateObjectEndpoint.js | 1 + .../objectEndpoint/EditObjectEndpoint.js | 83 +++++++++++++++++++ .../objectEndpoint/ObjectEndpointActions.js | 6 +- .../objectEndpoint/ObjectEndpointList.js | 11 ++- src/routes/objectEndpoint/index.js | 34 ++++++++ 5 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 src/routes/objectEndpoint/EditObjectEndpoint.js diff --git a/src/routes/objectEndpoint/CreateObjectEndpoint.js b/src/routes/objectEndpoint/CreateObjectEndpoint.js index 163daf581..5a7963812 100644 --- a/src/routes/objectEndpoint/CreateObjectEndpoint.js +++ b/src/routes/objectEndpoint/CreateObjectEndpoint.js @@ -42,6 +42,7 @@ const modal = ({ } const modalOpts = { + title: 'Create Object Endpoint', visible, onCancel, width: 800, diff --git a/src/routes/objectEndpoint/EditObjectEndpoint.js b/src/routes/objectEndpoint/EditObjectEndpoint.js new file mode 100644 index 000000000..9765218dd --- /dev/null +++ b/src/routes/objectEndpoint/EditObjectEndpoint.js @@ -0,0 +1,83 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Form, Input } from 'antd' +import { ModalBlur, SizeInput } from '../../components' + +const FormItem = Form.Item + +const formItemLayout = { + labelCol: { + span: 4, + }, + wrapperCol: { + span: 17, + }, +} + +const modal = ({ + form: { + getFieldDecorator, + validateFields, + getFieldsValue, + setFieldsValue, + }, + selected, + visible, + onCancel, + onOk, +}) => { + function handleOk() { + validateFields((errors) => { + if (errors) { return } + + const data = { + ...getFieldsValue(), + size: `${getFieldsValue().size}${getFieldsValue().unit}`, + } + + if (data.unit) { delete data.unit } + + onOk(data) + }) + } + + const modalOpts = { + title: 'Edit Object Endpoint', + visible, + onCancel, + width: 800, + onOk: handleOk, + style: { top: 0 }, + } + + const sizeInputProps = { + state: selected, + getFieldDecorator, + getFieldsValue, + setFieldsValue, + } + + return ( + +
+ + {getFieldDecorator('name', { + initialValue: selected.name, + })()} + + + + +
+ ) +} + +modal.propTypes = { + form: PropTypes.object.isRequired, + selected: PropTypes.object, + visible: PropTypes.bool, + onCancel: PropTypes.func, + onOk: PropTypes.func, +} + +export default Form.create()(modal) diff --git a/src/routes/objectEndpoint/ObjectEndpointActions.js b/src/routes/objectEndpoint/ObjectEndpointActions.js index 8cd5d101d..62a358931 100644 --- a/src/routes/objectEndpoint/ObjectEndpointActions.js +++ b/src/routes/objectEndpoint/ObjectEndpointActions.js @@ -29,8 +29,8 @@ function actions({ selected, deleteObjectEndpoint, editObjectEndpoint }) { } const availableActions = [ - { key: 'delete', name: 'Delete' }, { key: 'edit', name: 'Edit' }, + { key: 'delete', name: 'Delete' }, ] return ( @@ -42,8 +42,8 @@ function actions({ selected, deleteObjectEndpoint, editObjectEndpoint }) { actions.propTypes = { selected: PropTypes.object, - deleteRecurringJob: PropTypes.func, - editRecurringJob: PropTypes.func, + editObjectEndpoint: PropTypes.func, + deleteObjectEndpoint: PropTypes.func, } export default actions diff --git a/src/routes/objectEndpoint/ObjectEndpointList.js b/src/routes/objectEndpoint/ObjectEndpointList.js index 9bf11a7fd..023714dd1 100644 --- a/src/routes/objectEndpoint/ObjectEndpointList.js +++ b/src/routes/objectEndpoint/ObjectEndpointList.js @@ -4,8 +4,16 @@ import { Table, Tooltip } from 'antd' import { pagination } from '../../utils/page' import ObjectEndpointActions from './ObjectEndpointActions' -function list({ dataSource, height, loading, rowSelection, deleteObjectEndpoint }) { +function list({ + dataSource, + height, + loading, + rowSelection, + editObjectEndpoint, + deleteObjectEndpoint, +}) { const objectEndpointActionsProps = { + editObjectEndpoint, deleteObjectEndpoint, } @@ -90,6 +98,7 @@ list.propTypes = { heigth: PropTypes.number, loading: PropTypes.bool, rowSelection: PropTypes.object, + editObjectEndpoint: PropTypes.func, deleteObjectEndpoint: PropTypes.func, } diff --git a/src/routes/objectEndpoint/index.js b/src/routes/objectEndpoint/index.js index 911c985a1..f28f6cf81 100644 --- a/src/routes/objectEndpoint/index.js +++ b/src/routes/objectEndpoint/index.js @@ -6,6 +6,7 @@ import { routerRedux } from 'dva/router' import { Row, Col, Button } from 'antd' import { Filter } from '../../components/index' import CreateObjectEndpoint from './CreateObjectEndpoint' +import EditObjectEndpoint from './EditObjectEndpoint' import ObjectEndpointList from './ObjectEndpointList' import ObjectEndpointBulkActions from './ObjectEndpointBulkActions' @@ -17,6 +18,8 @@ class ObjectEndpoint extends React.Component { selectedRows: [], createObjectEndpointModalVisible: false, createObjectEndpointModalKey: Math.random(), + editObjectEndpointModalVisible: false, + editObjectEndpointModalKey: Math.random(), } } @@ -29,6 +32,14 @@ class ObjectEndpoint extends React.Component { }) } + showEditObjectEndpointModal = () => { + this.setState({ + ...this.state, + editObjectEndpointModalVisible: true, + editObjectEndpointModalKey: Math.random(), + }) + } + render() { const me = this const { dispatch, loading, location } = this.props @@ -71,6 +82,27 @@ class ObjectEndpoint extends React.Component { }, } + const editObjectEndpointModalProps = { + selected: {}, + visible: this.state.editObjectEndpointModalVisible, + onCancel() { + me.setState({ + ...me.state, + editObjectEndpointModalVisible: false, + }) + }, + onOk(record) { + me.setState({ + ...me.state, + editObjectEndpointModalVisible: false, + }) + dispatch({ + type: 'objectEndpoint/update', + payload: record, + }) + }, + } + const objectEndpointListProps = { dataSource: objectendpoints, height: this.state.height, @@ -84,6 +116,7 @@ class ObjectEndpoint extends React.Component { }) }, }, + editObjectEndpoint: this.showEditObjectEndpointModal, deleteObjectEndpoint(record) { dispatch({ type: 'objectEndpoint/delete', @@ -142,6 +175,7 @@ class ObjectEndpoint extends React.Component { {this.state.createObjectEndpointModalVisible && } + {this.state.editObjectEndpointModalVisible && }
) From cc43e42e379bea45203377e9ee89400978e51cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Wed, 6 Sep 2023 09:16:49 +0200 Subject: [PATCH 19/45] object store: rename, adjust creation dialogue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename "object endpoint" to "object store" - Adjust creation dialogue to match the newest iteration of the ObjectStore CRD Signed-off-by: Moritz Röhrich --- src/components/Layout/Footer.js | 10 +- .../StorageClassInput/StorageClassInput.js | 54 ----- src/components/index.js | 2 - src/index.js | 4 +- .../{objectendpoint.js => objectstore.js} | 40 ++-- src/models/storageclass.js | 65 ------ src/publicPath.js | 4 +- src/router.js | 8 +- .../objectEndpoint/CreateObjectEndpoint.js | 121 ----------- src/routes/objectstorage/CreateObjectStore.js | 197 ++++++++++++++++++ .../EditObjectStore.js} | 2 +- .../ObjectStoreActions.js} | 12 +- .../ObjectStoreBulkActions.js} | 8 +- .../ObjectStoreList.js} | 34 +-- .../index.js | 108 +++++----- src/services/objectendpoint.js | 32 --- src/services/objectstore.js | 32 +++ src/services/storageclass.js | 8 - src/utils/dataDependency.js | 26 +-- src/utils/menu.js | 2 +- 20 files changed, 347 insertions(+), 422 deletions(-) delete mode 100644 src/components/StorageClassInput/StorageClassInput.js rename src/models/{objectendpoint.js => objectstore.js} (55%) delete mode 100644 src/models/storageclass.js delete mode 100644 src/routes/objectEndpoint/CreateObjectEndpoint.js create mode 100644 src/routes/objectstorage/CreateObjectStore.js rename src/routes/{objectEndpoint/EditObjectEndpoint.js => objectstorage/EditObjectStore.js} (97%) rename src/routes/{objectEndpoint/ObjectEndpointActions.js => objectstorage/ObjectStoreActions.js} (74%) rename src/routes/{objectEndpoint/ObjectEndpointBulkActions.js => objectstorage/ObjectStoreBulkActions.js} (78%) rename src/routes/{objectEndpoint/ObjectEndpointList.js => objectstorage/ObjectStoreList.js} (73%) rename src/routes/{objectEndpoint => objectstorage}/index.js (53%) delete mode 100644 src/services/objectendpoint.js create mode 100644 src/services/objectstore.js delete mode 100644 src/services/storageclass.js diff --git a/src/components/Layout/Footer.js b/src/components/Layout/Footer.js index d6c16dfe5..a8a1613b3 100644 --- a/src/components/Layout/Footer.js +++ b/src/components/Layout/Footer.js @@ -10,7 +10,7 @@ import semver from 'semver' import BundlesModel from './BundlesModel' import StableLonghornVersions from './StableLonghornVersions' -function Footer({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectEndpoint, storageClass, backup, systemBackups, dispatch }) { +function Footer({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectstores, backup, systemBackups, dispatch }) { const { bundlesropsVisible, bundlesropsKey, stableLonghornVersionslVisible, stableLonghornVersionsKey, okText, modalButtonDisabled, progressPercentage } = app const currentVersion = config.version === '${VERSION}' ? 'dev' : config.version // eslint-disable-line no-template-curly-in-string const issueHref = 'https://github.com/longhorn/longhorn/issues/new/choose' @@ -128,8 +128,7 @@ function Footer({ app, host, volume, setting, engineimage, eventlog, backingImag {getStatusIcon(eventlog)} {getStatusIcon(backingImage)} {getStatusIcon(recurringJob)} - {getStatusIcon(objectEndpoint)} - {getStatusIcon(storageClass)} + {getStatusIcon(objectstores)} {getBackupStatusIcon(backup, 'backupVolumes')} {getBackupStatusIcon(backup, 'backups')} {getSystemBackupStatusIcon(systemBackups, 'systemBackup')} @@ -150,12 +149,11 @@ Footer.propTypes = { eventlog: PropTypes.object, backingImage: PropTypes.object, recurringJob: PropTypes.object, - objectEndpoint: PropTypes.object, - storageClass: PropTypes.object, + objectstores: PropTypes.object, app: PropTypes.object, backup: PropTypes.object, systemBackups: PropTypes.object, dispatch: PropTypes.func, } -export default connect(({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectEndpoint, storageClass, backup, systemBackups }) => ({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectEndpoint, storageClass, backup, systemBackups }))(Footer) +export default connect(({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectstores, backup, systemBackups }) => ({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectstores, backup, systemBackups }))(Footer) diff --git a/src/components/StorageClassInput/StorageClassInput.js b/src/components/StorageClassInput/StorageClassInput.js deleted file mode 100644 index 43ba2fd71..000000000 --- a/src/components/StorageClassInput/StorageClassInput.js +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { Form, Select } from 'antd' - -const FormItem = Form.Item -const Option = Select.Option - -class StorageClassInput extends React.Component { - state = { - storageclass: 'longhorn', - } - - render() { - const { getFieldDecorator, getFieldsValue, setFieldsValue } = this.props - - function classChange(value) { - setFieldsValue({ - ...getFieldsValue(), - storageclass: value, - }) - } - - return ( -
- - {getFieldDecorator('storageclass', { - initialValue: this.state.storageclass, - rules: [{ required: true, message: 'Please select a storage class' }], - })( - - )} - -
- ) - } -} - -StorageClassInput.propTypes = { - classes: PropTypes.array, - state: PropTypes.object, - getFieldDecorator: PropTypes.func, - getFieldsValue: PropTypes.func, - setFieldsValue: PropTypes.func, -} - -export default StorageClassInput diff --git a/src/components/index.js b/src/components/index.js index 1ea544717..47ca4fb4b 100755 --- a/src/components/index.js +++ b/src/components/index.js @@ -17,7 +17,6 @@ import BackupLabelInputForRecurring from './BackupLabelInputForRecurring/BackupL import ExpansionErrorDetail from './ExpansionErrorDetail/ExpansionErrorDetail' import AutoComplete from './AutoComplete/AutoComplete' import SizeInput from './SizeInput/SizeInput' -import StorageClassInput from './StorageClassInput/StorageClassInput' export { DropOption, @@ -39,5 +38,4 @@ export { ExpansionErrorDetail, AutoComplete, SizeInput, - StorageClassInput, } diff --git a/src/index.js b/src/index.js index 696340047..974d023c7 100755 --- a/src/index.js +++ b/src/index.js @@ -12,7 +12,7 @@ import recurringJob from './models/recurringJob' import instanceManager from './models/instanceManager' import orphanedData from './models/orphanedData' import systemBackups from './models/systemBackups' -import objectEndpoint from './models/objectendpoint' +import objectStore from './models/objectstore' // import assets import './assets/iconfont/iconfont.eot' @@ -41,7 +41,7 @@ app.model(recurringJob) app.model(instanceManager) app.model(orphanedData) app.model(systemBackups) -app.model(objectEndpoint) +app.model(objectStore) // 3. Router app.router(routerConfig) diff --git a/src/models/objectendpoint.js b/src/models/objectstore.js similarity index 55% rename from src/models/objectendpoint.js rename to src/models/objectstore.js index 5fdb4ca01..2d5bb7e9e 100644 --- a/src/models/objectendpoint.js +++ b/src/models/objectstore.js @@ -1,26 +1,24 @@ -import { listObjectEndpoints, getObjectEndpoint, createObjectEndpoint, deleteObjectEndpoint } from '../services/objectendpoint' -import { listStorageClasses } from '../services/storageclass' +import { listObjectStores, getObjectStore, createObjectStore, deleteObjectStore } from '../services/objectstore' import { wsChanges, updateState } from '../utils/websocket' import queryString from 'query-string' import { enableQueryData } from '../utils/dataDependency' export default { - namespace: 'objectEndpoint', + namespace: 'objectstorage', state: { ws: null, data: [], - storageclasses: [], selected: {}, - resourceType: 'objectEndpoint', + resourceType: 'objectstore', socketStatus: 'closed', }, subscriptions: { setup({ dispatch, history }) { history.listen(location => { - if (enableQueryData(location.pathname, 'objectEndpoint')) { + if (enableQueryData(location.pathname, 'objectstore')) { dispatch({ type: 'query', - payload: location.pathname.startsWith('/objectEndpoint') ? queryString.parse(location.search) : {}, + payload: location.pathname.startsWith('/objectstore') ? queryString.parse(location.search) : {}, }) } }) @@ -28,34 +26,32 @@ export default { }, effects: { *query({ payload }, { call, put }) { - const data = yield call(listObjectEndpoints, payload) - const classes = yield call(listStorageClasses, payload) - yield put({ type: 'listObjectEndpoints', payload: { ...data } }) - yield put({ type: 'listStorageClasses', payload: { ...classes } }) + const data = yield call(listObjectStores, payload) + yield put({ type: 'listObjectStores', payload: { ...data } }) }, *get({ payload }, { call, get }) { - const data = yield call(getObjectEndpoint, payload) - yield get({ type: 'getObjectEndpoint', payload: { ...data } }) + const data = yield call(getObjectStore, payload) + yield get({ type: 'getObjectStore', payload: { ...data } }) }, *create({ payload, callback }, { call, put }) { - yield call(createObjectEndpoint, payload) + yield call(createObjectStore, payload) if (callback) callback() yield put({ type: 'quey' }) }, *delete({ payload, callback }, { call, put }) { - yield call(deleteObjectEndpoint, payload) + yield call(deleteObjectStore, payload) if (callback) callback() yield put({ type: 'query' }) }, *bulkDelete({ payload, callback }, { call, put }) { if (payload && payload.length > 0) { - yield payload.map(item => call(deleteObjectEndpoint, item)) + yield payload.map(item => call(deleteObjectStore, item)) } if (callback) callback() yield put({ type: 'query' }) }, *startWS({ payload }, { select }) { - let ws = yield select(state => state.objectEndpoint.ws) + let ws = yield select(state => state.objectStore.ws) if (ws) { ws.open() } else { @@ -64,25 +60,19 @@ export default { }, // eslint-disable-next-line no-unused-vars *stopWS({ payload }, { select }) { - let ws = yield select(state => state.objectEndpoint.ws) + let ws = yield select(state => state.objectStore.ws) if (ws) { ws.close(1000) } }, }, reducers: { - listObjectEndpoints(state, action) { + listObjectStores(state, action) { return { ...state, ...action.payload, } }, - listStorageClasses(state, action) { - return { - ...state, - storageclasses: action.payload.data, - } - }, updateBackground(state, action) { return updateState(state, action) }, diff --git a/src/models/storageclass.js b/src/models/storageclass.js deleted file mode 100644 index 93389b2c7..000000000 --- a/src/models/storageclass.js +++ /dev/null @@ -1,65 +0,0 @@ -import { listStorageClasses } from '../services/storageclass' -import { wsChanges, updateState } from '../utils/websocket' -import { enableQueryData } from '../utils/dataDependency' -import queryString from 'query-string' - -export default { - namespace: 'storageClass', - state: { - ws: null, - data: [], - selected: {}, - resourceType: 'storageClass', - socketStatus: 'closed', - }, - subscriptions: { - setup({ dispatch, history }) { - history.listen(location => { - if (enableQueryData(location.pathname, 'storageClass')) { - dispatch({ - type: 'query', - payload: location.pathname.startsWith('/storageClass') ? queryString.parse(location.search) : {}, - }) - } - }) - }, - }, - effects: { - *query({ payload }, { call, put }) { - const data = yield call(listStorageClasses, payload) - yield put({ type: 'listStorageClasses', payload: { ...data } }) - }, - *startWS({ payload }, { select }) { - let ws = yield select(state => state.objectEndpoint.ws) - if (ws) { - ws.open() - } else { - wsChanges(payload.dispatch, payload.type, '1s', payload.ns) - } - }, - // eslint-disable-next-line no-unused-vars - *stopWS({ payload }, { select }) { - let ws = yield select(state => state.objectEndpoint.ws) - if (ws) { - ws.close(1000) - } - }, - }, - reducers: { - listStorageClasses(state, action) { - return { - ...state, - ...action.payload, - } - }, - updateBackground(state, action) { - return updateState(state, action) - }, - updateSocketStatus(state, action) { - return { ...state, socketStatus: action.payload } - }, - updateWs(state, action) { - return { ...state, ws: action.payload } - }, - }, -} diff --git a/src/publicPath.js b/src/publicPath.js index 7828a7a9e..48bb76c3d 100644 --- a/src/publicPath.js +++ b/src/publicPath.js @@ -18,7 +18,7 @@ let columnArr = [ 'lastBackupAt', 'actualSize', 'backendStoreDriver', - 'objectEndpoint', + 'objectStore', ] let pageSizeCollectionObject = { volumePageSize: 10, @@ -27,7 +27,7 @@ let pageSizeCollectionObject = { hostPageSize: 10, instanceManagerSize: 10, orphanedDataSize: 10, - objectEndpointSize: 10, + objectStoreSize: 10, } if (column) { columnArr = JSON.parse(column) diff --git a/src/router.js b/src/router.js index 3ec20787f..c7438e4c0 100755 --- a/src/router.js +++ b/src/router.js @@ -18,7 +18,7 @@ import recurringJobComponent from './routes/recurringJob/' import orphanedDataComponent from './routes/orphanedData/' import engineimageDetailComponent from './routes/engineimage/detail' import systemBackupsComponent from './routes/systemBackups/' -import objectEndpointComponent from './routes/objectEndpoint/' +import objectstorageComponent from './routes/objectstorage/' const Routers = function ({ history, app }) { const App = dynamic({ @@ -96,9 +96,9 @@ const Routers = function ({ history, app }) { component: () => systemBackupsComponent, }) - const objectEndpoint = dynamic({ + const objectstorage = dynamic({ app, - component: () => objectEndpointComponent, + component: () => objectstorageComponent, }) const path = '/' @@ -123,7 +123,7 @@ const Routers = function ({ history, app }) { - + diff --git a/src/routes/objectEndpoint/CreateObjectEndpoint.js b/src/routes/objectEndpoint/CreateObjectEndpoint.js deleted file mode 100644 index 5a7963812..000000000 --- a/src/routes/objectEndpoint/CreateObjectEndpoint.js +++ /dev/null @@ -1,121 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { Form, Input } from 'antd' -import { ModalBlur, SizeInput, StorageClassInput } from '../../components' - -const FormItem = Form.Item - -const formItemLayout = { - labelCol: { - span: 4, - }, - wrapperCol: { - span: 17, - }, -} - -const modal = ({ - form: { - getFieldDecorator, - validateFields, - getFieldsValue, - setFieldsValue, - }, - item, - visible, - onCancel, - onOk, -}) => { - function handleOk() { - validateFields((errors) => { - if (errors) { return } - - const data = { - ...getFieldsValue(), - size: `${getFieldsValue().size}${getFieldsValue().unit}`, - } - - if (data.unit) { delete data.unit } - - onOk(data) - }) - } - - const modalOpts = { - title: 'Create Object Endpoint', - visible, - onCancel, - width: 800, - onOk: handleOk, - style: { top: 0 }, - } - - const sizeInputProps = { - state: item, - getFieldDecorator, - getFieldsValue, - setFieldsValue, - } - - const storageclassInputProps = { - classes: item.storageclasses, - state: item, - getFieldDecorator, - getFieldsValue, - setFieldsValue, - } - - return ( - -
- - {getFieldDecorator('name', { - initialValue: item.name, - rules: [ - { - required: true, - message: 'Please input Object Endpoint name', - }, - ], - })()} - - - - - - - {getFieldDecorator('accesskey', { - initialValue: item.accesskey, - rules: [ - { - required: true, - message: 'Please input an access key', - }, - ], - })()} - - - {getFieldDecorator('secretkey', { - initialValue: item.secretkey, - rules: [ - { - required: true, - message: 'Please input a secret key', - }, - ], - })()} - - -
- ) -} - -modal.propTypes = { - form: PropTypes.object.isRequired, - item: PropTypes.object, - visible: PropTypes.bool, - onCancel: PropTypes.func, - onOk: PropTypes.func, -} - -export default Form.create()(modal) diff --git a/src/routes/objectstorage/CreateObjectStore.js b/src/routes/objectstorage/CreateObjectStore.js new file mode 100644 index 000000000..d1485f611 --- /dev/null +++ b/src/routes/objectstorage/CreateObjectStore.js @@ -0,0 +1,197 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Form, Input, InputNumber, Collapse, Select, Spin } from 'antd' +import { ModalBlur, SizeInput } from '../../components' + +const FormItem = Form.Item +const Panel = Collapse.Panel +const Option = Select.Option + +const formItemLayout = { + labelCol: { + span: 4, + }, + wrapperCol: { + span: 17, + }, +} + +const formItemLayoutForAdvanced = { + labelCol: { + span: 8, + }, + wrapperCol: { + span: 14, + }, +} + +const modal = ({ + form: { + getFieldDecorator, + validateFields, + getFieldsValue, + setFieldsValue, + }, + item, + visible, + diskTags, + nodeTags, + tagsLoading, + onCancel, + onOk, +}) => { + function handleOk() { + validateFields((errors) => { + if (errors) { return } + + const data = { + ...getFieldsValue(), + size: `${getFieldsValue().size}${getFieldsValue().unit}`, + staleReplicaTimeout: 2880, + } + + if (data.unit) { delete data.unit } + + onOk(data) + }) + } + + const modalOpts = { + title: 'Create Object Store', + visible, + onCancel, + width: 800, + onOk: handleOk, + style: { top: 0 }, + } + + const sizeInputProps = { + state: item, + getFieldDecorator, + getFieldsValue, + setFieldsValue, + } + + return ( + +
+ + {getFieldDecorator('name', { + initialValue: item.name, + rules: [ + { + required: true, + message: 'Please input Object Store name', + }, + ], + })()} + + + + + {getFieldDecorator('accesskey', { + initialValue: item.accesskey, + rules: [ + { + required: true, + message: 'Please input an access key', + }, + ], + })()} + + + {getFieldDecorator('secretkey', { + initialValue: item.secretkey, + rules: [ + { + required: true, + message: 'Please input a secret key', + }, + ], + })()} + + + + + {getFieldDecorator('numberOfReplicas', { + initialValue: item.numberOfReplicas, + rules: [ + { + required: true, + message: 'Please input the number of replicas', + }, + { + validator: (rule, value, callback) => { + if (value === '' || typeof value !== 'number') { + callback() + return + } + if (value < 1 || value > 10) { + callback('The value should be between 1 and 10') + } else if (!/^\d+$/.test(value)) { + callback('The value must be a positive integer') + } else { + callback() + } + }, + }, + ], + })()} + + + + + {getFieldDecorator('nodeSelector', { + initialValue: [], + })()} + + + + + {getFieldDecorator('diskSelector', { + initialValue: [], + })()} + + + + + {getFieldDecorator('replicaSoftAntiAffinity', { + initialValue: 'ignored', + })()} + + + {getFieldDecorator('replicaZoneSoftAntiAffinity', { + initialValue: 'ignored', + })()} + + + + +
+ ) +} + +modal.propTypes = { + form: PropTypes.object.isRequired, + item: PropTypes.object, + diskTags: PropTypes.array, + nodeTags: PropTypes.array, + visible: PropTypes.bool, + tagsLoading: PropTypes.bool, + onCancel: PropTypes.func, + onOk: PropTypes.func, +} + +export default Form.create()(modal) diff --git a/src/routes/objectEndpoint/EditObjectEndpoint.js b/src/routes/objectstorage/EditObjectStore.js similarity index 97% rename from src/routes/objectEndpoint/EditObjectEndpoint.js rename to src/routes/objectstorage/EditObjectStore.js index 9765218dd..6820be4e7 100644 --- a/src/routes/objectEndpoint/EditObjectEndpoint.js +++ b/src/routes/objectstorage/EditObjectStore.js @@ -42,7 +42,7 @@ const modal = ({ } const modalOpts = { - title: 'Edit Object Endpoint', + title: 'Edit Object Store', visible, onCancel, width: 800, diff --git a/src/routes/objectEndpoint/ObjectEndpointActions.js b/src/routes/objectstorage/ObjectStoreActions.js similarity index 74% rename from src/routes/objectEndpoint/ObjectEndpointActions.js rename to src/routes/objectstorage/ObjectStoreActions.js index 62a358931..900111b54 100644 --- a/src/routes/objectEndpoint/ObjectEndpointActions.js +++ b/src/routes/objectstorage/ObjectStoreActions.js @@ -5,24 +5,24 @@ import { DropOption } from '../../components' const confirm = Modal.confirm -function actions({ selected, deleteObjectEndpoint, editObjectEndpoint }) { +function actions({ selected, deleteObjectStore, editObjectStore }) { const handleMenuClick = (event, record) => { switch (event.key) { case 'delete': confirm({ - title: `Are you sure you want to delete Object Endpoint ${record.name} ?`, + title: `Are you sure you want to delete Object Store ${record.name} ?`, content: , width: 760, onOk() { - deleteObjectEndpoint(record) + deleteObjectStore(record) }, }) break case 'edit': - editObjectEndpoint(record) + editObjectStore(record) break default: } @@ -42,8 +42,8 @@ function actions({ selected, deleteObjectEndpoint, editObjectEndpoint }) { actions.propTypes = { selected: PropTypes.object, - editObjectEndpoint: PropTypes.func, - deleteObjectEndpoint: PropTypes.func, + editObjectStore: PropTypes.func, + deleteObjectStore: PropTypes.func, } export default actions diff --git a/src/routes/objectEndpoint/ObjectEndpointBulkActions.js b/src/routes/objectstorage/ObjectStoreBulkActions.js similarity index 78% rename from src/routes/objectEndpoint/ObjectEndpointBulkActions.js rename to src/routes/objectstorage/ObjectStoreBulkActions.js index bf026485a..0d5ae81a9 100644 --- a/src/routes/objectEndpoint/ObjectEndpointBulkActions.js +++ b/src/routes/objectstorage/ObjectStoreBulkActions.js @@ -4,19 +4,19 @@ import { Button, Modal, Alert } from 'antd' const confirm = Modal.confirm -function bulkActions({ selectedRows, deleteObjectEndpoint }) { +function bulkActions({ selectedRows, deleteObjectStore }) { const handleClick = (action) => { switch (action) { case 'delete': confirm({ - title: `Are you sure you want to delete Object Endpoint ${selectedRows.map(item => item.name).join(',')} ?`, + title: `Are you sure you want to delete Object Store(s) ${selectedRows.map(item => item.name).join(',')} ?`, content: , width: 760, onOk() { - deleteObjectEndpoint(selectedRows) + deleteObjectStore(selectedRows) }, }) break @@ -44,7 +44,7 @@ function bulkActions({ selectedRows, deleteObjectEndpoint }) { bulkActions.propTypes = { selectedRows: PropTypes.array, - deleteObjectEndpoint: PropTypes.func, + deleteObjectStore: PropTypes.func, } export default bulkActions diff --git a/src/routes/objectEndpoint/ObjectEndpointList.js b/src/routes/objectstorage/ObjectStoreList.js similarity index 73% rename from src/routes/objectEndpoint/ObjectEndpointList.js rename to src/routes/objectstorage/ObjectStoreList.js index 023714dd1..4cea86f48 100644 --- a/src/routes/objectEndpoint/ObjectEndpointList.js +++ b/src/routes/objectstorage/ObjectStoreList.js @@ -2,22 +2,22 @@ import React from 'react' import PropTypes from 'prop-types' import { Table, Tooltip } from 'antd' import { pagination } from '../../utils/page' -import ObjectEndpointActions from './ObjectEndpointActions' +import ObjectStoreActions from './ObjectStoreActions' function list({ dataSource, height, loading, rowSelection, - editObjectEndpoint, - deleteObjectEndpoint, + editObjectStore, + deleteObjectStore, }) { - const objectEndpointActionsProps = { - editObjectEndpoint, - deleteObjectEndpoint, + const actionsProps = { + editObjectStore, + deleteObjectStore, } - const endpointStateColorMap = { + const storeStateColorMap = { Unknown: { color: '#F15354', bg: 'rgba(241,83,84,.05)' }, Starting: { color: '#F1C40F', bg: 'rgba(241,196,15,.05)' }, Running: { color: '#27AE5F', bg: 'rgba(39,174,95,.05)' }, @@ -32,8 +32,8 @@ function list({ key: 'state', width: 160, render: (text, record) => { - const tooltip = `Endpoint ${record.name} is ${record.state}` - const colormap = endpointStateColorMap[record.state] || { color: '', bg: '' } + const tooltip = `Object Store ${record.name} is ${record.state}` + const colormap = storeStateColorMap[record.state] || { color: '', bg: '' } return (
@@ -54,12 +54,12 @@ function list({ }, }, { - title: 'Endpoint', - dataIndex: 'endpoint', - key: 'endpoint', + title: 'Endpoints', + dataIndex: 'endpoints', + key: 'endpoints', render: (text, record) => { return ( -
{record.endpoint}
+
{record.endpoints}
) }, }, @@ -69,14 +69,14 @@ function list({ width: 120, render: (text, record) => { return ( - + ) }, }, ] return ( -
+
{ + showCreateModal = () => { this.setState({ ...this.state, selected: {}, - createObjectEndpointModalVisible: true, - createObjectEndpointModalKey: Math.random(), + createModalVisible: true, + createModalKey: Math.random(), }) } - showEditObjectEndpointModal = () => { + showEditModal = () => { this.setState({ ...this.state, - editObjectEndpointModalVisible: true, - editObjectEndpointModalKey: Math.random(), + editModalVisible: true, + editModalKey: Math.random(), }) } render() { const me = this const { dispatch, loading, location } = this.props - const { data, storageclasses } = this.props.objectEndpoint + const { data } = this.props.objectstorage const { field, value } = queryString.parse(this.props.location.search) - let objectendpoints = data.filter((item) => { + let objectstores = data.filter((item) => { if (field === 'name') { return item[field] && item[field].indexOf(value.trim()) > -1 } return true }) - if (objectendpoints && objectendpoints.length > 0) { - objectendpoints.sort((a, b) => a.name.localeCompare(b.name)) + if (objectstores && objectstores.length > 0) { + objectstores.sort((a, b) => a.name.localeCompare(b.name)) } - const createObjectEndpointModalProps = { + const createModalProps = { item: { - storageclasses, accesskey: Math.random().toString(36).substr(2, 6), secretkey: Math.random().toString(36).substr(2, 6), }, - visible: this.state.createObjectEndpointModalVisible, + visible: this.state.createModalVisible, + diskTags: [], + nodeTags: [], + tagsLoading: false, onCancel() { me.setState({ ...me.state, - createObjectEndpointModalVisible: false, + createModalVisible: false, }) }, - onOk(newObjectEndpoint) { + onOk(newObjectStore) { me.setState({ ...me.state, - createObjectEndpointModalVisible: false, + createModalVisible: false, }) dispatch({ - type: 'objectEndpoint/create', - payload: newObjectEndpoint, + type: 'objectstorage/create', + payload: newObjectStore, }) }, } - const editObjectEndpointModalProps = { + const editModalProps = { selected: {}, - visible: this.state.editObjectEndpointModalVisible, + visible: this.state.editModalVisible, onCancel() { me.setState({ ...me.state, - editObjectEndpointModalVisible: false, + editModalVisible: false, }) }, onOk(record) { me.setState({ ...me.state, - editObjectEndpointModalVisible: false, + editModalVisible: false, }) dispatch({ - type: 'objectEndpoint/update', + type: 'objectstorage/update', payload: record, }) }, } - const objectEndpointListProps = { - dataSource: objectendpoints, + const listProps = { + dataSource: objectstores, height: this.state.height, loading, rowSelection: { @@ -116,16 +118,16 @@ class ObjectEndpoint extends React.Component { }) }, }, - editObjectEndpoint: this.showEditObjectEndpointModal, - deleteObjectEndpoint(record) { + editObjectStore: this.showEditModal, + deleteObjectStore(record) { dispatch({ - type: 'objectEndpoint/delete', + type: 'objectstorage/delete', payload: record, }) }, } - const objectEndpointFilterProps = { + const filterProps = { location, defaultField: 'name', fieldOption: [ @@ -134,24 +136,24 @@ class ObjectEndpoint extends React.Component { onSearch(filter) { const { field: filterField, value: filterValue } = filter filterField && filterValue ? dispatch(routerRedux.push({ - pathname: '/objectEndpoint', + pathname: '/objectstores', search: queryString.stringify({ ...queryString.parse(location.search), field: filterField, value: filterValue, }), })) : dispatch(routerRedux.push({ - pathname: '/objectEndpoint', + pathname: '/objectstores', search: queryString.stringify({}), })) }, } - const objectEndpointBulkActionsProps = { + const bulkActionsProps = { selectedRows: this.state.selectedRows, - deleteObjectEndpoint(record) { + deleteObjectStore(record) { dispatch({ - type: 'objectEndpoint/bulkDelete', + type: 'objectstorage/bulkDelete', payload: record, callback: () => { me.setState({ @@ -167,28 +169,28 @@ class ObjectEndpoint extends React.Component {
- + - + - - {this.state.createObjectEndpointModalVisible && } - {this.state.editObjectEndpointModalVisible && } - + + {this.state.createModalVisible && } + {this.state.editModalVisible && } + ) } } -ObjectEndpoint.propTypes = { - objectEndpoint: PropTypes.object, +ObjectStore.propTypes = { + objectstorage: PropTypes.object, loading: PropTypes.bool, location: PropTypes.object, dispatch: PropTypes.func, } export default connect( - ({ objectEndpoint, loading }) => ({ objectEndpoint, loading: loading.models.objectEndpoint }) -)(ObjectEndpoint) + ({ objectstorage, loading }) => ({ objectstorage, loading: loading.models.objectStore }) +)(ObjectStore) diff --git a/src/services/objectendpoint.js b/src/services/objectendpoint.js deleted file mode 100644 index fbc7ad613..000000000 --- a/src/services/objectendpoint.js +++ /dev/null @@ -1,32 +0,0 @@ -import { request } from '../utils' - -export async function listObjectEndpoints() { - return request({ - url: '/v1/objectendpoints', - method: 'get', - }) -} - -export async function getObjectEndpoint(name) { - return request({ - url: `/v1/objectendpoints/${name}`, - method: 'get', - }) -} - -export async function createObjectEndpoint(params) { - return request({ - url: '/v1/objectendpoints', - method: 'post', - data: params, - }) -} - -export async function deleteObjectEndpoint(params) { - if (params.name) { - return request({ - url: `/v1/objectendpoints/${params.name}`, - method: 'delete', - }) - } -} diff --git a/src/services/objectstore.js b/src/services/objectstore.js new file mode 100644 index 000000000..f15cfd6c5 --- /dev/null +++ b/src/services/objectstore.js @@ -0,0 +1,32 @@ +import { request } from '../utils' + +export async function listObjectStores() { + return request({ + url: '/v1/objectstores', + method: 'get', + }) +} + +export async function getObjectStore(name) { + return request({ + url: `/v1/objectstores/${name}`, + method: 'get', + }) +} + +export async function createObjectStore(params) { + return request({ + url: '/v1/objectstores', + method: 'post', + data: params, + }) +} + +export async function deleteObjectStore(params) { + if (params.name) { + return request({ + url: `/v1/objectstores/${params.name}`, + method: 'delete', + }) + } +} diff --git a/src/services/storageclass.js b/src/services/storageclass.js deleted file mode 100644 index c8a3eea17..000000000 --- a/src/services/storageclass.js +++ /dev/null @@ -1,8 +0,0 @@ -import { request } from '../utils' - -export async function listStorageClasses() { - return request({ - url: '/v1/storageclasses', - method: 'get', - }) -} diff --git a/src/utils/dataDependency.js b/src/utils/dataDependency.js index 7a787a071..516d48073 100644 --- a/src/utils/dataDependency.js +++ b/src/utils/dataDependency.js @@ -97,12 +97,12 @@ const dependency = { key: 'backups', }], }, - objectEndpoint: { - path: '/objectEndpoint', + objectstorage: { + path: '/objectstore', runWs: [ { - ns: 'objectEndpoint', - key: 'objectendpoints', + ns: 'objectstorage', + key: 'objectstores', }, ], }, @@ -124,15 +124,6 @@ const dependency = { key: 'systemBackups', }], }, - storageClass: { - path: '/storageClass', - runWs: [ - { - ns: 'storageClass', - key: 'storageclasses', - }, - ], - }, } const allWs = [{ ns: 'volume', @@ -168,11 +159,8 @@ const allWs = [{ ns: 'systemBackups', key: 'systemrestores', }, { - ns: 'objectEndpoint', - key: 'objectendpoints', -}, { - ns: 'storageClass', - key: 'storageclasses', + ns: 'objectstorage', + key: 'objectstores', }] const httpDataDependency = { @@ -187,7 +175,7 @@ const httpDataDependency = { '/instanceManager': ['volume', 'instanceManager'], '/orphanedData': ['orphanedData'], '/systemBackups': ['systemBackups'], - '/objectEndpoint': ['objectEndpoint', 'storageClass'], + '/objectstorage': ['objectstore'], } export function getDataDependency(pathName) { diff --git a/src/utils/menu.js b/src/utils/menu.js index 38aab4c1c..1ab612be0 100755 --- a/src/utils/menu.js +++ b/src/utils/menu.js @@ -31,7 +31,7 @@ module.exports = [ icon: 'copy', }, { - key: 'objectEndpoint', + key: 'objectstorage', name: 'Object Storage', icon: 'file', }, From b128ee7aa696cc8ec9846e369cb0a5b9d5472915 Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Wed, 6 Sep 2023 11:14:30 +0200 Subject: [PATCH 20/45] Various improvements - Change icon to something that looks like the AWS S3 logo (https://www.google.com/search?tbm=isch&as_q=aws+S3+icon), but the Ant icon library (https://ant.design/components/icon) is very limited in that regard. - Ignore IDE specific files Signed-off-by: Volker Theile --- src/utils/menu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/menu.js b/src/utils/menu.js index 1ab612be0..bfd4ce0d2 100755 --- a/src/utils/menu.js +++ b/src/utils/menu.js @@ -33,7 +33,7 @@ module.exports = [ { key: 'objectstorage', name: 'Object Storage', - icon: 'file', + icon: 'build', }, { key: 'setting', From bd555d8e47a39631bd9c079b81ca20a515512425 Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Wed, 6 Sep 2023 11:46:46 +0200 Subject: [PATCH 21/45] Code cleanup & improvements - Rename files and variables - Simplify code - Add more validators for access and secret key in the "Create Object Store" dialog - Use for secret key Signed-off-by: Volker Theile --- src/index.js | 2 +- src/models/{objectstore.js => objectStore.js} | 2 +- src/router.js | 8 ++-- .../CreateObjectStore.js | 42 +++++++++++++++---- .../EditObjectStore.js | 0 .../ObjectStoreActions.js | 0 .../ObjectStoreBulkActions.js | 0 .../ObjectStoreList.js | 0 .../{objectstorage => objectStorage}/index.js | 18 +++++--- .../{objectstore.js => objectStore.js} | 0 10 files changed, 51 insertions(+), 21 deletions(-) rename src/models/{objectstore.js => objectStore.js} (98%) rename src/routes/{objectstorage => objectStorage}/CreateObjectStore.js (87%) rename src/routes/{objectstorage => objectStorage}/EditObjectStore.js (100%) rename src/routes/{objectstorage => objectStorage}/ObjectStoreActions.js (100%) rename src/routes/{objectstorage => objectStorage}/ObjectStoreBulkActions.js (100%) rename src/routes/{objectstorage => objectStorage}/ObjectStoreList.js (100%) rename src/routes/{objectstorage => objectStorage}/index.js (89%) rename src/services/{objectstore.js => objectStore.js} (100%) diff --git a/src/index.js b/src/index.js index 974d023c7..0ed8f8901 100755 --- a/src/index.js +++ b/src/index.js @@ -12,7 +12,7 @@ import recurringJob from './models/recurringJob' import instanceManager from './models/instanceManager' import orphanedData from './models/orphanedData' import systemBackups from './models/systemBackups' -import objectStore from './models/objectstore' +import objectStore from './models/objectStore' // import assets import './assets/iconfont/iconfont.eot' diff --git a/src/models/objectstore.js b/src/models/objectStore.js similarity index 98% rename from src/models/objectstore.js rename to src/models/objectStore.js index 2d5bb7e9e..91301c053 100644 --- a/src/models/objectstore.js +++ b/src/models/objectStore.js @@ -1,4 +1,4 @@ -import { listObjectStores, getObjectStore, createObjectStore, deleteObjectStore } from '../services/objectstore' +import { listObjectStores, getObjectStore, createObjectStore, deleteObjectStore } from '../services/objectStore' import { wsChanges, updateState } from '../utils/websocket' import queryString from 'query-string' import { enableQueryData } from '../utils/dataDependency' diff --git a/src/router.js b/src/router.js index c7438e4c0..9277e01ed 100755 --- a/src/router.js +++ b/src/router.js @@ -18,7 +18,7 @@ import recurringJobComponent from './routes/recurringJob/' import orphanedDataComponent from './routes/orphanedData/' import engineimageDetailComponent from './routes/engineimage/detail' import systemBackupsComponent from './routes/systemBackups/' -import objectstorageComponent from './routes/objectstorage/' +import objectStorageComponent from './routes/objectStorage/' const Routers = function ({ history, app }) { const App = dynamic({ @@ -96,9 +96,9 @@ const Routers = function ({ history, app }) { component: () => systemBackupsComponent, }) - const objectstorage = dynamic({ + const objectStorage = dynamic({ app, - component: () => objectstorageComponent, + component: () => objectStorageComponent, }) const path = '/' @@ -123,7 +123,7 @@ const Routers = function ({ history, app }) { - + diff --git a/src/routes/objectstorage/CreateObjectStore.js b/src/routes/objectStorage/CreateObjectStore.js similarity index 87% rename from src/routes/objectstorage/CreateObjectStore.js rename to src/routes/objectStorage/CreateObjectStore.js index d1485f611..3ba769f16 100644 --- a/src/routes/objectstorage/CreateObjectStore.js +++ b/src/routes/objectStorage/CreateObjectStore.js @@ -9,7 +9,7 @@ const Option = Select.Option const formItemLayout = { labelCol: { - span: 4, + span: 5, }, wrapperCol: { span: 17, @@ -81,13 +81,17 @@ const modal = ({ rules: [ { required: true, - message: 'Please input Object Store name', + message: 'Please input the object store name', }, ], - })()} + })()} - - + +
+ + +
+ {getFieldDecorator('accesskey', { initialValue: item.accesskey, @@ -96,9 +100,22 @@ const modal = ({ required: true, message: 'Please input an access key', }, + { + pattern: /^[\w]+/, + message: 'The access key contains invalid characters', + }, + { + min: 16, + message: 'Minimum length is 16 characters', + }, + { + max: 128, + message: 'Maximum length is 128 characters', + }, ], - })()} + })()} + {getFieldDecorator('secretkey', { initialValue: item.secretkey, @@ -107,9 +124,14 @@ const modal = ({ required: true, message: 'Please input a secret key', }, + { + min: 16, + message: 'Minimum length is 16 characters', + }, ], - })()} + })()} + @@ -140,7 +162,7 @@ const modal = ({ - + {getFieldDecorator('nodeSelector', { initialValue: [], })()} + - + {getFieldDecorator('diskSelector', { initialValue: [], })()} + {getFieldDecorator('replicaZoneSoftAntiAffinity', { initialValue: 'ignored', diff --git a/src/routes/objectstorage/EditObjectStore.js b/src/routes/objectStorage/EditObjectStore.js similarity index 100% rename from src/routes/objectstorage/EditObjectStore.js rename to src/routes/objectStorage/EditObjectStore.js diff --git a/src/routes/objectstorage/ObjectStoreActions.js b/src/routes/objectStorage/ObjectStoreActions.js similarity index 100% rename from src/routes/objectstorage/ObjectStoreActions.js rename to src/routes/objectStorage/ObjectStoreActions.js diff --git a/src/routes/objectstorage/ObjectStoreBulkActions.js b/src/routes/objectStorage/ObjectStoreBulkActions.js similarity index 100% rename from src/routes/objectstorage/ObjectStoreBulkActions.js rename to src/routes/objectStorage/ObjectStoreBulkActions.js diff --git a/src/routes/objectstorage/ObjectStoreList.js b/src/routes/objectStorage/ObjectStoreList.js similarity index 100% rename from src/routes/objectstorage/ObjectStoreList.js rename to src/routes/objectStorage/ObjectStoreList.js diff --git a/src/routes/objectstorage/index.js b/src/routes/objectStorage/index.js similarity index 89% rename from src/routes/objectstorage/index.js rename to src/routes/objectStorage/index.js index 41393aa77..12930b47e 100644 --- a/src/routes/objectstorage/index.js +++ b/src/routes/objectStorage/index.js @@ -10,6 +10,11 @@ import EditObjectStore from './EditObjectStore' import ObjectStoreList from './ObjectStoreList' import ObjectStoreBulkActions from './ObjectStoreBulkActions' +// See https://docs.aws.amazon.com/IAM/latest/APIReference/API_AccessKey.html +const generateRandomKey = (length = 16, allowedChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_') => Array.from(window.crypto.getRandomValues(new Uint32Array(length))) + .map((x) => allowedChars[x % allowedChars.length]) + .join('') + class ObjectStore extends React.Component { constructor(props) { super(props) @@ -46,21 +51,22 @@ class ObjectStore extends React.Component { const { data } = this.props.objectstorage const { field, value } = queryString.parse(this.props.location.search) - let objectstores = data.filter((item) => { + let objectStores = data.filter((item) => { if (field === 'name') { return item[field] && item[field].indexOf(value.trim()) > -1 } return true }) - if (objectstores && objectstores.length > 0) { - objectstores.sort((a, b) => a.name.localeCompare(b.name)) + if (objectStores) { + objectStores.sort((a, b) => a.name.localeCompare(b.name)) } const createModalProps = { item: { - accesskey: Math.random().toString(36).substr(2, 6), - secretkey: Math.random().toString(36).substr(2, 6), + accesskey: generateRandomKey(), + secretkey: generateRandomKey(), + numberOfReplicas: 3, }, visible: this.state.createModalVisible, diskTags: [], @@ -106,7 +112,7 @@ class ObjectStore extends React.Component { } const listProps = { - dataSource: objectstores, + dataSource: objectStores, height: this.state.height, loading, rowSelection: { diff --git a/src/services/objectstore.js b/src/services/objectStore.js similarity index 100% rename from src/services/objectstore.js rename to src/services/objectStore.js From 34223a93aa928e0f7b91df37c25ab8a88a8e9fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Tue, 12 Sep 2023 09:55:05 +0200 Subject: [PATCH 22/45] object store: more options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add more options to object store creation dialogue - Organize advanced options in "data locality" and other "advanced configuration" to make the dialogue easier to understand - Propagate default number of replicas from settings Signed-off-by: Moritz Röhrich --- src/routes/objectStorage/CreateObjectStore.js | 85 ++++++++++++++++--- src/routes/objectStorage/index.js | 25 +++++- 2 files changed, 96 insertions(+), 14 deletions(-) diff --git a/src/routes/objectStorage/CreateObjectStore.js b/src/routes/objectStorage/CreateObjectStore.js index 3ba769f16..76848d83b 100644 --- a/src/routes/objectStorage/CreateObjectStore.js +++ b/src/routes/objectStorage/CreateObjectStore.js @@ -1,6 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' -import { Form, Input, InputNumber, Collapse, Select, Spin } from 'antd' +import { Form, Input, InputNumber, Collapse, Select, Spin, Checkbox } from 'antd' import { ModalBlur, SizeInput } from '../../components' const FormItem = Form.Item @@ -34,8 +34,6 @@ const modal = ({ }, item, visible, - diskTags, - nodeTags, tagsLoading, onCancel, onOk, @@ -133,7 +131,7 @@ const modal = ({ - + {getFieldDecorator('numberOfReplicas', { initialValue: item.numberOfReplicas, @@ -164,9 +162,9 @@ const modal = ({ {getFieldDecorator('nodeSelector', { - initialValue: [], + initialValue: item.nodeTags, })()} @@ -174,9 +172,9 @@ const modal = ({ {getFieldDecorator('diskSelector', { - initialValue: [], + initialValue: item.diskTags, })()} @@ -200,6 +198,75 @@ const modal = ({ )} + + + {getFieldDecorator('replicaDiskSoftAntiAffinity', { + initialValue: 'ignored', + })()} + + + + {getFieldDecorator('dataLocality', { + initialValue: item.defaultDataLocalityValue, + })()} + + + + + + + + {getFieldDecorator('backendStoreDriver', { + initialValue: 'v1', + rules: [ + { + validator: (rule, value, callback) => { + if (value === 'v2' && !item.enableSPDKDataEngineValue) { + callback('SPDK data engine is not enabled') + } + callback() + }, + }, + ], + })()} + + + + {getFieldDecorator('revisionCounterDisabled', { + valuePropName: 'checked', + initialValue: item.defaultRevisionCounterValue, + })()} + + + + {getFieldDecorator('replicaAutoBalance', { + initialValue: 'ignored', + })()} + + + + {getFieldDecorator('unmapMarkSnapChainRemoved', { + initialValue: 'ignored', + })()} + @@ -210,8 +277,6 @@ const modal = ({ modal.propTypes = { form: PropTypes.object.isRequired, item: PropTypes.object, - diskTags: PropTypes.array, - nodeTags: PropTypes.array, visible: PropTypes.bool, tagsLoading: PropTypes.bool, onCancel: PropTypes.func, diff --git a/src/routes/objectStorage/index.js b/src/routes/objectStorage/index.js index 12930b47e..f583949e1 100644 --- a/src/routes/objectStorage/index.js +++ b/src/routes/objectStorage/index.js @@ -51,6 +51,18 @@ class ObjectStore extends React.Component { const { data } = this.props.objectstorage const { field, value } = queryString.parse(this.props.location.search) + const settings = this.props.setting.data + const defaultReplicaCountSetting = settings.find(s => s.id === 'default-replica-count') + const defaultDataLocalitySetting = settings.find(s => s.id === 'default-data-locality') + const defaultRevisionCounterSetting = settings.find(s => s.id === 'disable-revision-counter') + const enableSPDKDataEngineSetting = settings.find(s => s.id === 'v2-data-engine') + + const defaultNumberOfReplicas = defaultReplicaCountSetting !== undefined ? parseInt(defaultReplicaCountSetting.value, 10) : 3 + const defaultDataLocalityOption = defaultDataLocalitySetting?.definition?.options ? defaultDataLocalitySetting.definition.options : [] + const defaultDataLocalityValue = defaultDataLocalitySetting?.value ? defaultDataLocalitySetting.value : 'disabled' + const defaultRevisionCounterValue = defaultRevisionCounterSetting?.value === 'true' + const enableSPDKDataEngineValue = enableSPDKDataEngineSetting?.value === 'true' + let objectStores = data.filter((item) => { if (field === 'name') { return item[field] && item[field].indexOf(value.trim()) > -1 @@ -66,11 +78,15 @@ class ObjectStore extends React.Component { item: { accesskey: generateRandomKey(), secretkey: generateRandomKey(), - numberOfReplicas: 3, + numberOfReplicas: defaultNumberOfReplicas, + diskTags: [], + nodeTags: [], + defaultDataLocalityOption, + defaultDataLocalityValue, + defaultRevisionCounterValue, + enableSPDKDataEngineValue, }, visible: this.state.createModalVisible, - diskTags: [], - nodeTags: [], tagsLoading: false, onCancel() { me.setState({ @@ -195,8 +211,9 @@ ObjectStore.propTypes = { loading: PropTypes.bool, location: PropTypes.object, dispatch: PropTypes.func, + setting: PropTypes.object, } export default connect( - ({ objectstorage, loading }) => ({ objectstorage, loading: loading.models.objectStore }) + ({ objectstorage, loading, setting }) => ({ objectstorage, loading: loading.models.objectStore, setting }) )(ObjectStore) From d0e5ae087c7afef07e0eedb2978744e210808771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Tue, 12 Sep 2023 10:20:24 +0200 Subject: [PATCH 23/45] object store: fix websocket, add state colors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add colors for states Terminating and Stopped - Fix websocket: typo in models/objectStore.js Signed-off-by: Moritz Röhrich --- src/models/objectStore.js | 4 ++-- src/routes/objectStorage/ObjectStoreList.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/models/objectStore.js b/src/models/objectStore.js index 91301c053..3413c85d7 100644 --- a/src/models/objectStore.js +++ b/src/models/objectStore.js @@ -51,7 +51,7 @@ export default { yield put({ type: 'query' }) }, *startWS({ payload }, { select }) { - let ws = yield select(state => state.objectStore.ws) + let ws = yield select(state => state.objectstorage.ws) if (ws) { ws.open() } else { @@ -60,7 +60,7 @@ export default { }, // eslint-disable-next-line no-unused-vars *stopWS({ payload }, { select }) { - let ws = yield select(state => state.objectStore.ws) + let ws = yield select(state => state.objectstorage.ws) if (ws) { ws.close(1000) } diff --git a/src/routes/objectStorage/ObjectStoreList.js b/src/routes/objectStorage/ObjectStoreList.js index 4cea86f48..acaed6b8c 100644 --- a/src/routes/objectStorage/ObjectStoreList.js +++ b/src/routes/objectStorage/ObjectStoreList.js @@ -21,7 +21,9 @@ function list({ Unknown: { color: '#F15354', bg: 'rgba(241,83,84,.05)' }, Starting: { color: '#F1C40F', bg: 'rgba(241,196,15,.05)' }, Running: { color: '#27AE5F', bg: 'rgba(39,174,95,.05)' }, - Stopping: { color: '#DEE1E3', bg: 'rgba(222,225,227,.05)' }, + Stopping: { color: '#DEE1E3', bg: 'rgba(241,241,241,.05)' }, + Stopped: { color: '#959CA6', bg: 'rgba(241,241,241,.05)' }, + Terminating: { color: '#DEE1E3', bg: 'rgba(222,225,227,.05)' }, Error: { color: '#F15354', bg: 'rgba(241,83,84,.1)' }, } From 961924db34cc7e4b36a3e81da85be5f59fb6ff61 Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Tue, 12 Sep 2023 16:07:32 +0200 Subject: [PATCH 24/45] Populate edit dialog Signed-off-by: Volker Theile --- src/routes/objectStorage/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/routes/objectStorage/index.js b/src/routes/objectStorage/index.js index f583949e1..c909cc18f 100644 --- a/src/routes/objectStorage/index.js +++ b/src/routes/objectStorage/index.js @@ -37,9 +37,10 @@ class ObjectStore extends React.Component { }) } - showEditModal = () => { + showEditModal = (record) => { this.setState({ ...this.state, + selected: record, editModalVisible: true, editModalKey: Math.random(), }) @@ -107,7 +108,7 @@ class ObjectStore extends React.Component { } const editModalProps = { - selected: {}, + selected: this.state.selected, visible: this.state.editModalVisible, onCancel() { me.setState({ From 825ed83bad9e24c6551d841e9085c4d8ed517251 Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Wed, 13 Sep 2023 09:48:36 +0200 Subject: [PATCH 25/45] Various enhancements - Reload list after creating a new ObjectStore - Add new `Administrate` menu which will redirect to the s3gw-ui - Disable action menus if necessary Signed-off-by: Volker Theile --- src/models/objectStore.js | 2 +- .../objectStorage/ObjectStoreActions.js | 12 +++++-- src/routes/objectStorage/ObjectStoreList.js | 3 ++ src/routes/objectStorage/helper/index.js | 10 ++++++ src/routes/objectStorage/index.js | 33 +++++++++---------- 5 files changed, 39 insertions(+), 21 deletions(-) create mode 100644 src/routes/objectStorage/helper/index.js diff --git a/src/models/objectStore.js b/src/models/objectStore.js index 3413c85d7..369cd94c7 100644 --- a/src/models/objectStore.js +++ b/src/models/objectStore.js @@ -36,7 +36,7 @@ export default { *create({ payload, callback }, { call, put }) { yield call(createObjectStore, payload) if (callback) callback() - yield put({ type: 'quey' }) + yield put({ type: 'query' }) }, *delete({ payload, callback }, { call, put }) { yield call(deleteObjectStore, payload) diff --git a/src/routes/objectStorage/ObjectStoreActions.js b/src/routes/objectStorage/ObjectStoreActions.js index 900111b54..f4242903b 100644 --- a/src/routes/objectStorage/ObjectStoreActions.js +++ b/src/routes/objectStorage/ObjectStoreActions.js @@ -2,10 +2,11 @@ import React from 'react' import PropTypes from 'prop-types' import { Modal, Alert } from 'antd' import { DropOption } from '../../components' +import { isObjectStoreAdministrable, isObjectStoreDeletable, isObjectStoreEditable } from './helper' const confirm = Modal.confirm -function actions({ selected, deleteObjectStore, editObjectStore }) { +function actions({ selected, deleteObjectStore, editObjectStore, administrateObjectStore }) { const handleMenuClick = (event, record) => { switch (event.key) { case 'delete': @@ -24,13 +25,17 @@ function actions({ selected, deleteObjectStore, editObjectStore }) { case 'edit': editObjectStore(record) break + case 'administrate': + administrateObjectStore(record) + break default: } } const availableActions = [ - { key: 'edit', name: 'Edit' }, - { key: 'delete', name: 'Delete' }, + { key: 'edit', name: 'Edit', disabled: !isObjectStoreEditable(selected) }, + { key: 'administrate', name: 'Administrate', disabled: !isObjectStoreAdministrable(selected) }, + { key: 'delete', name: 'Delete', disabled: !isObjectStoreDeletable(selected) }, ] return ( @@ -43,6 +48,7 @@ function actions({ selected, deleteObjectStore, editObjectStore }) { actions.propTypes = { selected: PropTypes.object, editObjectStore: PropTypes.func, + administrateObjectStore: PropTypes.func, deleteObjectStore: PropTypes.func, } diff --git a/src/routes/objectStorage/ObjectStoreList.js b/src/routes/objectStorage/ObjectStoreList.js index acaed6b8c..9b89a7c89 100644 --- a/src/routes/objectStorage/ObjectStoreList.js +++ b/src/routes/objectStorage/ObjectStoreList.js @@ -10,10 +10,12 @@ function list({ loading, rowSelection, editObjectStore, + administrateObjectStore, deleteObjectStore, }) { const actionsProps = { editObjectStore, + administrateObjectStore, deleteObjectStore, } @@ -101,6 +103,7 @@ list.propTypes = { loading: PropTypes.bool, rowSelection: PropTypes.object, editObjectStore: PropTypes.func, + administrateObjectStore: PropTypes.func, deleteObjectStore: PropTypes.func, } diff --git a/src/routes/objectStorage/helper/index.js b/src/routes/objectStorage/helper/index.js new file mode 100644 index 000000000..1df5829e8 --- /dev/null +++ b/src/routes/objectStorage/helper/index.js @@ -0,0 +1,10 @@ +// See https://docs.aws.amazon.com/IAM/latest/APIReference/API_AccessKey.html +export const generateRandomKey = (length = 16, allowedChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_') => Array.from(window.crypto.getRandomValues(new Uint32Array(length))) + .map((x) => allowedChars[x % allowedChars.length]) + .join('') + +export const isObjectStoreEditable = (record) => !['unknown', 'terminating'].includes(record.state) + +export const isObjectStoreAdministrable = (record) => isObjectStoreEditable(record) && record.endpoints?.length + +export const isObjectStoreDeletable = (record) => !['terminating'].includes(record.state) diff --git a/src/routes/objectStorage/index.js b/src/routes/objectStorage/index.js index c909cc18f..fe79ddc27 100644 --- a/src/routes/objectStorage/index.js +++ b/src/routes/objectStorage/index.js @@ -9,11 +9,7 @@ import CreateObjectStore from './CreateObjectStore' import EditObjectStore from './EditObjectStore' import ObjectStoreList from './ObjectStoreList' import ObjectStoreBulkActions from './ObjectStoreBulkActions' - -// See https://docs.aws.amazon.com/IAM/latest/APIReference/API_AccessKey.html -const generateRandomKey = (length = 16, allowedChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_') => Array.from(window.crypto.getRandomValues(new Uint32Array(length))) - .map((x) => allowedChars[x % allowedChars.length]) - .join('') +import { generateRandomKey } from './helper/index' class ObjectStore extends React.Component { constructor(props) { @@ -37,15 +33,6 @@ class ObjectStore extends React.Component { }) } - showEditModal = (record) => { - this.setState({ - ...this.state, - selected: record, - editModalVisible: true, - editModalKey: Math.random(), - }) - } - render() { const me = this const { dispatch, loading, location } = this.props @@ -141,8 +128,20 @@ class ObjectStore extends React.Component { }) }, }, - editObjectStore: this.showEditModal, - deleteObjectStore(record) { + editObjectStore: (record) => { + this.setState({ + ...this.state, + selected: record, + editModalVisible: true, + editModalKey: Math.random(), + }) + }, + administrateObjectStore: (record) => { + if (record.endpoints?.length) { + window.open(record.endpoints[0], '_blank', 'noreferrer') + } + }, + deleteObjectStore: (record) => { dispatch({ type: 'objectstorage/delete', payload: record, @@ -174,7 +173,7 @@ class ObjectStore extends React.Component { const bulkActionsProps = { selectedRows: this.state.selectedRows, - deleteObjectStore(record) { + deleteObjectStore: (record) => { dispatch({ type: 'objectstorage/bulkDelete', payload: record, From 9c7ce30fff5ba1d5f08471708cb89c03383d6b68 Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Wed, 13 Sep 2023 09:50:00 +0200 Subject: [PATCH 26/45] Fix table sizing/resizing Signed-off-by: Volker Theile --- src/routes/objectStorage/ObjectStoreList.js | 4 ++-- src/routes/objectStorage/index.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/routes/objectStorage/ObjectStoreList.js b/src/routes/objectStorage/ObjectStoreList.js index 9b89a7c89..c3f3bf425 100644 --- a/src/routes/objectStorage/ObjectStoreList.js +++ b/src/routes/objectStorage/ObjectStoreList.js @@ -80,7 +80,7 @@ function list({ ] return ( -
+
{ + height = document.getElementById('objectStoreTable').offsetHeight - C.ContainerMarginHeight + this.setState({ + height, + }) + this.props.dispatch({ type: 'app/changeNavbar' }) + } + } + showCreateModal = () => { this.setState({ ...this.state, From 543276d1bc2e36474f5ae176ef11b7f9da1c5239 Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Wed, 13 Sep 2023 09:50:32 +0200 Subject: [PATCH 27/45] Fix coloring of ObjectStore state and capitalize the text Signed-off-by: Volker Theile --- src/routes/objectStorage/ObjectStoreList.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/routes/objectStorage/ObjectStoreList.js b/src/routes/objectStorage/ObjectStoreList.js index c3f3bf425..49384c180 100644 --- a/src/routes/objectStorage/ObjectStoreList.js +++ b/src/routes/objectStorage/ObjectStoreList.js @@ -20,13 +20,13 @@ function list({ } const storeStateColorMap = { - Unknown: { color: '#F15354', bg: 'rgba(241,83,84,.05)' }, - Starting: { color: '#F1C40F', bg: 'rgba(241,196,15,.05)' }, - Running: { color: '#27AE5F', bg: 'rgba(39,174,95,.05)' }, - Stopping: { color: '#DEE1E3', bg: 'rgba(241,241,241,.05)' }, - Stopped: { color: '#959CA6', bg: 'rgba(241,241,241,.05)' }, - Terminating: { color: '#DEE1E3', bg: 'rgba(222,225,227,.05)' }, - Error: { color: '#F15354', bg: 'rgba(241,83,84,.1)' }, + unknown: { color: '#F15354', bg: 'rgba(241,83,84,.05)' }, + starting: { color: '#F1C40F', bg: 'rgba(241,196,15,.05)' }, + running: { color: '#27AE5F', bg: 'rgba(39,174,95,.05)' }, + stopping: { color: '#DEE1E3', bg: 'rgba(241,241,241,.05)' }, + stopped: { color: '#959CA6', bg: 'rgba(241,241,241,.05)' }, + terminating: { color: '#DEE1E3', bg: 'rgba(222,225,227,.05)' }, + error: { color: '#F15354', bg: 'rgba(241,83,84,.1)' }, } const columns = [ @@ -40,7 +40,7 @@ function list({ const colormap = storeStateColorMap[record.state] || { color: '', bg: '' } return ( -
+
{record.state}
From 25b916328f4ec74ee3b920ab38d4c57c2fcf68e9 Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Wed, 13 Sep 2023 12:44:16 +0200 Subject: [PATCH 28/45] Disable `Delete` bulk action button if not all selected rows can be deleted Signed-off-by: Volker Theile --- src/routes/objectStorage/ObjectStoreBulkActions.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/routes/objectStorage/ObjectStoreBulkActions.js b/src/routes/objectStorage/ObjectStoreBulkActions.js index 0d5ae81a9..0a3717b40 100644 --- a/src/routes/objectStorage/ObjectStoreBulkActions.js +++ b/src/routes/objectStorage/ObjectStoreBulkActions.js @@ -1,6 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' import { Button, Modal, Alert } from 'antd' +import { isObjectStoreDeletable } from './helper' const confirm = Modal.confirm @@ -25,7 +26,7 @@ function bulkActions({ selectedRows, deleteObjectStore }) { } const allActions = [ - { key: 'delete', name: 'Delete', disabled() { return selectedRows.length === 0 } }, + { key: 'delete', name: 'Delete', disabled: () => selectedRows.length === 0 || !selectedRows.every(isObjectStoreDeletable) }, ] return ( From 504803c195f4df37f808fc40141acf7c8999b22b Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Wed, 13 Sep 2023 12:50:50 +0200 Subject: [PATCH 29/45] Fix operation column to the right side Signed-off-by: Volker Theile --- src/routes/objectStorage/ObjectStoreList.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/routes/objectStorage/ObjectStoreList.js b/src/routes/objectStorage/ObjectStoreList.js index 49384c180..3978b9cc2 100644 --- a/src/routes/objectStorage/ObjectStoreList.js +++ b/src/routes/objectStorage/ObjectStoreList.js @@ -51,6 +51,7 @@ function list({ title: 'Name', dataIndex: 'name', key: 'name', + width: 200, render: (text, record) => { return (
{record.name}
@@ -61,6 +62,7 @@ function list({ title: 'Endpoints', dataIndex: 'endpoints', key: 'endpoints', + width: 500, render: (text, record) => { return (
{record.endpoints}
@@ -70,7 +72,8 @@ function list({ { title: 'Operation', key: 'operation', - width: 120, + width: 110, + fixed: 'right', render: (text, record) => { return ( From fdbbadb8a2485d71864da41b9744b2ed090b87d0 Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Wed, 13 Sep 2023 13:00:43 +0200 Subject: [PATCH 30/45] Add divider to menu Signed-off-by: Volker Theile --- src/components/DropOption/DropOption.js | 3 +++ src/routes/objectStorage/ObjectStoreActions.js | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/DropOption/DropOption.js b/src/components/DropOption/DropOption.js index 52ae59459..23467e963 100755 --- a/src/components/DropOption/DropOption.js +++ b/src/components/DropOption/DropOption.js @@ -4,6 +4,9 @@ import { Dropdown, Button, Icon, Menu, Tooltip } from 'antd' const DropOption = ({ onMenuClick, menuOptions = [], buttonStyle, dropdownProps, tooltipProps }) => { const menu = menuOptions.map(item => { + if (item.type === 'divider') { + return () + } const tooltip = item.tooltip !== undefined ? item.tooltip : '' return ( diff --git a/src/routes/objectStorage/ObjectStoreActions.js b/src/routes/objectStorage/ObjectStoreActions.js index f4242903b..c1a127c69 100644 --- a/src/routes/objectStorage/ObjectStoreActions.js +++ b/src/routes/objectStorage/ObjectStoreActions.js @@ -34,8 +34,9 @@ function actions({ selected, deleteObjectStore, editObjectStore, administrateObj const availableActions = [ { key: 'edit', name: 'Edit', disabled: !isObjectStoreEditable(selected) }, - { key: 'administrate', name: 'Administrate', disabled: !isObjectStoreAdministrable(selected) }, { key: 'delete', name: 'Delete', disabled: !isObjectStoreDeletable(selected) }, + { type: 'divider' }, + { key: 'administrate', name: 'Administrate', disabled: !isObjectStoreAdministrable(selected) }, ] return ( From 1e7b1c7c93f2538b5c0a8a379ea0035dce316cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Fri, 20 Oct 2023 17:24:30 +0200 Subject: [PATCH 31/45] object store: serve longhorn UI from /longhorn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Serve Longhorn UI from /longhorn sub path. This avoids conflicts with s3gw instances and allows redirecting the browser to the S3 management UI Signed-off-by: Moritz Röhrich --- Dockerfile | 12 ++++++++---- src/routes/objectStorage/index.js | 4 ++-- webpack.config.production.js | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4766fc2b0..098bf7a2c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,8 +13,9 @@ RUN npm run build FROM registry.suse.com/bci/bci-base:15.4 RUN zypper -n ref && \ - zypper -n install curl libxml2 bash gettext shadow nginx && \ - rm -f /bin/sh && ln -s /bin/bash /bin/sh + zypper -n install curl libxml2 bash gettext shadow nginx + +SHELL [ "/bin/bash", "-o", "pipefail", "-c" ] RUN mkdir -p web/dist WORKDIR /web @@ -26,9 +27,12 @@ EXPOSE 8000 ENV LONGHORN_MANAGER_IP http://localhost:9500 ENV LONGHORN_UI_PORT 8000 -RUN mkdir -p /var/config/ && touch /var/run/nginx.pid && chown -R 499 /var/config /var/run/nginx.pid +RUN mkdir -p /var/config/nginx/ \ + && cp -r /etc/nginx/* /var/config/nginx/ \ + && touch /var/run/nginx.pid \ + && chown -R 499 /var/config /var/run/nginx.pid # Use the uid of the default user (nginx) from the installed nginx package USER 499 -CMD ["/bin/bash", "-c", "mkdir -p /var/config/nginx/ && cp -r /etc/nginx/* /var/config/nginx/; envsubst '${LONGHORN_MANAGER_IP},${LONGHORN_UI_PORT}' < /etc/nginx/nginx.conf.template > /var/config/nginx/nginx.conf && nginx -c /var/config/nginx/nginx.conf -g 'daemon off;'"] +CMD ["/bin/bash", "-c", "envsubst '${LONGHORN_MANAGER_IP},${LONGHORN_UI_PORT}' < /etc/nginx/nginx.conf.template > /var/config/nginx/nginx.conf && nginx -c /var/config/nginx/nginx.conf -g 'daemon off;'"] diff --git a/src/routes/objectStorage/index.js b/src/routes/objectStorage/index.js index ca71727b3..7cfa4290b 100644 --- a/src/routes/objectStorage/index.js +++ b/src/routes/objectStorage/index.js @@ -152,8 +152,8 @@ class ObjectStore extends React.Component { }) }, administrateObjectStore: (record) => { - if (record.endpoints?.length) { - window.open(record.endpoints[0], '_blank', 'noreferrer') + if (record.name?.length) { + window.open(record.name, '_blank', 'noreferrer') } }, deleteObjectStore: (record) => { diff --git a/webpack.config.production.js b/webpack.config.production.js index 0bdbbde2f..a348d8636 100755 --- a/webpack.config.production.js +++ b/webpack.config.production.js @@ -23,7 +23,7 @@ module.exports = { output: { filename: "[name].[chunkhash:8].js", path: path.resolve(__dirname, "dist"), - publicPath: "./", + publicPath: "/longhorn/", chunkFilename: "[name].[chunkhash:8].async.js" }, resolve: { From c5e1281c0a6e5acf9bde824fa4013087b285a485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Thu, 26 Oct 2023 13:40:51 +0200 Subject: [PATCH 32/45] object store: fix model functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix model function `get` --> `put` - Fix divider in action menu not having a key and thus throwing a warning Signed-off-by: Moritz Röhrich --- src/components/DropOption/DropOption.js | 2 +- src/models/objectStore.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/DropOption/DropOption.js b/src/components/DropOption/DropOption.js index 23467e963..577f6969d 100755 --- a/src/components/DropOption/DropOption.js +++ b/src/components/DropOption/DropOption.js @@ -5,7 +5,7 @@ import { Dropdown, Button, Icon, Menu, Tooltip } from 'antd' const DropOption = ({ onMenuClick, menuOptions = [], buttonStyle, dropdownProps, tooltipProps }) => { const menu = menuOptions.map(item => { if (item.type === 'divider') { - return () + return () } const tooltip = item.tooltip !== undefined ? item.tooltip : '' return ( diff --git a/src/models/objectStore.js b/src/models/objectStore.js index 369cd94c7..a582a78ca 100644 --- a/src/models/objectStore.js +++ b/src/models/objectStore.js @@ -29,9 +29,9 @@ export default { const data = yield call(listObjectStores, payload) yield put({ type: 'listObjectStores', payload: { ...data } }) }, - *get({ payload }, { call, get }) { + *get({ payload }, { call, put }) { const data = yield call(getObjectStore, payload) - yield get({ type: 'getObjectStore', payload: { ...data } }) + yield put({ type: 'getObjectStore', payload: { ...data } }) }, *create({ payload, callback }, { call, put }) { yield call(createObjectStore, payload) From 0f9ee0ce74b7b72136d7a21fe9a8c82929ca7500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Tue, 31 Oct 2023 17:26:33 +0100 Subject: [PATCH 33/45] object store: endpoint creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add endpoint creation to the object store creation dialogue Signed-off-by: Moritz Röhrich Signed-off-by: Volker Theile --- src/components/EndpointInput/EndpointInput.js | 43 ++++++++++++++ src/components/index.js | 2 + src/index.js | 2 + src/models/secret.js | 57 +++++++++++++++++++ src/routes/objectStorage/CreateObjectStore.js | 21 ++++++- src/routes/objectStorage/EditObjectStore.js | 2 +- src/routes/objectStorage/index.js | 6 +- src/services/secret.js | 8 +++ src/utils/dataDependency.js | 9 ++- webpack.config.production.js | 2 +- 10 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 src/components/EndpointInput/EndpointInput.js create mode 100644 src/models/secret.js create mode 100644 src/services/secret.js diff --git a/src/components/EndpointInput/EndpointInput.js b/src/components/EndpointInput/EndpointInput.js new file mode 100644 index 000000000..b2b6a20b2 --- /dev/null +++ b/src/components/EndpointInput/EndpointInput.js @@ -0,0 +1,43 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Form, Select, Input } from 'antd' + +const FormItem = Form.Item +const Option = Select.Option + +class EndpointInput extends React.Component { + state = { + domainName: 'example.com', + tlsSecret: '', + } + + render() { + const { tlsSecrets } = this.props + + return ( +
+ + + + + + + +
+ ) + } +} + +EndpointInput.propTypes = { + state: PropTypes.object, + tlsSecrets: PropTypes.array, + getFieldDecorator: PropTypes.func, + getFieldsValue: PropTypes.func, + setFieldsValue: PropTypes.func, +} + +export default EndpointInput diff --git a/src/components/index.js b/src/components/index.js index 47ca4fb4b..d367b097f 100755 --- a/src/components/index.js +++ b/src/components/index.js @@ -17,6 +17,7 @@ import BackupLabelInputForRecurring from './BackupLabelInputForRecurring/BackupL import ExpansionErrorDetail from './ExpansionErrorDetail/ExpansionErrorDetail' import AutoComplete from './AutoComplete/AutoComplete' import SizeInput from './SizeInput/SizeInput' +import EndpointInput from './EndpointInput/EndpointInput' export { DropOption, @@ -38,4 +39,5 @@ export { ExpansionErrorDetail, AutoComplete, SizeInput, + EndpointInput, } diff --git a/src/index.js b/src/index.js index 0ed8f8901..a285a20e0 100755 --- a/src/index.js +++ b/src/index.js @@ -13,6 +13,7 @@ import instanceManager from './models/instanceManager' import orphanedData from './models/orphanedData' import systemBackups from './models/systemBackups' import objectStore from './models/objectStore' +import secret from './models/secret' // import assets import './assets/iconfont/iconfont.eot' @@ -42,6 +43,7 @@ app.model(instanceManager) app.model(orphanedData) app.model(systemBackups) app.model(objectStore) +app.model(secret) // 3. Router app.router(routerConfig) diff --git a/src/models/secret.js b/src/models/secret.js new file mode 100644 index 000000000..44ce43c80 --- /dev/null +++ b/src/models/secret.js @@ -0,0 +1,57 @@ +import { listSecrets } from '../services/secret' +import { wsChanges } from '../utils/websocket' +import queryString from 'query-string' +import { enableQueryData } from '../utils/dataDependency' + +export default { + namespace: 'secret', + state: { + ws: null, + data: [], + selected: {}, + resourceType: 'secretRef', + socketStatus: 'closed', + }, + subscriptions: { + setup({ dispatch, history }) { + history.listen(location => { + if (enableQueryData(location.pathname, 'secret')) { + dispatch({ + type: 'query', + payload: location.pathname.startsWith('/secret') ? queryString.parse(location.search) : {}, + }) + } + }) + }, + }, + effects: { + *query({ payload }, { call, put }) { + const data = yield call(listSecrets, payload) + console.log(data) + yield put({ type: 'listSecrets', payload: { ...data } }) + }, + *startWS({ payload }, { select }) { + let ws = yield select(state => state.secret.ws) + if (ws) { + ws.open() + } else { + wsChanges(payload.dispatch, payload.type, '1s', payload.ns) + } + }, + // eslint-disable-next-line no-unused-vars + *stopWS({ payload }, { select }) { + let ws = yield select(state => state.secret.ws) + if (ws) { + ws.close(1000) + } + }, + }, + reducers: { + listSecrets(state, action) { + return { + ...state, + ...action.payload, + } + }, + }, +} diff --git a/src/routes/objectStorage/CreateObjectStore.js b/src/routes/objectStorage/CreateObjectStore.js index 76848d83b..03a4d4cb4 100644 --- a/src/routes/objectStorage/CreateObjectStore.js +++ b/src/routes/objectStorage/CreateObjectStore.js @@ -1,7 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' import { Form, Input, InputNumber, Collapse, Select, Spin, Checkbox } from 'antd' -import { ModalBlur, SizeInput } from '../../components' +import { ModalBlur, SizeInput, EndpointInput } from '../../components' const FormItem = Form.Item const Panel = Collapse.Panel @@ -70,6 +70,14 @@ const modal = ({ setFieldsValue, } + const endpointInputProps = { + state: item, + tlsSecrets: item.tlsSecrets, + getFieldDecorator, + getFieldsValue, + setFieldsValue, + } + return (
@@ -131,7 +139,14 @@ const modal = ({ - + + + + + + + + {getFieldDecorator('numberOfReplicas', { initialValue: item.numberOfReplicas, @@ -220,7 +235,7 @@ const modal = ({ - + {getFieldDecorator('backendStoreDriver', { initialValue: 'v1', diff --git a/src/routes/objectStorage/EditObjectStore.js b/src/routes/objectStorage/EditObjectStore.js index 6820be4e7..908c4d37d 100644 --- a/src/routes/objectStorage/EditObjectStore.js +++ b/src/routes/objectStorage/EditObjectStore.js @@ -63,7 +63,7 @@ const modal = ({ {getFieldDecorator('name', { initialValue: selected.name, - })()} + })()} diff --git a/src/routes/objectStorage/index.js b/src/routes/objectStorage/index.js index 7cfa4290b..ca386c55d 100644 --- a/src/routes/objectStorage/index.js +++ b/src/routes/objectStorage/index.js @@ -66,6 +66,8 @@ class ObjectStore extends React.Component { const defaultRevisionCounterValue = defaultRevisionCounterSetting?.value === 'true' const enableSPDKDataEngineValue = enableSPDKDataEngineSetting?.value === 'true' + const secret = this.props.secret.data + let objectStores = data.filter((item) => { if (field === 'name') { return item[field] && item[field].indexOf(value.trim()) > -1 @@ -88,6 +90,7 @@ class ObjectStore extends React.Component { defaultDataLocalityValue, defaultRevisionCounterValue, enableSPDKDataEngineValue, + tlsSecrets: secret, }, visible: this.state.createModalVisible, tagsLoading: false, @@ -227,8 +230,9 @@ ObjectStore.propTypes = { location: PropTypes.object, dispatch: PropTypes.func, setting: PropTypes.object, + secret: PropTypes.object, } export default connect( - ({ objectstorage, loading, setting }) => ({ objectstorage, loading: loading.models.objectStore, setting }) + ({ objectstorage, loading, setting, secret }) => ({ objectstorage, loading: loading.models.objectStore, setting, secret }) )(ObjectStore) diff --git a/src/services/secret.js b/src/services/secret.js new file mode 100644 index 000000000..1757047f8 --- /dev/null +++ b/src/services/secret.js @@ -0,0 +1,8 @@ +import { request } from '../utils' + +export async function listSecrets() { + return request({ + url: '/v1/secrets', + method: 'get', + }) +} diff --git a/src/utils/dataDependency.js b/src/utils/dataDependency.js index 516d48073..ff707adb4 100644 --- a/src/utils/dataDependency.js +++ b/src/utils/dataDependency.js @@ -104,6 +104,10 @@ const dependency = { ns: 'objectstorage', key: 'objectstores', }, + { + ns: 'secret', + key: 'secrets', + }, ], }, instanceManager: { @@ -161,6 +165,9 @@ const allWs = [{ }, { ns: 'objectstorage', key: 'objectstores', +}, { + ns: 'secret', + key: 'secrets', }] const httpDataDependency = { @@ -175,7 +182,7 @@ const httpDataDependency = { '/instanceManager': ['volume', 'instanceManager'], '/orphanedData': ['orphanedData'], '/systemBackups': ['systemBackups'], - '/objectstorage': ['objectstore'], + '/objectstorage': ['objectstore', 'secret'], } export function getDataDependency(pathName) { diff --git a/webpack.config.production.js b/webpack.config.production.js index a348d8636..fb08fdc89 100755 --- a/webpack.config.production.js +++ b/webpack.config.production.js @@ -23,7 +23,7 @@ module.exports = { output: { filename: "[name].[chunkhash:8].js", path: path.resolve(__dirname, "dist"), - publicPath: "/longhorn/", + publicPath: "/", chunkFilename: "[name].[chunkhash:8].async.js" }, resolve: { From c03f4b78db7c6ed2df3590d281328f39f547d00e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Fri, 20 Oct 2023 17:24:30 +0200 Subject: [PATCH 34/45] object store: serve longhorn UI from /longhorn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Serve Longhorn UI from /longhorn sub path. This avoids conflicts with s3gw instances and allows redirecting the browser to the S3 management UI Signed-off-by: Moritz Röhrich --- nginx.conf.template | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/nginx.conf.template b/nginx.conf.template index 4a6acfa51..cb45f62b1 100644 --- a/nginx.conf.template +++ b/nginx.conf.template @@ -34,16 +34,15 @@ http { client_max_body_size 0; } + location ~ ^/.(images|javascript|js|css|flash|media|static)/ { + root /web/dist; + } + location / { root /web/dist; index index.html; add_header Cache-Control "max-age=0"; try_files $uri $uri/ /index.html =404; } - - location ~ ^/.(images|javascript|js|css|flash|media|static)/ { - root /web/dist; - } - } } From c76687706e48c19b0aebbb293e8b4f0d48819077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Thu, 2 Nov 2023 12:46:33 +0100 Subject: [PATCH 35/45] object store: make endpoint input work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the endpoint input component work correctly by letting it propagate the user input to the parent component Signed-off-by: Moritz Röhrich --- src/components/EndpointInput/EndpointInput.js | 24 +++++++++++++------ src/models/secret.js | 1 - src/routes/objectStorage/CreateObjectStore.js | 6 +++++ 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/components/EndpointInput/EndpointInput.js b/src/components/EndpointInput/EndpointInput.js index b2b6a20b2..d7cb47ba8 100644 --- a/src/components/EndpointInput/EndpointInput.js +++ b/src/components/EndpointInput/EndpointInput.js @@ -12,20 +12,30 @@ class EndpointInput extends React.Component { } render() { + const { getFieldDecorator, getFieldsValue, setFieldsValue } = this.props const { tlsSecrets } = this.props + function secretSelect(value) { + setFieldsValue({ + ...getFieldsValue(), + tlsSecret: value, + }) + } + return (
- + {getFieldDecorator('domainName', {})()} - - + + {getFieldDecorator('tlsSecret', {})( + + )}
) diff --git a/src/models/secret.js b/src/models/secret.js index 44ce43c80..85a8ea6fb 100644 --- a/src/models/secret.js +++ b/src/models/secret.js @@ -27,7 +27,6 @@ export default { effects: { *query({ payload }, { call, put }) { const data = yield call(listSecrets, payload) - console.log(data) yield put({ type: 'listSecrets', payload: { ...data } }) }, *startWS({ payload }, { select }) { diff --git a/src/routes/objectStorage/CreateObjectStore.js b/src/routes/objectStorage/CreateObjectStore.js index 03a4d4cb4..9a38d5867 100644 --- a/src/routes/objectStorage/CreateObjectStore.js +++ b/src/routes/objectStorage/CreateObjectStore.js @@ -46,6 +46,12 @@ const modal = ({ ...getFieldsValue(), size: `${getFieldsValue().size}${getFieldsValue().unit}`, staleReplicaTimeout: 2880, + endpoints: [ + { + domainName: `${getFieldsValue().domainName}`, + secretName: `${getFieldsValue().tlsSecret}`, + }, + ], } if (data.unit) { delete data.unit } From c676c4e452998bce042f7feb14f428ec3dd89cfa Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Thu, 2 Nov 2023 14:11:38 +0100 Subject: [PATCH 36/45] Various UI improvements - Add sorter - Init pagination correctly - Cleanup store handling in objectStorage Signed-off-by: Volker Theile --- src/models/objectStore.js | 7 +- .../objectStorage/ObjectStoreActions.js | 5 ++ src/routes/objectStorage/ObjectStoreList.js | 16 ++++- src/routes/objectStorage/index.js | 70 ++++++++++++++++--- 4 files changed, 88 insertions(+), 10 deletions(-) diff --git a/src/models/objectStore.js b/src/models/objectStore.js index a582a78ca..dcc932b39 100644 --- a/src/models/objectStore.js +++ b/src/models/objectStore.js @@ -2,15 +2,16 @@ import { listObjectStores, getObjectStore, createObjectStore, deleteObjectStore import { wsChanges, updateState } from '../utils/websocket' import queryString from 'query-string' import { enableQueryData } from '../utils/dataDependency' +import { getSorter, saveSorter } from '../utils/store' export default { namespace: 'objectstorage', state: { ws: null, data: [], - selected: {}, resourceType: 'objectstore', socketStatus: 'closed', + sorter: getSorter('objectstoreList.sorter'), }, subscriptions: { setup({ dispatch, history }) { @@ -82,5 +83,9 @@ export default { updateWs(state, action) { return { ...state, ws: action.payload } }, + updateSorter(state, action) { + saveSorter('objectstoreList.sorter', action.payload) + return { ...state, sorter: action.payload } + }, }, } diff --git a/src/routes/objectStorage/ObjectStoreActions.js b/src/routes/objectStorage/ObjectStoreActions.js index c1a127c69..352761d8e 100644 --- a/src/routes/objectStorage/ObjectStoreActions.js +++ b/src/routes/objectStorage/ObjectStoreActions.js @@ -8,6 +8,11 @@ const confirm = Modal.confirm function actions({ selected, deleteObjectStore, editObjectStore, administrateObjectStore }) { const handleMenuClick = (event, record) => { + // Stop event propagation, otherwise the click will be processed by + // other UI components, e.g. the table row below the clicked menu, + // which is not wanted in that case. + event.domEvent.stopPropagation() + switch (event.key) { case 'delete': confirm({ diff --git a/src/routes/objectStorage/ObjectStoreList.js b/src/routes/objectStorage/ObjectStoreList.js index 3978b9cc2..d0b3d44e0 100644 --- a/src/routes/objectStorage/ObjectStoreList.js +++ b/src/routes/objectStorage/ObjectStoreList.js @@ -3,6 +3,8 @@ import PropTypes from 'prop-types' import { Table, Tooltip } from 'antd' import { pagination } from '../../utils/page' import ObjectStoreActions from './ObjectStoreActions' +import { sortTable } from '../../utils/sort' +import { setSortOrder } from '../../utils/store' function list({ dataSource, @@ -12,6 +14,9 @@ function list({ editObjectStore, administrateObjectStore, deleteObjectStore, + onSorterChange, + sorter, + onRowClick, }) { const actionsProps = { editObjectStore, @@ -35,6 +40,7 @@ function list({ dataIndex: 'state', key: 'state', width: 160, + sorter: (a, b) => sortTable(a, b, 'state'), render: (text, record) => { const tooltip = `Object Store ${record.name} is ${record.state}` const colormap = storeStateColorMap[record.state] || { color: '', bg: '' } @@ -52,6 +58,7 @@ function list({ dataIndex: 'name', key: 'name', width: 200, + sorter: (a, b) => sortTable(a, b, 'name'), render: (text, record) => { return (
{record.name}
@@ -82,17 +89,21 @@ function list({ }, ] + setSortOrder(columns, sorter) + return (
onSorterChange(s)} rowSelection={rowSelection} dataSource={dataSource} loading={loading} + onRowClick={onRowClick} simple - pagination={pagination} + pagination={pagination('objectStoreSize')} rowKey={record => record.id} scroll={{ x: 970, y: dataSource.length > 0 ? height : 1 }} /> @@ -108,6 +119,9 @@ list.propTypes = { editObjectStore: PropTypes.func, administrateObjectStore: PropTypes.func, deleteObjectStore: PropTypes.func, + sorter: PropTypes.object, + onSorterChange: PropTypes.func, + onRowClick: PropTypes.func, } export default list diff --git a/src/routes/objectStorage/index.js b/src/routes/objectStorage/index.js index ca386c55d..b1535e8eb 100644 --- a/src/routes/objectStorage/index.js +++ b/src/routes/objectStorage/index.js @@ -22,6 +22,7 @@ class ObjectStore extends React.Component { createModalKey: Math.random(), editModalVisible: false, editModalKey: Math.random(), + commandKeyDown: false, } } @@ -37,12 +38,38 @@ class ObjectStore extends React.Component { }) this.props.dispatch({ type: 'app/changeNavbar' }) } + window.addEventListener('keydown', this.onKeyDown) + window.addEventListener('keyup', this.onKeyUp) + } + + componentWillUnmount() { + window.onresize = () => { + this.props.dispatch({ type: 'app/changeNavbar' }) + } + window.removeEventListener('keydown', this.onKeyDown) + window.removeEventListener('keyup', this.onKeyUp) + } + + onKeyUp = () => { + this.setState({ + ...this.state, + commandKeyDown: false, + }) + } + + onKeyDown = (e) => { + if ((e.keyCode === 91 || e.keyCode === 17) && !this.state.commandKeyDown) { + this.setState({ + ...this.state, + commandKeyDown: true, + }) + } } showCreateModal = () => { this.setState({ ...this.state, - selected: {}, + selectedRows: [], createModalVisible: true, createModalKey: Math.random(), }) @@ -51,7 +78,7 @@ class ObjectStore extends React.Component { render() { const me = this const { dispatch, loading, location } = this.props - const { data } = this.props.objectstorage + const { data, sorter } = this.props.objectstorage const { field, value } = queryString.parse(this.props.location.search) const settings = this.props.setting.data @@ -113,7 +140,7 @@ class ObjectStore extends React.Component { } const editModalProps = { - selected: this.state.selected, + selected: this.state.selectedRows[0], visible: this.state.editModalVisible, onCancel() { me.setState({ @@ -142,14 +169,14 @@ class ObjectStore extends React.Component { onChange(_, records) { me.setState({ ...me.state, - selectedRows: records, + selectedRows: [...records], }) }, }, editObjectStore: (record) => { - this.setState({ - ...this.state, - selected: record, + me.setState({ + ...me.state, + selectedRows: [record], editModalVisible: true, editModalKey: Math.random(), }) @@ -165,6 +192,33 @@ class ObjectStore extends React.Component { payload: record, }) }, + onSorterChange: (s) => { + dispatch({ + type: 'objectstorage/updateSorter', + payload: { field: s.field, order: s.order, columnKey: s.columnKey }, + }) + }, + sorter, + onRowClick: (record) => { + let selecteRowByClick = [record] + if (me.state.commandKeyDown) { + me.state.selectedRows.forEach((item) => { + if (selecteRowByClick.every((ele) => { + return ele.id !== item.id + })) { + selecteRowByClick.push(item) + } else { + selecteRowByClick = selecteRowByClick.filter((ele) => { + return ele.id !== item.id + }) + } + }) + } + me.setState({ + ...me.state, + selectedRows: [...selecteRowByClick], + }) + }, } const filterProps = { @@ -197,7 +251,7 @@ class ObjectStore extends React.Component { payload: record, callback: () => { me.setState({ - ...this.state, + ...me.state, selectedRows: [], }) }, From 2b94c3e24cf5c96a223d223be5ae23d9f1813d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Mon, 6 Nov 2023 16:04:47 +0100 Subject: [PATCH 37/45] object store: proxy object store UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Proxy object store UI in the nginx of the Longhorn UI. This allows for port-forwarding of the Longhorn UI to also access the administrative interface of the s3gw UI instances Signed-off-by: Moritz Röhrich --- Dockerfile | 8 +++++--- entrypoint.sh | 13 +++++++++++++ nginx.conf.template | 11 +++++++++++ src/routes/objectStorage/index.js | 2 +- 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100755 entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 098bf7a2c..85275f75b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN npm run build FROM registry.suse.com/bci/bci-base:15.4 RUN zypper -n ref && \ - zypper -n install curl libxml2 bash gettext shadow nginx + zypper -n install curl libxml2 bash gettext shadow nginx awk SHELL [ "/bin/bash", "-o", "pipefail", "-c" ] @@ -21,11 +21,13 @@ RUN mkdir -p web/dist WORKDIR /web COPY --from=builder /web/dist /web/dist -COPY --from=builder /web/nginx.conf.template /etc/nginx/nginx.conf.template +COPY nginx.conf.template /etc/nginx/nginx.conf.template +COPY entrypoint.sh /entrypoint.sh EXPOSE 8000 ENV LONGHORN_MANAGER_IP http://localhost:9500 ENV LONGHORN_UI_PORT 8000 +ENV LONGHORN_NAMESPACE longhorn-system RUN mkdir -p /var/config/nginx/ \ && cp -r /etc/nginx/* /var/config/nginx/ \ @@ -35,4 +37,4 @@ RUN mkdir -p /var/config/nginx/ \ # Use the uid of the default user (nginx) from the installed nginx package USER 499 -CMD ["/bin/bash", "-c", "envsubst '${LONGHORN_MANAGER_IP},${LONGHORN_UI_PORT}' < /etc/nginx/nginx.conf.template > /var/config/nginx/nginx.conf && nginx -c /var/config/nginx/nginx.conf -g 'daemon off;'"] +ENTRYPOINT ["/entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 000000000..2cf07a411 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +NAMESERVER_IP="$(grep -E '^nameserver' /etc/resolv.conf | head -n 1 | awk '{print $2}')" +LONGHORN_NAMESPACE_DOMAIN="$(grep -E '^search' /etc/resolv.conf | head -n 1 | awk '{print $2}')" + +export NAMESERVER_IP +export LONGHORN_NAMESPACE_DOMAIN + +envsubst '${LONGHORN_MANAGER_IP},${LONGHORN_UI_PORT},${LONGHORN_NAMESPACE_DOMAIN},${NAMESERVER_IP}' \ + < /etc/nginx/nginx.conf.template \ + > /var/config/nginx/nginx.conf + +nginx -c /var/config/nginx/nginx.conf diff --git a/nginx.conf.template b/nginx.conf.template index cb45f62b1..ba6f938dc 100644 --- a/nginx.conf.template +++ b/nginx.conf.template @@ -1,5 +1,11 @@ events { worker_connections 1024; } +daemon off; + +error_log /dev/stdout info; + http { + access_log /dev/stdout; + server { gzip on; gzip_min_length 1k; @@ -38,6 +44,11 @@ http { root /web/dist; } + location ~ ^/objectstore/([^/]+) { + resolver ${NAMESERVER_IP}; + proxy_pass http://$1.${LONGHORN_NAMESPACE_DOMAIN}:8080; + } + location / { root /web/dist; index index.html; diff --git a/src/routes/objectStorage/index.js b/src/routes/objectStorage/index.js index b1535e8eb..b9acded6c 100644 --- a/src/routes/objectStorage/index.js +++ b/src/routes/objectStorage/index.js @@ -183,7 +183,7 @@ class ObjectStore extends React.Component { }, administrateObjectStore: (record) => { if (record.name?.length) { - window.open(record.name, '_blank', 'noreferrer') + window.open(`objectstore/${record.name}/`, '_blank', 'noreferrer') } }, deleteObjectStore: (record) => { From de3a8a7e84e7b2f908546ea1e24165eae29c361d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Tue, 7 Nov 2023 10:36:56 +0100 Subject: [PATCH 38/45] object store: disable request body size limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Disable request body size limit for the object store UI location in the nginx config. This allows uploading large files through the s3gw UI to the object store. Signed-off-by: Moritz Röhrich --- nginx.conf.template | 1 + 1 file changed, 1 insertion(+) diff --git a/nginx.conf.template b/nginx.conf.template index ba6f938dc..75c468c5e 100644 --- a/nginx.conf.template +++ b/nginx.conf.template @@ -46,6 +46,7 @@ http { location ~ ^/objectstore/([^/]+) { resolver ${NAMESERVER_IP}; + client_max_body_size 0; proxy_pass http://$1.${LONGHORN_NAMESPACE_DOMAIN}:8080; } From a6a514f28b8c66a2950b9a52eab4ff3d27c6142e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Wed, 8 Nov 2023 12:07:04 +0100 Subject: [PATCH 39/45] object store: fix websockets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix websocket connections in the object store page: - Name secrets, settings and objectstores in the data dependency model - Fix secrets model by adding state update functions for the websocket - Fix footer component - Ensure all resource names, types, namespaces etc. match what is exposed by the websocket API of the longhorn manager Signed-off-by: Moritz Röhrich --- src/components/Layout/Footer.js | 10 ++++++---- src/models/objectStore.js | 6 +++--- src/models/secret.js | 11 ++++++++++- src/routes/objectStorage/helper/index.js | 2 +- src/routes/objectStorage/index.js | 10 +++++----- src/utils/dataDependency.js | 19 +++++++++++++++++-- 6 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/components/Layout/Footer.js b/src/components/Layout/Footer.js index a8a1613b3..707f76d5e 100644 --- a/src/components/Layout/Footer.js +++ b/src/components/Layout/Footer.js @@ -10,7 +10,7 @@ import semver from 'semver' import BundlesModel from './BundlesModel' import StableLonghornVersions from './StableLonghornVersions' -function Footer({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectstores, backup, systemBackups, dispatch }) { +function Footer({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectstorage, secret, backup, systemBackups, dispatch }) { const { bundlesropsVisible, bundlesropsKey, stableLonghornVersionslVisible, stableLonghornVersionsKey, okText, modalButtonDisabled, progressPercentage } = app const currentVersion = config.version === '${VERSION}' ? 'dev' : config.version // eslint-disable-line no-template-curly-in-string const issueHref = 'https://github.com/longhorn/longhorn/issues/new/choose' @@ -128,7 +128,8 @@ function Footer({ app, host, volume, setting, engineimage, eventlog, backingImag {getStatusIcon(eventlog)} {getStatusIcon(backingImage)} {getStatusIcon(recurringJob)} - {getStatusIcon(objectstores)} + {getStatusIcon(objectstorage)} + {getStatusIcon(secret)} {getBackupStatusIcon(backup, 'backupVolumes')} {getBackupStatusIcon(backup, 'backups')} {getSystemBackupStatusIcon(systemBackups, 'systemBackup')} @@ -149,11 +150,12 @@ Footer.propTypes = { eventlog: PropTypes.object, backingImage: PropTypes.object, recurringJob: PropTypes.object, - objectstores: PropTypes.object, + objectstorage: PropTypes.object, + secret: PropTypes.object, app: PropTypes.object, backup: PropTypes.object, systemBackups: PropTypes.object, dispatch: PropTypes.func, } -export default connect(({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectstores, backup, systemBackups }) => ({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectstores, backup, systemBackups }))(Footer) +export default connect(({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectstorage, secret, backup, systemBackups }) => ({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, objectstorage, secret, backup, systemBackups }))(Footer) diff --git a/src/models/objectStore.js b/src/models/objectStore.js index dcc932b39..1fa72cc1c 100644 --- a/src/models/objectStore.js +++ b/src/models/objectStore.js @@ -9,17 +9,17 @@ export default { state: { ws: null, data: [], - resourceType: 'objectstore', + resourceType: 'objectStore', socketStatus: 'closed', sorter: getSorter('objectstoreList.sorter'), }, subscriptions: { setup({ dispatch, history }) { history.listen(location => { - if (enableQueryData(location.pathname, 'objectstore')) { + if (enableQueryData(location.pathname, 'objectstorage')) { dispatch({ type: 'query', - payload: location.pathname.startsWith('/objectstore') ? queryString.parse(location.search) : {}, + payload: location.pathname.startsWith('/objectstorage') ? queryString.parse(location.search) : {}, }) } }) diff --git a/src/models/secret.js b/src/models/secret.js index 85a8ea6fb..342e1ce55 100644 --- a/src/models/secret.js +++ b/src/models/secret.js @@ -1,5 +1,5 @@ import { listSecrets } from '../services/secret' -import { wsChanges } from '../utils/websocket' +import { wsChanges, updateState } from '../utils/websocket' import queryString from 'query-string' import { enableQueryData } from '../utils/dataDependency' @@ -52,5 +52,14 @@ export default { ...action.payload, } }, + updateBackground(state, action) { + return updateState(state, action) + }, + updateSocketStatus(state, action) { + return { ...state, socketStatus: action.payload } + }, + updateWs(state, action) { + return { ...state, ws: action.payload } + }, }, } diff --git a/src/routes/objectStorage/helper/index.js b/src/routes/objectStorage/helper/index.js index 1df5829e8..afeebe90b 100644 --- a/src/routes/objectStorage/helper/index.js +++ b/src/routes/objectStorage/helper/index.js @@ -5,6 +5,6 @@ export const generateRandomKey = (length = 16, allowedChars = 'abcdefghijklmnopq export const isObjectStoreEditable = (record) => !['unknown', 'terminating'].includes(record.state) -export const isObjectStoreAdministrable = (record) => isObjectStoreEditable(record) && record.endpoints?.length +export const isObjectStoreAdministrable = (record) => ['running'].includes(record.state) export const isObjectStoreDeletable = (record) => !['terminating'].includes(record.state) diff --git a/src/routes/objectStorage/index.js b/src/routes/objectStorage/index.js index b9acded6c..619d5a7a7 100644 --- a/src/routes/objectStorage/index.js +++ b/src/routes/objectStorage/index.js @@ -230,14 +230,14 @@ class ObjectStore extends React.Component { onSearch(filter) { const { field: filterField, value: filterValue } = filter filterField && filterValue ? dispatch(routerRedux.push({ - pathname: '/objectstores', + pathname: '/objectstorage', search: queryString.stringify({ ...queryString.parse(location.search), field: filterField, value: filterValue, }), })) : dispatch(routerRedux.push({ - pathname: '/objectstores', + pathname: '/objectstorage', search: queryString.stringify({}), })) }, @@ -280,13 +280,13 @@ class ObjectStore extends React.Component { ObjectStore.propTypes = { objectstorage: PropTypes.object, + secret: PropTypes.object, + setting: PropTypes.object, loading: PropTypes.bool, location: PropTypes.object, dispatch: PropTypes.func, - setting: PropTypes.object, - secret: PropTypes.object, } export default connect( - ({ objectstorage, loading, setting, secret }) => ({ objectstorage, loading: loading.models.objectStore, setting, secret }) + ({ objectstorage, secret, setting, loading }) => ({ objectstorage, secret, setting, loading: loading.models.objectStore }) )(ObjectStore) diff --git a/src/utils/dataDependency.js b/src/utils/dataDependency.js index ff707adb4..fdc248594 100644 --- a/src/utils/dataDependency.js +++ b/src/utils/dataDependency.js @@ -98,7 +98,7 @@ const dependency = { }], }, objectstorage: { - path: '/objectstore', + path: '/objectstorage', runWs: [ { ns: 'objectstorage', @@ -108,6 +108,19 @@ const dependency = { ns: 'secret', key: 'secrets', }, + { + ns: 'setting', + key: 'settings', + }, + ], + }, + secret: { + path: '/secret', + runWs: [ + { + ns: 'secret', + key: 'secrets', + }, ], }, instanceManager: { @@ -129,6 +142,7 @@ const dependency = { }], }, } + const allWs = [{ ns: 'volume', key: 'volumes', @@ -182,7 +196,8 @@ const httpDataDependency = { '/instanceManager': ['volume', 'instanceManager'], '/orphanedData': ['orphanedData'], '/systemBackups': ['systemBackups'], - '/objectstorage': ['objectstore', 'secret'], + '/objectstorage': ['objectstorage', 'secret', 'setting'], + '/secret': ['secret'], } export function getDataDependency(pathName) { From e3363aa67b0f424fcbe7318ea0cd38145774154d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Wed, 8 Nov 2023 17:50:52 +0100 Subject: [PATCH 40/45] object store: stop/start object stores MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add action menu entry and plumbing for stopping/restarting an object store. This is useful to allow for certain kinds of volume maintenance operations, since it stops the workload on the volume backing the object store. Signed-off-by: Moritz Röhrich --- src/models/objectStore.js | 7 +++++- .../objectStorage/ObjectStoreActions.js | 22 ++++++++++++++++++- src/routes/objectStorage/ObjectStoreList.js | 9 +++++--- src/routes/objectStorage/index.js | 21 +++++++++++++++--- src/services/objectStore.js | 8 +++++++ 5 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/models/objectStore.js b/src/models/objectStore.js index 1fa72cc1c..fca8b479f 100644 --- a/src/models/objectStore.js +++ b/src/models/objectStore.js @@ -1,4 +1,4 @@ -import { listObjectStores, getObjectStore, createObjectStore, deleteObjectStore } from '../services/objectStore' +import { listObjectStores, getObjectStore, createObjectStore, updateObjectStore, deleteObjectStore } from '../services/objectStore' import { wsChanges, updateState } from '../utils/websocket' import queryString from 'query-string' import { enableQueryData } from '../utils/dataDependency' @@ -39,6 +39,11 @@ export default { if (callback) callback() yield put({ type: 'query' }) }, + *update({ payload, callback }, { call, put }) { + yield call(updateObjectStore, payload) + if (callback) callback() + yield put({ type: 'query' }) + }, *delete({ payload, callback }, { call, put }) { yield call(deleteObjectStore, payload) if (callback) callback() diff --git a/src/routes/objectStorage/ObjectStoreActions.js b/src/routes/objectStorage/ObjectStoreActions.js index 352761d8e..5d0a39d88 100644 --- a/src/routes/objectStorage/ObjectStoreActions.js +++ b/src/routes/objectStorage/ObjectStoreActions.js @@ -6,7 +6,13 @@ import { isObjectStoreAdministrable, isObjectStoreDeletable, isObjectStoreEditab const confirm = Modal.confirm -function actions({ selected, deleteObjectStore, editObjectStore, administrateObjectStore }) { +function actions({ + selected, + editObjectStore, + stopStartObjectStore, + deleteObjectStore, + administrateObjectStore, +}) { const handleMenuClick = (event, record) => { // Stop event propagation, otherwise the click will be processed by // other UI components, e.g. the table row below the clicked menu, @@ -30,6 +36,9 @@ function actions({ selected, deleteObjectStore, editObjectStore, administrateObj case 'edit': editObjectStore(record) break + case 'stopstart': + stopStartObjectStore(record) + break case 'administrate': administrateObjectStore(record) break @@ -37,8 +46,18 @@ function actions({ selected, deleteObjectStore, editObjectStore, administrateObj } } + const stopStartName = (record) => { + switch (record.state) { + case 'stopped': + return 'Start' + default: + return 'Stop' + } + } + const availableActions = [ { key: 'edit', name: 'Edit', disabled: !isObjectStoreEditable(selected) }, + { key: 'stopstart', name: stopStartName(selected), disabled: !isObjectStoreEditable(selected) }, { key: 'delete', name: 'Delete', disabled: !isObjectStoreDeletable(selected) }, { type: 'divider' }, { key: 'administrate', name: 'Administrate', disabled: !isObjectStoreAdministrable(selected) }, @@ -54,6 +73,7 @@ function actions({ selected, deleteObjectStore, editObjectStore, administrateObj actions.propTypes = { selected: PropTypes.object, editObjectStore: PropTypes.func, + stopStartObjectStore: PropTypes.func, administrateObjectStore: PropTypes.func, deleteObjectStore: PropTypes.func, } diff --git a/src/routes/objectStorage/ObjectStoreList.js b/src/routes/objectStorage/ObjectStoreList.js index d0b3d44e0..103eee70c 100644 --- a/src/routes/objectStorage/ObjectStoreList.js +++ b/src/routes/objectStorage/ObjectStoreList.js @@ -12,16 +12,18 @@ function list({ loading, rowSelection, editObjectStore, - administrateObjectStore, + stopStartObjectStore, deleteObjectStore, + administrateObjectStore, onSorterChange, sorter, onRowClick, }) { const actionsProps = { editObjectStore, - administrateObjectStore, + stopStartObjectStore, deleteObjectStore, + administrateObjectStore, } const storeStateColorMap = { @@ -117,8 +119,9 @@ list.propTypes = { loading: PropTypes.bool, rowSelection: PropTypes.object, editObjectStore: PropTypes.func, - administrateObjectStore: PropTypes.func, deleteObjectStore: PropTypes.func, + stopStartObjectStore: PropTypes.func, + administrateObjectStore: PropTypes.func, sorter: PropTypes.object, onSorterChange: PropTypes.func, onRowClick: PropTypes.func, diff --git a/src/routes/objectStorage/index.js b/src/routes/objectStorage/index.js index 619d5a7a7..43fc8b7ab 100644 --- a/src/routes/objectStorage/index.js +++ b/src/routes/objectStorage/index.js @@ -181,10 +181,20 @@ class ObjectStore extends React.Component { editModalKey: Math.random(), }) }, - administrateObjectStore: (record) => { - if (record.name?.length) { - window.open(`objectstore/${record.name}/`, '_blank', 'noreferrer') + stopStartObjectStore: (record) => { + record.endpoints = [] // Don't update any endpoints + switch (record.state) { + case 'stopped': + record.targetState = 'running' + break + default: + record.targetState = 'stopped' + break } + dispatch({ + type: 'objectstorage/update', + payload: record, + }) }, deleteObjectStore: (record) => { dispatch({ @@ -192,6 +202,11 @@ class ObjectStore extends React.Component { payload: record, }) }, + administrateObjectStore: (record) => { + if (record.name?.length) { + window.open(`objectstore/${record.name}/`, '_blank', 'noreferrer') + } + }, onSorterChange: (s) => { dispatch({ type: 'objectstorage/updateSorter', diff --git a/src/services/objectStore.js b/src/services/objectStore.js index f15cfd6c5..1e7a0ee70 100644 --- a/src/services/objectStore.js +++ b/src/services/objectStore.js @@ -22,6 +22,14 @@ export async function createObjectStore(params) { }) } +export async function updateObjectStore(params) { + return request({ + url: `/v1/objectstores/${params.name}`, + method: 'put', + data: params, + }) +} + export async function deleteObjectStore(params) { if (params.name) { return request({ From 7d78aefc55ca01b72641710c0ac69de7274e3adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Thu, 9 Nov 2023 13:27:37 +0100 Subject: [PATCH 41/45] object store: display space usage, filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Display size and free space of the volume associated with an object store in the object store view. This makes it easy for users to view the amount of free space in an object store. Fix filters in object store view: Allow object stores to be filtered by their state and endpoints in addition to their name. This makes it easy for users to find stopped object stores if they have too many to display on a single page. It also makes it easy to find the object store associated with a domain name, which otherwise would be hard if the name of an object store has no relation to the domain name in use. Signed-off-by: Moritz Röhrich --- src/routes/objectStorage/ObjectStoreList.js | 31 ++++++++++++++++++- src/routes/objectStorage/ObjectStoreList.less | 7 +++++ src/routes/objectStorage/helper/index.js | 15 +++++++++ src/routes/objectStorage/index.js | 22 +++++++++++-- 4 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 src/routes/objectStorage/ObjectStoreList.less diff --git a/src/routes/objectStorage/ObjectStoreList.js b/src/routes/objectStorage/ObjectStoreList.js index 103eee70c..315b88b18 100644 --- a/src/routes/objectStorage/ObjectStoreList.js +++ b/src/routes/objectStorage/ObjectStoreList.js @@ -1,10 +1,12 @@ import React from 'react' import PropTypes from 'prop-types' -import { Table, Tooltip } from 'antd' +import { Progress, Table, Tooltip } from 'antd' import { pagination } from '../../utils/page' import ObjectStoreActions from './ObjectStoreActions' import { sortTable } from '../../utils/sort' import { setSortOrder } from '../../utils/store' +import styles from './ObjectStoreList.less' +import { bytesToGi, getStorageStatus } from './helper/index' function list({ dataSource, @@ -36,6 +38,10 @@ function list({ error: { color: '#F15354', bg: 'rgba(241,83,84,.1)' }, } + const computeUsage = (record) => { + return Math.round(record.occupiedSize / record.allocatedSize * 100) + } + const columns = [ { title: 'State', @@ -67,6 +73,29 @@ function list({ ) }, }, + { + title: 'Usage', + dataIndex: 'storageUsed', + key: 'usage', + width: 160, + className: styles.allocated, + sorter: (a, b) => computeUsage(a) - computeUsage(b), + render: (text, record) => { + const p = computeUsage(record) + return ( +
+
+ + 100 ? 100 : p} showInfo={false} /> + +
+
+ {bytesToGi(record.occupiedSize)} / {bytesToGi(record.allocatedSize)} Gi +
+
+ ) + }, + }, { title: 'Endpoints', dataIndex: 'endpoints', diff --git a/src/routes/objectStorage/ObjectStoreList.less b/src/routes/objectStorage/ObjectStoreList.less new file mode 100644 index 000000000..80db0ccf6 --- /dev/null +++ b/src/routes/objectStorage/ObjectStoreList.less @@ -0,0 +1,7 @@ +.allocated { + text-align: center !important; +} + +.secondLabel { + font-size: 13px; +} diff --git a/src/routes/objectStorage/helper/index.js b/src/routes/objectStorage/helper/index.js index afeebe90b..fd68b69f5 100644 --- a/src/routes/objectStorage/helper/index.js +++ b/src/routes/objectStorage/helper/index.js @@ -8,3 +8,18 @@ export const isObjectStoreEditable = (record) => !['unknown', 'terminating'].inc export const isObjectStoreAdministrable = (record) => ['running'].includes(record.state) export const isObjectStoreDeletable = (record) => !['terminating'].includes(record.state) + +export function bytesToGi(value) { + const val = Number(value) + return Math.round(val / (1024 * 1024 * 1024)) +} + +export function getStorageStatus(percent) { + if (percent > 100 || percent < 0) { + return 'exception' + } + if (percent > 95) { + return 'active' + } + return 'success' +} diff --git a/src/routes/objectStorage/index.js b/src/routes/objectStorage/index.js index 43fc8b7ab..43b76791b 100644 --- a/src/routes/objectStorage/index.js +++ b/src/routes/objectStorage/index.js @@ -79,7 +79,7 @@ class ObjectStore extends React.Component { const me = this const { dispatch, loading, location } = this.props const { data, sorter } = this.props.objectstorage - const { field, value } = queryString.parse(this.props.location.search) + const { field, value, stateValue } = queryString.parse(this.props.location.search) const settings = this.props.setting.data const defaultReplicaCountSetting = settings.find(s => s.id === 'default-replica-count') @@ -98,6 +98,10 @@ class ObjectStore extends React.Component { let objectStores = data.filter((item) => { if (field === 'name') { return item[field] && item[field].indexOf(value.trim()) > -1 + } else if (field === 'status') { + return item.state && item.state === stateValue.trim() + } else if (field === 'endpoints') { + return item.endpoints && item.endpoints.some((endpoint) => endpoint.includes(value.trim())) } return true }) @@ -239,17 +243,29 @@ class ObjectStore extends React.Component { const filterProps = { location, defaultField: 'name', + stateOption: [ + { value: 'running', name: 'Running' }, + { value: 'stopped', name: 'Stopped' }, + ], + fieldOption: [ + { value: 'status', name: 'State' }, { value: 'name', name: 'Name' }, + { value: 'endpoints', name: 'Endpoint' }, ], onSearch(filter) { - const { field: filterField, value: filterValue } = filter - filterField && filterValue ? dispatch(routerRedux.push({ + const { + field: filterField, + value: filterValue, + stateValue: filterStateValue, + } = filter + filterField && (filterValue || filterStateValue) ? dispatch(routerRedux.push({ pathname: '/objectstorage', search: queryString.stringify({ ...queryString.parse(location.search), field: filterField, value: filterValue, + stateValue: filterStateValue, }), })) : dispatch(routerRedux.push({ pathname: '/objectstorage', From 29f1b7ab62094eca762536350edbe95a188da033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Thu, 9 Nov 2023 18:57:02 +0100 Subject: [PATCH 42/45] object store: fix flickering keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix flickering access/secret keys in create object store dialogue Signed-off-by: Moritz Röhrich --- src/routes/objectStorage/index.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/routes/objectStorage/index.js b/src/routes/objectStorage/index.js index 43b76791b..3495c369c 100644 --- a/src/routes/objectStorage/index.js +++ b/src/routes/objectStorage/index.js @@ -23,6 +23,8 @@ class ObjectStore extends React.Component { editModalVisible: false, editModalKey: Math.random(), commandKeyDown: false, + initialAccessKey: generateRandomKey(), + initialSecretKey: generateRandomKey(), } } @@ -72,6 +74,8 @@ class ObjectStore extends React.Component { selectedRows: [], createModalVisible: true, createModalKey: Math.random(), + initialAccessKey: generateRandomKey(), + initialSecretKey: generateRandomKey(), }) } @@ -112,8 +116,8 @@ class ObjectStore extends React.Component { const createModalProps = { item: { - accesskey: generateRandomKey(), - secretkey: generateRandomKey(), + accesskey: this.state.initialAccessKey, + secretkey: this.state.initialSecretKey, numberOfReplicas: defaultNumberOfReplicas, diskTags: [], nodeTags: [], From 82d47cf9a498dcb492a6eb53ec43dafc764c2415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Tue, 14 Nov 2023 16:27:32 +0100 Subject: [PATCH 43/45] object store: fix volume expansion, from backup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix volume expansion: - don't allow a user to edit the size of an object store and make it smaller - propagate the size from the edit menu to the backend, so the volume can be expanded Fix from backup: - add an input field to allow populating a new object store with data from a previous backup Signed-off-by: Moritz Röhrich --- src/components/SizeInput/SizeInput.js | 16 +++++++++------ src/routes/objectStorage/CreateObjectStore.js | 7 +++++++ src/routes/objectStorage/EditObjectStore.js | 20 ++++++++++++++++++- src/routes/objectStorage/ObjectStoreList.js | 4 ++-- src/routes/objectStorage/helper/index.js | 2 +- src/routes/objectStorage/index.js | 3 +++ 6 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/components/SizeInput/SizeInput.js b/src/components/SizeInput/SizeInput.js index ac426c5a5..9cd4360c1 100644 --- a/src/components/SizeInput/SizeInput.js +++ b/src/components/SizeInput/SizeInput.js @@ -9,10 +9,12 @@ class SizeInput extends React.Component { state = { size: 1, unit: 'Gi', + mustExpand: false, } render() { const { getFieldDecorator, getFieldsValue, setFieldsValue } = this.props + const { size, unit, mustExpand } = this.props.state function unitChange(value) { const unitmap = new Map([ @@ -41,7 +43,7 @@ class SizeInput extends React.Component {
{getFieldDecorator('size', { - initialValue: this.state.size, + initialValue: size, rules: [ { required: true, @@ -56,6 +58,8 @@ class SizeInput extends React.Component { callback('The value should be between 0 and 65535') } else if (!/^\d+([.]\d{1,2})?$/.test(value)) { callback('This value should have at most two decimal places') + } else if (mustExpand && value < size) { + callback(`Size should be larger than ${size} ${unit}`) } else if (value < 10 && getFieldsValue().unit === 'Mi') { callback('The volume size must be greater than 10 Mi') } else if (value % 1 !== 0 && getFieldsValue().unit === 'Mi') { @@ -70,13 +74,13 @@ class SizeInput extends React.Component { {getFieldDecorator('unit', { - initialValue: this.state.unit, + initialValue: unit, rules: [{ required: true, message: 'Please select your unit!' }], })( - + + + , )} diff --git a/src/routes/objectStorage/CreateObjectStore.js b/src/routes/objectStorage/CreateObjectStore.js index 9a38d5867..df4832dad 100644 --- a/src/routes/objectStorage/CreateObjectStore.js +++ b/src/routes/objectStorage/CreateObjectStore.js @@ -288,6 +288,13 @@ const modal = ({ )} + + + {getFieldDecorator('fromBackup', { + initialValue: item.fromBackup, + rules: [], + })()} + diff --git a/src/routes/objectStorage/EditObjectStore.js b/src/routes/objectStorage/EditObjectStore.js index 908c4d37d..bac51f3f1 100644 --- a/src/routes/objectStorage/EditObjectStore.js +++ b/src/routes/objectStorage/EditObjectStore.js @@ -2,6 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import { Form, Input } from 'antd' import { ModalBlur, SizeInput } from '../../components' +import { bytesToGiB } from './helper' const FormItem = Form.Item @@ -36,6 +37,9 @@ const modal = ({ } if (data.unit) { delete data.unit } + // don't send data if it's not changed + if (data.image === selected.image) { delete data.image } + if (data.uiImage === selected.uiImage) { delete data.uiImage } onOk(data) }) @@ -51,7 +55,11 @@ const modal = ({ } const sizeInputProps = { - state: selected, + state: { + size: bytesToGiB(selected.allocatedSize), + unit: 'Gi', + mustExpand: true, + }, getFieldDecorator, getFieldsValue, setFieldsValue, @@ -67,6 +75,16 @@ const modal = ({ + + {getFieldDecorator('image', { + initialValue: selected.image, + })()} + + + {getFieldDecorator('uiImage', { + initialValue: selected.uiImage, + })()} + ) diff --git a/src/routes/objectStorage/ObjectStoreList.js b/src/routes/objectStorage/ObjectStoreList.js index 315b88b18..d6a76fcbb 100644 --- a/src/routes/objectStorage/ObjectStoreList.js +++ b/src/routes/objectStorage/ObjectStoreList.js @@ -6,7 +6,7 @@ import ObjectStoreActions from './ObjectStoreActions' import { sortTable } from '../../utils/sort' import { setSortOrder } from '../../utils/store' import styles from './ObjectStoreList.less' -import { bytesToGi, getStorageStatus } from './helper/index' +import { bytesToGiB, getStorageStatus } from './helper/index' function list({ dataSource, @@ -90,7 +90,7 @@ function list({
- {bytesToGi(record.occupiedSize)} / {bytesToGi(record.allocatedSize)} Gi + {bytesToGiB(record.occupiedSize)} / {bytesToGiB(record.allocatedSize)} GiB
) diff --git a/src/routes/objectStorage/helper/index.js b/src/routes/objectStorage/helper/index.js index fd68b69f5..0ed515155 100644 --- a/src/routes/objectStorage/helper/index.js +++ b/src/routes/objectStorage/helper/index.js @@ -9,7 +9,7 @@ export const isObjectStoreAdministrable = (record) => ['running'].includes(recor export const isObjectStoreDeletable = (record) => !['terminating'].includes(record.state) -export function bytesToGi(value) { +export function bytesToGiB(value) { const val = Number(value) return Math.round(val / (1024 * 1024 * 1024)) } diff --git a/src/routes/objectStorage/index.js b/src/routes/objectStorage/index.js index 3495c369c..456cc8c85 100644 --- a/src/routes/objectStorage/index.js +++ b/src/routes/objectStorage/index.js @@ -126,6 +126,9 @@ class ObjectStore extends React.Component { defaultRevisionCounterValue, enableSPDKDataEngineValue, tlsSecrets: secret, + size: 1, + unit: 'Gi', + mustExpand: false, }, visible: this.state.createModalVisible, tagsLoading: false, From 8abb39169d21cc4c7423915ebc3cd223dca7e6bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Thu, 16 Nov 2023 18:32:31 +0100 Subject: [PATCH 44/45] object store: remove unused code path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit remove superfluous getObjectStore function and code path. It is not needed as there is no detailed view of object stores and the list view contains all available information Signed-off-by: Moritz Röhrich --- src/models/objectStore.js | 6 +----- src/routes/objectStorage/EditObjectStore.js | 2 +- src/routes/objectStorage/ObjectStoreList.js | 4 ++-- src/routes/objectStorage/index.js | 6 +++++- src/services/objectStore.js | 7 ------- 5 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/models/objectStore.js b/src/models/objectStore.js index fca8b479f..779b542dc 100644 --- a/src/models/objectStore.js +++ b/src/models/objectStore.js @@ -1,4 +1,4 @@ -import { listObjectStores, getObjectStore, createObjectStore, updateObjectStore, deleteObjectStore } from '../services/objectStore' +import { listObjectStores, createObjectStore, updateObjectStore, deleteObjectStore } from '../services/objectStore' import { wsChanges, updateState } from '../utils/websocket' import queryString from 'query-string' import { enableQueryData } from '../utils/dataDependency' @@ -30,10 +30,6 @@ export default { const data = yield call(listObjectStores, payload) yield put({ type: 'listObjectStores', payload: { ...data } }) }, - *get({ payload }, { call, put }) { - const data = yield call(getObjectStore, payload) - yield put({ type: 'getObjectStore', payload: { ...data } }) - }, *create({ payload, callback }, { call, put }) { yield call(createObjectStore, payload) if (callback) callback() diff --git a/src/routes/objectStorage/EditObjectStore.js b/src/routes/objectStorage/EditObjectStore.js index bac51f3f1..32e5d3ba0 100644 --- a/src/routes/objectStorage/EditObjectStore.js +++ b/src/routes/objectStorage/EditObjectStore.js @@ -56,7 +56,7 @@ const modal = ({ const sizeInputProps = { state: { - size: bytesToGiB(selected.allocatedSize), + size: bytesToGiB(selected.size), unit: 'Gi', mustExpand: true, }, diff --git a/src/routes/objectStorage/ObjectStoreList.js b/src/routes/objectStorage/ObjectStoreList.js index d6a76fcbb..63852a099 100644 --- a/src/routes/objectStorage/ObjectStoreList.js +++ b/src/routes/objectStorage/ObjectStoreList.js @@ -39,7 +39,7 @@ function list({ } const computeUsage = (record) => { - return Math.round(record.occupiedSize / record.allocatedSize * 100) + return Math.round(record.actualSize / record.size * 100) } const columns = [ @@ -90,7 +90,7 @@ function list({
- {bytesToGiB(record.occupiedSize)} / {bytesToGiB(record.allocatedSize)} GiB + {bytesToGiB(record.actualSize)} / {bytesToGiB(record.size)} GiB
) diff --git a/src/routes/objectStorage/index.js b/src/routes/objectStorage/index.js index 456cc8c85..e51a91cf1 100644 --- a/src/routes/objectStorage/index.js +++ b/src/routes/objectStorage/index.js @@ -193,7 +193,11 @@ class ObjectStore extends React.Component { }) }, stopStartObjectStore: (record) => { - record.endpoints = [] // Don't update any endpoints + // don't update any properties besides the target state + delete record.endpoints + delete record.size + delete record.image + delete record.uiImage switch (record.state) { case 'stopped': record.targetState = 'running' diff --git a/src/services/objectStore.js b/src/services/objectStore.js index 1e7a0ee70..4ad51e6a7 100644 --- a/src/services/objectStore.js +++ b/src/services/objectStore.js @@ -7,13 +7,6 @@ export async function listObjectStores() { }) } -export async function getObjectStore(name) { - return request({ - url: `/v1/objectstores/${name}`, - method: 'get', - }) -} - export async function createObjectStore(params) { return request({ url: '/v1/objectstores', From 4ce6b89cc0fdae1b28f7b8d77b57bed34c90f6c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20R=C3=B6hrich?= Date: Mon, 27 Nov 2023 23:01:33 +0100 Subject: [PATCH 45/45] object store: fix TLS secret bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When deploying an object store, there can be situations where the json object sent to the API is incorrectly constructed with null values. To avoid this, construct valid json objects regardless of the setting for the tls secret and domain name. Signed-off-by: Moritz Röhrich --- src/routes/objectStorage/CreateObjectStore.js | 21 +++++++++++++------ src/routes/objectStorage/ObjectStoreList.js | 12 +++++++++-- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/routes/objectStorage/CreateObjectStore.js b/src/routes/objectStorage/CreateObjectStore.js index df4832dad..51dc92fb8 100644 --- a/src/routes/objectStorage/CreateObjectStore.js +++ b/src/routes/objectStorage/CreateObjectStore.js @@ -46,12 +46,21 @@ const modal = ({ ...getFieldsValue(), size: `${getFieldsValue().size}${getFieldsValue().unit}`, staleReplicaTimeout: 2880, - endpoints: [ - { - domainName: `${getFieldsValue().domainName}`, - secretName: `${getFieldsValue().tlsSecret}`, - }, - ], + endpoints: [], + } + + if (getFieldsValue().domainName) { + let endpoint = { + domainName: `${getFieldsValue().domainName}`, + } + + if (getFieldsValue().tlsSecret) { + endpoint.secretName = `${getFieldsValue().tlsSecret}` + } else { + endpoint.secretName = '' + } + + data.endpoints.push(endpoint) } if (data.unit) { delete data.unit } diff --git a/src/routes/objectStorage/ObjectStoreList.js b/src/routes/objectStorage/ObjectStoreList.js index 63852a099..af0641831 100644 --- a/src/routes/objectStorage/ObjectStoreList.js +++ b/src/routes/objectStorage/ObjectStoreList.js @@ -1,6 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' -import { Progress, Table, Tooltip } from 'antd' +import { Progress, Table, Tooltip, Tag } from 'antd' import { pagination } from '../../utils/page' import ObjectStoreActions from './ObjectStoreActions' import { sortTable } from '../../utils/sort' @@ -102,8 +102,16 @@ function list({ key: 'endpoints', width: 500, render: (text, record) => { + let tags = [] + if (record.endpoints !== null && record.endpoints.length > 0) { + for (const endpoint of record.endpoints) { + tags.push({endpoint}) + } + } return ( -
{record.endpoints}
+
+ {tags} +
) }, },