This is an automated email from the ASF dual-hosted git repository.
rohit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack-primate.git
The following commit(s) were added to refs/heads/master by this push:
new 3ab9bf1 infra: add primary storage form (#126)
3ab9bf1 is described below
commit 3ab9bf1cf3e32945c605f2d533a15c99102432ed
Author: Pearl Dsilva <[email protected]>
AuthorDate: Mon Mar 23 10:38:49 2020 +0530
infra: add primary storage form (#126)
Adds form to allow root admins to add a primary storage pool.
Signed-off-by: Abhishek Kumar <[email protected]>
Signed-off-by: Rohit Yadav <[email protected]>
Co-authored-by: Pearl Dsilva <[email protected]>
Co-authored-by: Abhishek Kumar <[email protected]>
Co-authored-by: Rohit Yadav <[email protected]>
---
src/components/view/InfoCard.vue | 4 +-
src/config/section/infra/primaryStorages.js | 7 +-
src/views/AutogenView.vue | 2 +-
src/views/infra/AddPrimaryStorage.vue | 568 ++++++++++++++++++++++++++++
4 files changed, 575 insertions(+), 6 deletions(-)
diff --git a/src/components/view/InfoCard.vue b/src/components/view/InfoCard.vue
index 13c87fd..9b92779 100644
--- a/src/components/view/InfoCard.vue
+++ b/src/components/view/InfoCard.vue
@@ -821,7 +821,7 @@ export default {
margin-bottom: 0;
font-size: 18px;
line-height: 1;
- word-wrap: break-word;
+ word-break: break-all;
text-align: left;
}
@@ -829,7 +829,7 @@ export default {
}
.resource-detail-item {
margin-bottom: 20px;
- word-break: break-word;
+ word-break: break-all;
&__details {
display: flex;
diff --git a/src/config/section/infra/primaryStorages.js
b/src/config/section/infra/primaryStorages.js
index 1c29a1f..1bdb52b 100644
--- a/src/config/section/infra/primaryStorages.js
+++ b/src/config/section/infra/primaryStorages.js
@@ -21,7 +21,7 @@ export default {
icon: 'database',
permission: ['listStoragePoolsMetrics', 'listStoragePools'],
columns: ['name', 'state', 'ipaddress', 'type', 'path', 'scope',
'disksizeusedgb', 'disksizetotalgb', 'disksizeallocatedgb',
'disksizeunallocatedgb', 'clustername', 'zonename'],
- details: ['name', 'id', 'ipaddress', 'type', 'scope', 'path', 'provider',
'hypervisor', 'overprovisionfactor', 'disksizetotal', 'disksizeallocated',
'disksizeused', 'clustername', 'podname', 'zonename', 'created'],
+ details: ['name', 'id', 'ipaddress', 'type', 'scope', 'tags', 'path',
'provider', 'hypervisor', 'overprovisionfactor', 'disksizetotal',
'disksizeallocated', 'disksizeused', 'clustername', 'podname', 'zonename',
'created'],
related: [{
name: 'volume',
title: 'Volumes',
@@ -40,7 +40,8 @@ export default {
icon: 'plus',
label: 'label.add.primary.storage',
listView: true,
- args: ['scope', 'zoneid', 'podid', 'clusterid', 'name', 'provider',
'managed', 'capacityBytes', 'capacityIops', 'url', 'tags']
+ popup: true,
+ component: () => import('@/views/infra/AddPrimaryStorage.vue')
},
{
api: 'updateStoragePool',
@@ -69,7 +70,7 @@ export default {
label: 'label.action.delete.primary.storage',
dataView: true,
args: ['forced'],
- show: (record) => { return !(record.state === 'Down' || record.state ===
'Alert' || record.state === 'Maintenance' || record.state === 'Disconnected') }
+ show: (record) => { return (record.state === 'Down' || record.state ===
'Maintenance' || record.state === 'Disconnected') }
}
]
}
diff --git a/src/views/AutogenView.vue b/src/views/AutogenView.vue
index 40b9107..318a996 100644
--- a/src/views/AutogenView.vue
+++ b/src/views/AutogenView.vue
@@ -546,7 +546,7 @@ export default {
this.showAction = true
for (const param of this.currentAction.paramFields) {
- if (param.type === 'list' && param.name === 'hosttags') {
+ if (param.type === 'list' && ['tags',
'hosttags'].includes(param.name)) {
param.type = 'string'
}
if (param.type === 'uuid' || param.type === 'list' || param.name ===
'account' || (this.currentAction.mapping && param.name in
this.currentAction.mapping)) {
diff --git a/src/views/infra/AddPrimaryStorage.vue
b/src/views/infra/AddPrimaryStorage.vue
new file mode 100644
index 0000000..974d252
--- /dev/null
+++ b/src/views/infra/AddPrimaryStorage.vue
@@ -0,0 +1,568 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+ <div class="form-layout">
+ <a-spin :spinning="loading">
+ <a-form :form="form" layout="vertical">
+ <a-form-item :label="$t('scope')">
+ <a-select v-decorator="['scope', { initialValue: 'cluster' }]"
@change="val => { this.scope = val }">
+ <a-select-option :value="'cluster'"> {{ $t('clusterid') }}
</a-select-option>
+ <a-select-option :value="'zone'"> {{ $t('zoneid') }}
</a-select-option>
+ </a-select>
+ </a-form-item>
+ <div v-if="this.scope === 'zone'">
+ <a-form-item :label="$t('hypervisor')">
+ <a-select
+ v-decorator="['hypervisor', { initialValue: hypervisors[0]}]"
+ @change="val => this.selectedHypervisor = val">
+ <a-select-option :value="hypervisor" v-for="(hypervisor, idx) in
hypervisors" :key="idx">
+ {{ hypervisor }}
+ </a-select-option>
+ </a-select>
+ </a-form-item>
+ </div>
+ <a-form-item :label="$t('zoneid')">
+ <a-select
+ v-decorator="['zone', { initialValue: this.zoneSelected, rules: [{
required: true, message: 'required'}] }]"
+ @change="val => changeZone(val)">
+ <a-select-option :value="zone.id" v-for="(zone) in zones"
:key="zone.id">
+ {{ zone.name }}
+ </a-select-option>
+ </a-select>
+ </a-form-item>
+ <div v-if="this.scope === 'cluster' || this.scope === 'host'">
+ <a-form-item :label="$t('podId')">
+ <a-select
+ v-decorator="['pod', { initialValue: this.podSelected, rules: [{
required: true, message: 'required'}] }]"
+ @change="val => changePod(val)">
+ <a-select-option :value="pod.id" v-for="(pod) in pods"
:key="pod.id">
+ {{ pod.name }}
+ </a-select-option>
+ </a-select>
+ </a-form-item>
+ <a-form-item :label="$t('clusterId')">
+ <a-select
+ v-decorator="['cluster', { initialValue: this.clusterSelected,
rules: [{ required: true, message: 'required'}] }]"
+ @change="val => fetchHypervisor(val)">
+ <a-select-option :value="cluster.id" v-for="cluster in clusters"
:key="cluster.id">
+ {{ cluster.name }}
+ </a-select-option>
+ </a-select>
+ </a-form-item>
+ </div>
+ <div v-if="this.scope === 'host'">
+ <a-form-item :label="$t('hostId')">
+ <a-select
+ v-decorator="['host', { initialValue: this.hostSelected, rules:
[{ required: true, message: 'required'}] }]"
+ @change="val => this.hostSelected = val">
+ <a-select-option :value="host.id" v-for="host in hosts"
:key="host.id">
+ {{ host.name }}
+ </a-select-option>
+ </a-select>
+ </a-form-item>
+ </div>
+ <a-form-item :label="$t('name')">
+ <a-input v-decorator="['name', { rules: [{ required: true, message:
'required' }] }]"/>
+ </a-form-item>
+ <a-form-item :label="$t('protocol')">
+ <a-select
+ v-decorator="['protocol', { initialValue: this.protocols[0],
rules: [{ required: true, message: 'required'}] }]"
+ @change="val => this.protocolSelected = val">
+ <a-select-option :value="protocol" v-for="(protocol,idx) in
protocols" :key="idx">
+ {{ protocol }}
+ </a-select-option>
+ </a-select>
+ </a-form-item>
+ <div
+ v-if="protocolSelected === 'nfs' || protocolSelected === 'SMB' ||
protocolSelected === 'iscsi' || protocolSelected === 'vmfs'|| protocolSelected
=== 'Gluster'">
+ <a-form-item :label="$t('server')">
+ <a-input v-decorator="['server', { rules: [{ required: true,
message: 'required' }] }]" />
+ </a-form-item>
+ </div>
+ <div v-if="protocolSelected === 'nfs' || protocolSelected === 'SMB' ||
protocolSelected === 'ocfs2' || protocolSelected === 'preSetup'||
protocolSelected === 'SharedMountPoint'">
+ <a-form-item :label="$t('path')">
+ <a-input v-decorator="['path', { rules: [{ required: true,
message: 'required' }] }]" />
+ </a-form-item>
+ </div>
+ <div v-if="protocolSelected === 'SMB'">
+ <a-form-item :label="$t('smbUsername')">
+ <a-input v-decorator="['smbUsername', { rules: [{ required: true,
message: 'required' }] }]"/>
+ </a-form-item>
+ <a-form-item :label="$t('smbPassword')">
+ <a-input-password v-decorator="['smbPassword', { rules: [{
required: true, message: 'required' }] }]"/>
+ </a-form-item>
+ <a-form-item :label="$t('smbDomain')">
+ <a-input v-decorator="['smbDomain', { rules: [{ required: true,
message: 'required' }] }]"/>
+ </a-form-item>
+ </div>
+ <div v-if="protocolSelected === 'iscsi'">
+ <a-form-item :label="$t('iqn')">
+ <a-input v-decorator="['iqn', { rules: [{ required: true, message:
'required' }] }]"/>
+ </a-form-item>
+ <a-form-item :label="$t('lun')">
+ <a-input v-decorator="['lun', { rules: [{ required: true, message:
'required' }] }]"/>
+ </a-form-item>
+ </div>
+ <div v-if="protocolSelected === 'vmfs'">
+ <a-form-item :label="$t('vCenterDataCenter')">
+ <a-input v-decorator="['vCenterDataCenter', { rules: [{ required:
true, message: 'required' }] }]"/>
+ </a-form-item>
+ <a-form-item :label="$t('vCenterDataStore')">
+ <a-input v-decorator="['vCenterDataStore', { rules: [{ required:
true, message: 'required' }] }]"/>
+ </a-form-item>
+ </div>
+ <a-form-item :label="$t('providername')">
+ <a-select
+ v-decorator="['provider', { initialValue: providerSelected, rules:
[{ required: true, message: 'required'}] }]"
+ @change="val => this.providerSelected = val">
+ <a-select-option :value="provider" v-for="(provider,idx) in
providers" :key="idx">
+ {{ provider }}
+ </a-select-option>
+ </a-select>
+ </a-form-item>
+ <div v-if="this.providerSelected !== 'DefaultPrimary'">
+ <a-form-item :label="$t('isManaged')">
+ <a-checkbox-group v-decorator="['managed']" >
+ <a-checkbox value="ismanaged"></a-checkbox>
+ </a-checkbox-group>
+ </a-form-item>
+ <a-form-item :label="$t('capacityBytes')">
+ <a-input v-decorator="['capacityBytes']" />
+ </a-form-item>
+ <a-form-item :label="$t('capacityIops')">
+ <a-input v-decorator="['capacityIops']" />
+ </a-form-item>
+ <a-form-item :label="$t('url')">
+ <a-input v-decorator="['url']" />
+ </a-form-item>
+ </div>
+ <div v-if="this.protocolSelected === 'RBD'">
+ <a-form-item :label="$t('radosmonitor')">
+ <a-input v-decorator="['radosmonitor']" />
+ </a-form-item><a-form-item :label="$t('radospool')">
+ <a-input v-decorator="['radospool']" />
+ </a-form-item>
+ <a-form-item :label="$t('radosuser')">
+ <a-input v-decorator="['radosuser']" />
+ </a-form-item>
+ <a-form-item :label="$t('radossecret')">
+ <a-input v-decorator="['radossecret']" />
+ </a-form-item>
+ </div>
+ <div v-if="protocolSelected === 'CLVM'">
+ <a-form-item :label="$t('volumegroup')">
+ <a-input v-decorator="['volumegroup', { rules: [{ required: true,
message: 'required'}] }]" />
+ </a-form-item>
+ </div>
+ <div v-if="protocolSelected === 'Gluster'">
+ <a-form-item :label="$t('volume')">
+ <a-input v-decorator="['volume']" />
+ </a-form-item>
+ </div>
+ <a-form-item :label="$t('storageTags')">
+ <a-select
+ mode="tags"
+ v-model="selectedTags"
+ >
+ <a-select-option v-for="tag in storageTags" :key="tag.name">{{
tag.name }}</a-select-option>
+ </a-select>
+ </a-form-item>
+ </a-form>
+ <div class="actions">
+ <a-button @click="closeModal">{{ $t('cancel') }}</a-button>
+ <a-button type="primary" @click="handleSubmit">{{ $t('ok')
}}</a-button>
+ </div>
+ </a-spin>
+ </div>
+</template>
+
+<script>
+import { api } from '@/api'
+export default {
+ name: 'AddPrimaryStorage',
+ props: {
+ resource: {
+ type: Object,
+ required: true
+ }
+ },
+ inject: ['parentFetchData'],
+ data () {
+ return {
+ hypervisors: ['KVM', 'VMware', 'Hyperv', 'Any'],
+ protocols: [],
+ providers: [],
+ scope: 'cluster',
+ zones: [],
+ pods: [],
+ clusters: [],
+ hosts: [],
+ selectedTags: [],
+ storageTags: [],
+ zoneId: '',
+ zoneSelected: '',
+ podSelected: '',
+ clusterSelected: '',
+ hostSelected: '',
+ hypervisorType: '',
+ protocolSelected: 'nfs',
+ providerSelected: 'DefaultPrimary',
+ selectedHypervisor: 'KVM',
+ size: 'default',
+ loading: false
+ }
+ },
+ beforeCreate () {
+ this.form = this.$form.createForm(this)
+ },
+ mounted () {
+ this.fetchData()
+ },
+ methods: {
+ fetchData () {
+ this.getInfraData()
+ this.listStorageTags()
+ this.listStorageProviders()
+ },
+ getInfraData () {
+ this.loading = true
+ api('listZones').then(json => {
+ this.zones = json.listzonesresponse.zone || []
+ this.changeZone(this.zones[0] ? this.zones[0].id : '')
+ }).finally(() => {
+ this.loading = false
+ })
+ },
+ changeZone (value) {
+ this.zoneSelected = value
+ if (this.zoneSelected === '') {
+ this.podSelected = ''
+ return
+ }
+ api('listPods', {
+ zoneid: this.zoneSelected
+ }).then(json => {
+ this.pods = json.listpodsresponse.pod || []
+ this.changePod(this.pods[0] ? this.pods[0].id : '')
+ })
+ },
+ changePod (value) {
+ this.podSelected = value
+ if (this.podSelected === '') {
+ this.clusterSelected = ''
+ return
+ }
+ api('listClusters', {
+ podid: this.podSelected
+ }).then(json => {
+ this.clusters = json.listclustersresponse.cluster || []
+ if (this.clusters.length > 0) {
+ this.clusterSelected = this.clusters[0].id
+ this.fetchHypervisor()
+ }
+ }).then(() => {
+ api('listHosts', {
+ clusterid: this.clusterSelected
+ }).then(json => {
+ this.hosts = json.listhostsresponse.host || []
+ if (this.hosts.length > 0) {
+ this.hostSelected = this.hosts[0].id
+ }
+ })
+ })
+ },
+ listStorageProviders () {
+ this.providers = []
+ this.loading = true
+ api('listStorageProviders', { type: 'primary' }).then(json => {
+ var providers = json.liststorageprovidersresponse.dataStoreProvider ||
[]
+ for (const provider of providers) {
+ this.providers.push(provider.name)
+ }
+ }).finally(() => {
+ this.loading = false
+ })
+ },
+ listStorageTags () {
+ this.loading = true
+ api('listStorageTags').then(json => {
+ this.storageTags = json.liststoragetagsresponse.storagetag || []
+ }).finally(() => {
+ this.loading = false
+ })
+ },
+ fetchHypervisor (value) {
+ const cluster = this.clusters.find(cluster => cluster.id ===
this.clusterSelected)
+ this.hypervisorType = cluster.hypervisortype
+ if (this.hypervisorType === 'KVM') {
+ this.protocols = ['nfs', 'SharedMountPoint', 'RBD', 'CLVM', 'Gluster',
'custom']
+ } else if (this.hypervisorType === 'XenServer') {
+ this.protocols = ['nfs', 'preSetup', 'iscsi', 'custom']
+ } else if (this.hypervisorType === 'VMware') {
+ this.protocols = ['nfs', 'vmfs', 'custom']
+ } else if (this.hypervisorType === 'Hyperv') {
+ this.protocols = ['SMB']
+ } else if (this.hypervisorType === 'Ovm') {
+ this.protocols = ['nfs', 'ocfs2']
+ } else if (this.hypervisorType === 'LXC') {
+ this.protocols = ['nfs', 'SharedMountPoint', 'RBD']
+ } else {
+ this.protocols = ['nfs']
+ }
+ },
+ nfsURL (server, path) {
+ var url
+ if (path.substring(0, 1) !== '/') {
+ path = '/' + path
+ }
+ if (server.indexOf('://') === -1) {
+ url = 'nfs://' + server + path
+ } else {
+ url = server + path
+ }
+
+ return url
+ },
+ smbURL (server, path, smbUsername, smbPassword, smbDomain) {
+ var url = ''
+ if (path.substring(0, 1) !== '/') {
+ path = '/' + path
+ }
+ if (server.indexOf('://') === -1) {
+ url += 'cifs://'
+ }
+ url += (server + path)
+ return url
+ },
+ presetupURL (server, path) {
+ var url
+ if (server.indexOf('://') === -1) {
+ url = 'presetup://' + server + path
+ } else {
+ url = server + path
+ }
+ return url
+ },
+ ocfs2URL (server, path) {
+ var url
+ if (server.indexOf('://') === -1) {
+ url = 'ocfs2://' + server + path
+ } else {
+ url = server + path
+ }
+ return url
+ },
+ SharedMountPointURL (server, path) {
+ var url
+ if (server.indexOf('://') === -1) {
+ url = 'SharedMountPoint://' + server + path
+ } else {
+ url = server + path
+ }
+ return url
+ },
+ rbdURL (monitor, pool, id, secret) {
+ var url
+ /* Replace the + and / symbols by - and _ to have URL-safe base64 going
to the API
+ It's hacky, but otherwise we'll confuse java.net.URI which splits
the incoming URI
+ */
+ secret = secret.replace('+', '-')
+ secret = secret.replace('/', '_')
+ if (id !== null && secret !== null) {
+ monitor = id + ':' + secret + '@' + monitor
+ }
+ if (pool.substring(0, 1) !== '/') {
+ pool = '/' + pool
+ }
+ if (monitor.indexOf('://') === -1) {
+ url = 'rbd://' + monitor + pool
+ } else {
+ url = monitor + pool
+ }
+ return url
+ },
+ clvmURL (vgname) {
+ var url
+ if (vgname.indexOf('://') === -1) {
+ url = 'clvm://localhost/' + vgname
+ } else {
+ url = vgname
+ }
+ return url
+ },
+ vmfsURL (server, path) {
+ var url
+ if (server.indexOf('://') === -1) {
+ url = 'vmfs://' + server + path
+ } else {
+ url = server + path
+ }
+ return url
+ },
+ iscsiURL (server, iqn, lun) {
+ var url
+ if (server.indexOf('://') === -1) {
+ url = 'iscsi://' + server + iqn + '/' + lun
+ } else {
+ url = server + iqn + '/' + lun
+ }
+ return url
+ },
+ glusterURL (server, path) {
+ var url
+ if (server.indexOf('://') === -1) {
+ url = 'gluster://' + server + path
+ } else {
+ url = server + path
+ }
+ return url
+ },
+ closeModal () {
+ this.$parent.$parent.close()
+ },
+ handleSubmit (e) {
+ e.preventDefault()
+ this.form.validateFields((err, values) => {
+ if (err) {
+ return
+ }
+ var params = {
+ scope: values.scope,
+ zoneid: values.zone,
+ name: values.name,
+ provider: values.provider
+ }
+ if (values.scope === 'zone') {
+ params.hypervisor = values.hypervisor
+ }
+ if (values.scope === 'cluster' || values.scope === 'host') {
+ params.podid = values.pod
+ params.clusterid = values.cluster
+ }
+ if (values.scope === 'host') {
+ params.hostid = values.host
+ }
+ var server = values.server ? values.server : null
+ var path = values.path ? values.path : null
+ if (path !== null && path.substring(0, 1) !== '/') {
+ path = '/' + path
+ }
+ var url = ''
+ if (values.protocol === 'nfs') {
+ url = this.nfsURL(server, path)
+ } else if (values.protocol === 'SMB') {
+ url = this.smbURL(server, path)
+ const smbParams = {
+ user: values.smbUsername,
+ password: values.smbPassword,
+ domain: values.smbDomain
+ }
+ Object.keys(smbParams).forEach((key, index) => {
+ params['details[' + index.toString() + '].' + key] = smbParams[key]
+ })
+ } else if (values.protocol === 'PreSetup') {
+ url = this.presetupURL(server, path)
+ } else if (values.protocol === 'ocfs2') {
+ url = this.ocfs2URL(server, path)
+ } else if (values.protocol === 'SharedMountPoint') {
+ url = this.SharedMountPointURL(server, path)
+ } else if (values.protocol === 'CLVM') {
+ var vg = (values.volumegroup.substring(0, 1) !== '/') ? ('/' +
values.volumegroup) : values.volumegroup
+ url = this.clvmURL(vg)
+ } else if (values.protocol === 'RBD') {
+ url = this.rbdURL(values.radosmonitor, values.radospool,
values.radosuser, values.radossecret)
+ } else if (values.protocol === 'vmfs') {
+ path = values.vCenterDataCenter
+ if (path.substring(0, 1) !== '/') {
+ path = '/' + path
+ }
+ path += '/' + values.vCenterDataStore
+ url = this.vmfsURL(server, path)
+ } else if (values.protocol === 'Gluster') {
+ var glustervolume = values.volume
+ if (glustervolume.substring(0, 1) !== '/') {
+ glustervolume = '/' + glustervolume
+ }
+ url = this.glusterURL(server, glustervolume)
+ } else if (values.protocol === 'iscsi') {
+ var iqn = values.iqn
+ if (iqn.substring(0, 1) !== '/') {
+ iqn = '/' + iqn
+ }
+ var lun = values.lun
+ url = this.iscsiURL(server, iqn, lun)
+ }
+ params.url = url
+ if (values.provider !== 'DefaultPrimary') {
+ if (values.managed) {
+ params.managed = true
+ } else {
+ params.managed = false
+ }
+ if (values.capacityBytes && values.capacityBytes.length > 0) {
+ params.capacityBytes = values.capacityBytes.split(',').join('')
+ }
+ if (values.capacityIops && values.capacityIops.length > 0) {
+ params.capacityIops = values.capacityIops.split(',').join('')
+ }
+ if (values.url && values.url.length > 0) {
+ params.url = values.url
+ }
+ }
+ if (this.selectedTags.length > 0) {
+ params.tags = this.selectedTags.join()
+ }
+ this.loading = true
+ api('createStoragePool', params).then(json => {
+ this.$notification.success({
+ message: this.$t('label.add.primary.storage'),
+ description: this.$t('label.add.primary.storage')
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: 'Request Failed',
+ description: (error.response && error.response.headers &&
error.response.headers['x-description']) || error.message
+ })
+ }).finally(() => {
+ this.loading = false
+ this.closeModal()
+ this.parentFetchData()
+ })
+ })
+ }
+ }
+}
+</script>
+<style lang="scss" scoped>
+.form-layout {
+ width: 80vw;
+ @media (min-width: 1000px) {
+ width: 500px;
+ }
+}
+.actions {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 20px;
+ button {
+ &:not(:last-child) {
+ margin-right: 10px;
+ }
+ }
+}
+</style>