feat: Add swift

1. Add swift container list page
2. Add swift container object list page
3. Add create/delete container
4. Add create/edit/delete/copy/cut/paste/rename file
5. Add create/delete folder

Change-Id: Id4a675688b4a8beb40921173d7637e331a77b77e
This commit is contained in:
Jingwei.Zhang 2021-11-25 10:43:20 +08:00
parent 986891f430
commit ace9ca0ece
32 changed files with 2389 additions and 10 deletions

View File

@ -21,9 +21,11 @@ export default class BaseClient {
getUrl = (url) => { getUrl = (url) => {
if (this.projectInUrl) { if (this.projectInUrl) {
return `${this.baseUrl}/${this.project}/${url}`; return url
? `${this.baseUrl}/${this.project}/${url}`
: `${this.baseUrl}/${this.project}`;
} }
return `${this.baseUrl}/${url}`; return url ? `${this.baseUrl}/${url}` : `${this.baseUrl}`;
}; };
get request() { get request() {
@ -39,6 +41,7 @@ export default class BaseClient {
patch: (url, data, params, conf) => patch: (url, data, params, conf) =>
request.patch(this.getUrl(url), data, params, conf), request.patch(this.getUrl(url), data, params, conf),
head: (url, params, conf) => request.head(this.getUrl(url), params, conf), head: (url, params, conf) => request.head(this.getUrl(url), params, conf),
copy: (url, params, conf) => request.copy(this.getUrl(url), params, conf),
}; };
} }
@ -94,16 +97,19 @@ export default class BaseClient {
if (!resourceName) { if (!resourceName) {
return subResourceName; return subResourceName;
} }
if (!subResourceName) {
return resourceName;
}
if (resourceName[resourceName.length - 1] === '/') { if (resourceName[resourceName.length - 1] === '/') {
return `${resourceName.substr( return `${resourceName}${subResourceName}`;
0,
resourceName.length - 1
)}/${subResourceName}`;
} }
return `${resourceName}/${subResourceName}`; return `${resourceName}/${subResourceName}`;
} }
getSubResourceUrlById(resourceName, subResourceName, id) { getSubResourceUrlById(resourceName, subResourceName, id) {
if (!subResourceName) {
return this.getDetailUrl(resourceName, id);
}
return `${this.getDetailUrl(resourceName, id)}/${subResourceName}`; return `${this.getDetailUrl(resourceName, id)}/${subResourceName}`;
} }
@ -167,6 +173,8 @@ export default class BaseClient {
this.request.patch(this.getDetailUrl(resourceName, id), data, ...args), this.request.patch(this.getDetailUrl(resourceName, id), data, ...args),
delete: (id, ...args) => delete: (id, ...args) =>
this.request.delete(this.getDetailUrl(resourceName, id), ...args), this.request.delete(this.getDetailUrl(resourceName, id), ...args),
head: (id, ...args) =>
this.request.head(this.getDetailUrl(resourceName, id), ...args),
responseKey, responseKey,
enabled, enabled,
}; };
@ -223,6 +231,11 @@ export default class BaseClient {
this.getSubResourceUrlBySubId(resourceName, subResourceName, id, subId), this.getSubResourceUrlBySubId(resourceName, subResourceName, id, subId),
...args ...args
), ),
head: (id, subId, ...args) =>
this.request.head(
this.getSubResourceUrlBySubId(resourceName, subResourceName, id, subId),
...args
),
responseKey, responseKey,
enabled, enabled,
}); });
@ -308,6 +321,18 @@ export default class BaseClient {
), ),
...args ...args
), ),
head: (id, subId, subSubId, ...args) =>
this.request.head(
this.getSubSubResourceDetailUrl(
resourceName,
subResourceName,
subSubResonseName,
id,
subId,
subSubId
),
...args
),
responseKey, responseKey,
}); });

View File

@ -29,6 +29,7 @@ export const endpointVersionMap = {
ironicInspector: 'v1', ironicInspector: 'v1',
heat: 'v1', heat: 'v1',
octavia: 'v2', octavia: 'v2',
swift: 'v1',
}; };
export const endpointsDefault = { export const endpointsDefault = {
@ -63,11 +64,13 @@ export const ironicInspectorBase = () =>
export const placementBase = () => getOpenstackEndpoint('placement'); export const placementBase = () => getOpenstackEndpoint('placement');
export const heatBase = () => getOpenstackEndpoint('heat'); export const heatBase = () => getOpenstackEndpoint('heat');
export const octaviaBase = () => getOpenstackEndpoint('octavia'); export const octaviaBase = () => getOpenstackEndpoint('octavia');
export const swiftBase = () => getOpenstackEndpoint('swift');
export const ironicOriginEndpoint = () => getOriginEndpoint('ironic'); export const ironicOriginEndpoint = () => getOriginEndpoint('ironic');
export const vpnEndpoint = () => getOriginEndpoint('neutron_vpn'); export const vpnEndpoint = () => getOriginEndpoint('neutron_vpn');
export const lbEndpoint = () => getOriginEndpoint('octavia'); export const lbEndpoint = () => getOriginEndpoint('octavia');
export const qosEndpoint = () => getOriginEndpoint('neutron_qos'); export const qosEndpoint = () => getOriginEndpoint('neutron_qos');
export const swiftEndpoint = () => getOriginEndpoint('swift');
export const apiVersionMaps = { export const apiVersionMaps = {
nova: { nova: {

View File

@ -14,11 +14,11 @@
import Axios from 'axios'; import Axios from 'axios';
import { getLocalStorageItem } from 'utils/local-storage'; import { getLocalStorageItem } from 'utils/local-storage';
import { isEqual } from 'lodash'; import { isEmpty } from 'lodash';
import qs from 'qs'; import qs from 'qs';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']; const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'COPY'];
/** /**
* @class HttpRequest * @class HttpRequest
* request with axios * request with axios
@ -54,11 +54,12 @@ export class HttpRequest {
config.headers[apiVersionMap.key] = apiVersionMap.value; config.headers[apiVersionMap.key] = apiVersionMap.value;
} }
const { options: { headers, isFormData, ...rest } = {} } = config; const { options: { headers, isFormData, ...rest } = {} } = config;
if (!isEqual(headers)) { if (!isEmpty(headers)) {
config.headers = { config.headers = {
...config.headers, ...config.headers,
...headers, ...headers,
}; };
console.log('new config headers', config.headers);
} }
if (isFormData) { if (isFormData) {
delete config.headers['Content-Type']; delete config.headers['Content-Type'];
@ -77,6 +78,10 @@ export class HttpRequest {
const { data, status } = response; const { data, status } = response;
const disposition = response.headers['content-disposition'] || ''; const disposition = response.headers['content-disposition'] || '';
const contentType = response.headers['content-type'] || ''; const contentType = response.headers['content-type'] || '';
const { method = 'get' } = response.config || {};
if (method.toLowerCase() === 'head') {
return response;
}
if (contentType.includes('application/octet-stream')) { if (contentType.includes('application/octet-stream')) {
return response; return response;
} }
@ -156,7 +161,11 @@ export class HttpRequest {
generateRequestMap = () => { generateRequestMap = () => {
METHODS.forEach((method) => { METHODS.forEach((method) => {
const lowerMethod = method.toLowerCase(); const lowerMethod = method.toLowerCase();
if (lowerMethod === 'get' || lowerMethod === 'head') { if (
lowerMethod === 'get' ||
lowerMethod === 'head' ||
lowerMethod === 'copy'
) {
this.request[lowerMethod] = (url, params = {}, options) => { this.request[lowerMethod] = (url, params = {}, options) => {
return this.buildRequest({ return this.buildRequest({
method: lowerMethod, method: lowerMethod,

View File

@ -22,6 +22,7 @@ import heat from './heat';
import octavia from './octavia'; import octavia from './octavia';
import placement from './placement'; import placement from './placement';
import ironic from './ironic'; import ironic from './ironic';
import swift from './swift';
const client = { const client = {
skyline, skyline,
@ -34,6 +35,7 @@ const client = {
octavia, octavia,
placement, placement,
ironic, ironic,
swift,
}; };
window.client = client; window.client = client;

134
src/client/swift/index.js Normal file
View File

@ -0,0 +1,134 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import Base from '../client/base';
import { swiftBase } from '../client/constants';
class SwiftClient extends Base {
get baseUrl() {
return swiftBase();
}
get checkNameCode() {
return {
200: 'container exists',
401: 'user not login',
403: 'not allow to access this container',
404: 'not found this container',
500: 'other exception',
};
}
get checkNameCodeObject() {
return {
200: 'container exists',
401: 'user not login',
403: 'not allow to access this container',
404: 'not found this container',
500: 'other exception',
};
}
get projectInUrl() {
return true;
}
getUrl = (url) => {
const prefix = `${this.baseUrl}/AUTH_${this.project}`;
return url ? `${prefix}/${url}` : prefix;
};
get resources() {
return [
{
name: 'container',
key: '',
extendOperations: [
{
key: 'url',
generate: (name) => {
return this.getUrl(name);
},
},
{
key: 'create',
generate: (name) => {
return this.request.put(encodeURIComponent(name));
},
},
{
key: 'showMetadata',
generate: (name) => {
return this.request.head(encodeURIComponent(name));
},
},
{
key: 'updateMetadata',
generate: (name, headers) => {
return this.request.post(encodeURIComponent(name), null, null, {
headers,
});
},
},
{
key: 'uploadFile',
generate: (container, name, content, conf) => {
const url = `${encodeURIComponent(
container
)}/${encodeURIComponent(name)}`;
return this.request.put(url, content, null, conf);
},
},
{
key: 'createFolder',
generate: (container, name) => {
const url = `${encodeURIComponent(
container
)}/${encodeURIComponent(name)}`;
return this.request.put(url);
},
},
{
key: 'showObjectMetadata',
generate: (container, objectName) => {
const url = `${encodeURIComponent(
container
)}/${encodeURIComponent(objectName)}`;
return this.request.head(url);
},
},
{
key: 'copy',
generate: (fromContaier, fromName, toContainer, toName) => {
const url = `${fromContaier}/${fromName}`;
const headers = {
Destination: encodeURIComponent(`${toContainer}/${toName}`),
};
return this.request.copy(url, null, { headers });
},
},
],
subResources: [
{
key: '',
name: 'object',
},
],
},
];
}
}
const swiftClient = new SwiftClient();
export default swiftClient;

View File

@ -25,6 +25,7 @@ const {
crontabNameMessage, crontabNameMessage,
imageNameMessage, imageNameMessage,
instanceNameMessage, instanceNameMessage,
swiftFilenameMessage,
} = nameMessageInfo; } = nameMessageInfo;
const { const {
@ -36,6 +37,7 @@ const {
crontabNameValidate, crontabNameValidate,
imageNameValidate, imageNameValidate,
instanceNameValidate, instanceNameValidate,
swiftFileNameValidate,
} = nameTypeValidate; } = nameTypeValidate;
export default class index extends Component { export default class index extends Component {
@ -51,6 +53,7 @@ export default class index extends Component {
isCrontab, isCrontab,
isImage, isImage,
isInstance, isInstance,
isSwiftFile,
}) { }) {
const uniqueNameValidate = (rule, value) => { const uniqueNameValidate = (rule, value) => {
if (names && names.length && names.includes(value)) { if (names && names.length && names.includes(value)) {
@ -77,6 +80,8 @@ export default class index extends Component {
validator = instanceNameValidate; validator = instanceNameValidate;
} else if (isCrontab) { } else if (isCrontab) {
validator = crontabNameValidate; validator = crontabNameValidate;
} else if (isSwiftFile) {
validator = swiftFileNameValidate;
} }
const newRules = { const newRules = {
validator, validator,
@ -95,6 +100,7 @@ export default class index extends Component {
isCrontab, isCrontab,
isImage, isImage,
isInstance, isInstance,
isSwiftFile,
}) { }) {
if (withoutChinese) { if (withoutChinese) {
return nameMessageWithoutChinese; return nameMessageWithoutChinese;
@ -117,6 +123,9 @@ export default class index extends Component {
if (isInstance) { if (isInstance) {
return instanceNameMessage; return instanceNameMessage;
} }
if (isSwiftFile) {
return swiftFilenameMessage;
}
return nameMessage; return nameMessage;
} }
@ -126,6 +135,7 @@ export default class index extends Component {
withoutChinese = false, withoutChinese = false,
isFile = false, isFile = false,
isKeypair = false, isKeypair = false,
isSwiftFile = false,
isStack, isStack,
isCrontab, isCrontab,
isImage, isImage,
@ -151,6 +161,7 @@ export default class index extends Component {
isCrontab, isCrontab,
isImage, isImage,
isInstance, isInstance,
isSwiftFile,
}); });
const message = this.getMessage({ const message = this.getMessage({
withoutChinese, withoutChinese,
@ -160,6 +171,7 @@ export default class index extends Component {
isCrontab, isCrontab,
isImage, isImage,
isInstance, isInstance,
isSwiftFile,
}); });
const newFormItemProps = { const newFormItemProps = {
...rest, ...rest,

View File

@ -194,6 +194,28 @@ const renderMenu = (t) => {
}, },
], ],
}, },
{
path: '/storage/container',
name: t('Object Storage'),
key: 'container',
level: 1,
children: [
{
path: /^\/storage\/container\/detail\/[^/]+$/,
name: t('Container Detail'),
key: 'containerDetail',
level: 2,
routePath: '/storage/container/detail/:id',
},
{
path: /^\/storage\/container\/detail\/[^/]+\/.+$/,
name: t('Folder Detail'),
key: 'folderDetail',
level: 2,
routePath: '/storage/container/detail/:container/:folder',
},
],
},
], ],
}, },
{ {

View File

@ -7,15 +7,22 @@
"1. The name of the custom resource class property should start with CUSTOM_, can only contain uppercase letters A ~ Z, numbers 0 ~ 9 or underscores, and the length should not exceed 255 characters (for example: CUSTOM_BAREMETAL_SMALL).": "1. The name of the custom resource class property should start with CUSTOM_, can only contain uppercase letters A ~ Z, numbers 0 ~ 9 or underscores, and the length should not exceed 255 characters (for example: CUSTOM_BAREMETAL_SMALL).", "1. The name of the custom resource class property should start with CUSTOM_, can only contain uppercase letters A ~ Z, numbers 0 ~ 9 or underscores, and the length should not exceed 255 characters (for example: CUSTOM_BAREMETAL_SMALL).": "1. The name of the custom resource class property should start with CUSTOM_, can only contain uppercase letters A ~ Z, numbers 0 ~ 9 or underscores, and the length should not exceed 255 characters (for example: CUSTOM_BAREMETAL_SMALL).",
"1. The name of the trait should start with CUSTOM_, can only contain uppercase letters A ~ Z, numbers 0 ~ 9 or underscores, and the length should not exceed 255 characters (for example: CUSTOM_TRAIT1).": "1. The name of the trait should start with CUSTOM_, can only contain uppercase letters A ~ Z, numbers 0 ~ 9 or underscores, and the length should not exceed 255 characters (for example: CUSTOM_TRAIT1).", "1. The name of the trait should start with CUSTOM_, can only contain uppercase letters A ~ Z, numbers 0 ~ 9 or underscores, and the length should not exceed 255 characters (for example: CUSTOM_TRAIT1).": "1. The name of the trait should start with CUSTOM_, can only contain uppercase letters A ~ Z, numbers 0 ~ 9 or underscores, and the length should not exceed 255 characters (for example: CUSTOM_TRAIT1).",
"1. The volume associated with the backup is available.": "1. The volume associated with the backup is available.", "1. The volume associated with the backup is available.": "1. The volume associated with the backup is available.",
"10s": "10s",
"1D": "1D",
"1H": "1H",
"1min": "1min",
"2. The trait of the scheduled node needs to correspond to the trait of the flavor used by the ironic instance; by injecting the necessary traits into the ironic instance, the computing service will only schedule the instance to the bare metal node with all the necessary traits (for example, the ironic instance which use the flavor that has CUSTOM_TRAIT1 as a necessary trait, can be scheduled to the node which has the trait of CUSTOM_TRAIT1).": "2. The trait of the scheduled node needs to correspond to the trait of the flavor used by the ironic instance; by injecting the necessary traits into the ironic instance, the computing service will only schedule the instance to the bare metal node with all the necessary traits (for example, the ironic instance which use the flavor that has CUSTOM_TRAIT1 as a necessary trait, can be scheduled to the node which has the trait of CUSTOM_TRAIT1).", "2. The trait of the scheduled node needs to correspond to the trait of the flavor used by the ironic instance; by injecting the necessary traits into the ironic instance, the computing service will only schedule the instance to the bare metal node with all the necessary traits (for example, the ironic instance which use the flavor that has CUSTOM_TRAIT1 as a necessary trait, can be scheduled to the node which has the trait of CUSTOM_TRAIT1).": "2. The trait of the scheduled node needs to correspond to the trait of the flavor used by the ironic instance; by injecting the necessary traits into the ironic instance, the computing service will only schedule the instance to the bare metal node with all the necessary traits (for example, the ironic instance which use the flavor that has CUSTOM_TRAIT1 as a necessary trait, can be scheduled to the node which has the trait of CUSTOM_TRAIT1).",
"2. The volume associated with the backup has been mounted, and the instance is shut down.": "2. The volume associated with the backup has been mounted, and the instance is shut down.", "2. The volume associated with the backup has been mounted, and the instance is shut down.": "2. The volume associated with the backup has been mounted, and the instance is shut down.",
"2. To ensure the integrity of the data, it is recommended that you suspend the write operation of all files when creating a backup.": "2. To ensure the integrity of the data, it is recommended that you suspend the write operation of all files when creating a backup.", "2. To ensure the integrity of the data, it is recommended that you suspend the write operation of all files when creating a backup.": "2. To ensure the integrity of the data, it is recommended that you suspend the write operation of all files when creating a backup.",
"2. You can customize the resource class name of the flavor, but it needs to correspond to the resource class of the scheduled node (for example, the resource class name of the scheduling node is baremetal.with-GPU, and the custom resource class name of the flavor is CUSTOM_BAREMETAL_WITH_GPU=1).": "2. You can customize the resource class name of the flavor, but it needs to correspond to the resource class of the scheduled node (for example, the resource class name of the scheduling node is baremetal.with-GPU, and the custom resource class name of the flavor is CUSTOM_BAREMETAL_WITH_GPU=1).", "2. You can customize the resource class name of the flavor, but it needs to correspond to the resource class of the scheduled node (for example, the resource class name of the scheduling node is baremetal.with-GPU, and the custom resource class name of the flavor is CUSTOM_BAREMETAL_WITH_GPU=1).": "2. You can customize the resource class name of the flavor, but it needs to correspond to the resource class of the scheduled node (for example, the resource class name of the scheduling node is baremetal.with-GPU, and the custom resource class name of the flavor is CUSTOM_BAREMETAL_WITH_GPU=1).",
"5min": "5min",
"8 to 16 characters, at least one uppercase letter, one lowercase letter, one number and one special character.": "8 to 16 characters, at least one uppercase letter, one lowercase letter, one number and one special character.", "8 to 16 characters, at least one uppercase letter, one lowercase letter, one number and one special character.": "8 to 16 characters, at least one uppercase letter, one lowercase letter, one number and one special character.",
"8 to 16 characters, at least one uppercase letter, one lowercase letter, one number.": "8 to 16 characters, at least one uppercase letter, one lowercase letter, one number.", "8 to 16 characters, at least one uppercase letter, one lowercase letter, one number.": "8 to 16 characters, at least one uppercase letter, one lowercase letter, one number.",
"A DNAT rule has been created for this port of this IP, please choose another port.": "A DNAT rule has been created for this port of this IP, please choose another port.", "A DNAT rule has been created for this port of this IP, please choose another port.": "A DNAT rule has been created for this port of this IP, please choose another port.",
"A container with the same name already exists": "A container with the same name already exists",
"A dynamic scheduling algorithm that estimates the server load based on the number of currently active connections. The system allocates new connection requests to the server with the least number of current connections. Commonly used for long connection services, such as database connections and other services.": "A dynamic scheduling algorithm that estimates the server load based on the number of currently active connections. The system allocates new connection requests to the server with the least number of current connections. Commonly used for long connection services, such as database connections and other services.", "A dynamic scheduling algorithm that estimates the server load based on the number of currently active connections. The system allocates new connection requests to the server with the least number of current connections. Commonly used for long connection services, such as database connections and other services.": "A dynamic scheduling algorithm that estimates the server load based on the number of currently active connections. The system allocates new connection requests to the server with the least number of current connections. Commonly used for long connection services, such as database connections and other services.",
"A host aggregate can be associated with at most one AZ. Once the association is established, the AZ cannot be disassociated.": "A host aggregate can be associated with at most one AZ. Once the association is established, the AZ cannot be disassociated.", "A host aggregate can be associated with at most one AZ. Once the association is established, the AZ cannot be disassociated.": "A host aggregate can be associated with at most one AZ. Once the association is established, the AZ cannot be disassociated.",
"A public container will allow anyone to use the objects in your container through a public URL.": "A public container will allow anyone to use the objects in your container through a public URL.",
"A snapshot is an image which preserves the disk state of a running instance, which can be used to start a new instance.": "A snapshot is an image which preserves the disk state of a running instance, which can be used to start a new instance.", "A snapshot is an image which preserves the disk state of a running instance, which can be used to start a new instance.": "A snapshot is an image which preserves the disk state of a running instance, which can be used to start a new instance.",
"A template is a YAML file that contains configuration information, please enter the correct format.": "A template is a YAML file that contains configuration information, please enter the correct format.", "A template is a YAML file that contains configuration information, please enter the correct format.": "A template is a YAML file that contains configuration information, please enter the correct format.",
"A template is a YAML file that contains configuration information.": "A template is a YAML file that contains configuration information.", "A template is a YAML file that contains configuration information.": "A template is a YAML file that contains configuration information.",
@ -89,6 +96,7 @@
"Allocation Pools": "Allocation Pools", "Allocation Pools": "Allocation Pools",
"Allowed Address Pair": "Allowed Address Pair", "Allowed Address Pair": "Allowed Address Pair",
"American Samoa": "American Samoa", "American Samoa": "American Samoa",
"An object with the same name already exists": "An object with the same name already exists",
"Andorra": "Andorra", "Andorra": "Andorra",
"Angola": "Angola", "Angola": "Angola",
"Anguilla": "Anguilla", "Anguilla": "Anguilla",
@ -236,6 +244,7 @@
"CPU value is { cpu }, NUMA CPU value is { totalCpu }, need to be equal. ": "CPU value is { cpu }, NUMA CPU value is { totalCpu }, need to be equal. ", "CPU value is { cpu }, NUMA CPU value is { totalCpu }, need to be equal. ": "CPU value is { cpu }, NUMA CPU value is { totalCpu }, need to be equal. ",
"CPU(Core)": "CPU(Core)", "CPU(Core)": "CPU(Core)",
"CPU/memory can only be increased or expanded online, and cannot be decreased or reduced online.": "CPU/memory can only be increased or expanded online, and cannot be decreased or reduced online.", "CPU/memory can only be increased or expanded online, and cannot be decreased or reduced online.": "CPU/memory can only be increased or expanded online, and cannot be decreased or reduced online.",
"Cache Service": "Cache Service",
"Cameroon": "Cameroon", "Cameroon": "Cameroon",
"Can add { number } {name}": "Can add { number } {name}", "Can add { number } {name}": "Can add { number } {name}",
"Canada": "Canada", "Canada": "Canada",
@ -275,6 +284,7 @@
"Clean Wait": "Clean Wait", "Clean Wait": "Clean Wait",
"Cleaning": "Cleaning", "Cleaning": "Cleaning",
"Clear Gateway": "Clear Gateway", "Clear Gateway": "Clear Gateway",
"Click To View": "Click To View",
"Click here for filters.": "Click here for filters.", "Click here for filters.": "Click here for filters.",
"Click to Upload": "Click to Upload", "Click to Upload": "Click to Upload",
"Click to show detail": "Click to show detail", "Click to show detail": "Click to show detail",
@ -330,10 +340,15 @@
"Console Interface": "Console Interface", "Console Interface": "Console Interface",
"Consumer": "Consumer", "Consumer": "Consumer",
"Container": "Container", "Container": "Container",
"Container Detail": "Container Detail",
"Container Format": "Container Format", "Container Format": "Container Format",
"Container Name": "Container Name",
"Content": "Content", "Content": "Content",
"Content Type": "Content Type",
"Control Location": "Control Location", "Control Location": "Control Location",
"Cook Islands": "Cook Islands", "Cook Islands": "Cook Islands",
"Copy": "Copy",
"Copy File": "Copy File",
"CoreOS": "CoreOS", "CoreOS": "CoreOS",
"Costa Rica": "Costa Rica", "Costa Rica": "Costa Rica",
"Cote D'Ivoire": "Cote D'Ivoire", "Cote D'Ivoire": "Cote D'Ivoire",
@ -346,6 +361,7 @@
"Create Bandwidth Limit Rule": "Create Bandwidth Limit Rule", "Create Bandwidth Limit Rule": "Create Bandwidth Limit Rule",
"Create Bare Metal Node": "Create Bare Metal Node", "Create Bare Metal Node": "Create Bare Metal Node",
"Create Complete": "Create Complete", "Create Complete": "Create Complete",
"Create Container": "Create Container",
"Create DNAT Rule": "Create DNAT Rule", "Create DNAT Rule": "Create DNAT Rule",
"Create DNAT rule": "Create DNAT rule", "Create DNAT rule": "Create DNAT rule",
"Create DSCP Marking Rule": "Create DSCP Marking Rule", "Create DSCP Marking Rule": "Create DSCP Marking Rule",
@ -355,6 +371,7 @@
"Create Extra Specs": "Create Extra Specs", "Create Extra Specs": "Create Extra Specs",
"Create Failed": "Create Failed", "Create Failed": "Create Failed",
"Create Flavor": "Create Flavor", "Create Flavor": "Create Flavor",
"Create Folder": "Create Folder",
"Create Host Aggregate": "Create Host Aggregate", "Create Host Aggregate": "Create Host Aggregate",
"Create IPsec Site Connection": "Create IPsec Site Connection", "Create IPsec Site Connection": "Create IPsec Site Connection",
"Create Image": "Create Image", "Create Image": "Create Image",
@ -419,6 +436,7 @@
"Current Host": "Current Host", "Current Host": "Current Host",
"Current Interface": "Current Interface", "Current Interface": "Current Interface",
"Current Password": "Current Password", "Current Password": "Current Password",
"Current Path: ": "Current Path: ",
"Current Project": "Current Project", "Current Project": "Current Project",
"Current Project Image": "Current Project Image", "Current Project Image": "Current Project Image",
"Current Project Network": "Current Project Network", "Current Project Network": "Current Project Network",
@ -433,6 +451,8 @@
"Custom TCP Rule": "Custom TCP Rule", "Custom TCP Rule": "Custom TCP Rule",
"Custom Trait": "Custom Trait", "Custom Trait": "Custom Trait",
"Custom UDP Rule": "Custom UDP Rule", "Custom UDP Rule": "Custom UDP Rule",
"Cut": "Cut",
"Cut File": "Cut File",
"Cyprus": "Cyprus", "Cyprus": "Cyprus",
"Czech Republic": "Czech Republic", "Czech Republic": "Czech Republic",
"DCCP": "DCCP", "DCCP": "DCCP",
@ -455,6 +475,7 @@
"Data Source Type": "Data Source Type", "Data Source Type": "Data Source Type",
"Database": "Database", "Database": "Database",
"Database Instance": "Database Instance", "Database Instance": "Database Instance",
"Database Service": "Database Service",
"Deactivated": "Deactivated", "Deactivated": "Deactivated",
"Debian": "Debian", "Debian": "Debian",
"Dedicated": "Dedicated", "Dedicated": "Dedicated",
@ -468,6 +489,7 @@
"Delete Bandwidth Egress Rules": "Delete Bandwidth Egress Rules", "Delete Bandwidth Egress Rules": "Delete Bandwidth Egress Rules",
"Delete Bandwidth Ingress Rules": "Delete Bandwidth Ingress Rules", "Delete Bandwidth Ingress Rules": "Delete Bandwidth Ingress Rules",
"Delete Complete": "Delete Complete", "Delete Complete": "Delete Complete",
"Delete Container": "Delete Container",
"Delete DNAT Rule": "Delete DNAT Rule", "Delete DNAT Rule": "Delete DNAT Rule",
"Delete DSCP Marking Rules": "Delete DSCP Marking Rules", "Delete DSCP Marking Rules": "Delete DSCP Marking Rules",
"Delete Default Pool": "Delete Default Pool", "Delete Default Pool": "Delete Default Pool",
@ -475,7 +497,9 @@
"Delete Encryption": "Delete Encryption", "Delete Encryption": "Delete Encryption",
"Delete Extra Specs": "Delete Extra Specs", "Delete Extra Specs": "Delete Extra Specs",
"Delete Failed": "Delete Failed", "Delete Failed": "Delete Failed",
"Delete File": "Delete File",
"Delete Flavor": "Delete Flavor", "Delete Flavor": "Delete Flavor",
"Delete Folder": "Delete Folder",
"Delete Group": "Delete Group", "Delete Group": "Delete Group",
"Delete Host Aggregate": "Delete Host Aggregate", "Delete Host Aggregate": "Delete Host Aggregate",
"Delete IPsec Site Connection": "Delete IPsec Site Connection", "Delete IPsec Site Connection": "Delete IPsec Site Connection",
@ -523,6 +547,7 @@
"Deploying": "Deploying", "Deploying": "Deploying",
"Deployment Parameters": "Deployment Parameters", "Deployment Parameters": "Deployment Parameters",
"Description": "Description", "Description": "Description",
"Dest Folder": "Dest Folder",
"Destination CIDR": "Destination CIDR", "Destination CIDR": "Destination CIDR",
"Detach": "Detach", "Detach": "Detach",
"Detach Instance": "Detach Instance", "Detach Instance": "Detach Instance",
@ -532,6 +557,7 @@
"Detach interface": "Detach interface", "Detach interface": "Detach interface",
"Detaching": "Detaching", "Detaching": "Detaching",
"Detail": "Detail", "Detail": "Detail",
"Detail Info": "Detail Info",
"Details": "Details", "Details": "Details",
"Device ID": "Device ID", "Device ID": "Device ID",
"Device Owner": "Device Owner", "Device Owner": "Device Owner",
@ -558,6 +584,7 @@
"Disk size is limited by the min disk of flavor, image, etc.": "Disk size is limited by the min disk of flavor, image, etc.", "Disk size is limited by the min disk of flavor, image, etc.": "Disk size is limited by the min disk of flavor, image, etc.",
"Djibouti": "Djibouti", "Djibouti": "Djibouti",
"Do Build And Run Instance": "Do Build And Run Instance", "Do Build And Run Instance": "Do Build And Run Instance",
"Do HH:mm": "Do HH:mm",
"Do not reset the normally mounted volume to the \"available\"、\"maintenance\" or \"error\" status. The reset state does not remove the volume from the instance. If you need to remove the volume from the instance, please go to the console of the corresponding project and use the \"detach\" operation.": "Do not reset the normally mounted volume to the \"available\"、\"maintenance\" or \"error\" status. The reset state does not remove the volume from the instance. If you need to remove the volume from the instance, please go to the console of the corresponding project and use the \"detach\" operation.", "Do not reset the normally mounted volume to the \"available\"、\"maintenance\" or \"error\" status. The reset state does not remove the volume from the instance. If you need to remove the volume from the instance, please go to the console of the corresponding project and use the \"detach\" operation.": "Do not reset the normally mounted volume to the \"available\"、\"maintenance\" or \"error\" status. The reset state does not remove the volume from the instance. If you need to remove the volume from the instance, please go to the console of the corresponding project and use the \"detach\" operation.",
"Do not set with a backend": "Do not set with a backend", "Do not set with a backend": "Do not set with a backend",
"Domain": "Domain", "Domain": "Domain",
@ -569,6 +596,7 @@
"Domains": "Domains", "Domains": "Domains",
"Dominica": "Dominica", "Dominica": "Dominica",
"Down": "Down", "Down": "Down",
"Download File": "Download File",
"Download all data": "Download all data", "Download all data": "Download all data",
"Download canceled!": "Download canceled!", "Download canceled!": "Download canceled!",
"Download current data": "Download current data", "Download current data": "Download current data",
@ -623,6 +651,7 @@
"Edit host aggregate": "Edit host aggregate", "Edit host aggregate": "Edit host aggregate",
"Edit metadata": "Edit metadata", "Edit metadata": "Edit metadata",
"Edit quota": "Edit quota", "Edit quota": "Edit quota",
"Editing only changes the content of the file, not the file name.": "Editing only changes the content of the file, not the file name.",
"Effective Mode": "Effective Mode", "Effective Mode": "Effective Mode",
"Effective mode after configuration changes": "Effective mode after configuration changes", "Effective mode after configuration changes": "Effective mode after configuration changes",
"Egress": "Egress", "Egress": "Egress",
@ -699,6 +728,7 @@
"Fiji": "Fiji", "Fiji": "Fiji",
"File": "File", "File": "File",
"Filename": "Filename", "Filename": "Filename",
"Files: {names}": "Files: {names}",
"Fill In The Parameters": "Fill In The Parameters", "Fill In The Parameters": "Fill In The Parameters",
"Filter Project": "Filter Project", "Filter Project": "Filter Project",
"Fingerprint": "Fingerprint", "Fingerprint": "Fingerprint",
@ -720,6 +750,8 @@
"Floating Ip Address": "Floating Ip Address", "Floating Ip Address": "Floating Ip Address",
"Floating Ip Detail": "Floating Ip Detail", "Floating Ip Detail": "Floating Ip Detail",
"Floating ip has already been associate, Please check Force release": "Floating ip has already been associate, Please check Force release", "Floating ip has already been associate, Please check Force release": "Floating ip has already been associate, Please check Force release",
"Folder Detail": "Folder Detail",
"Folder Name": "Folder Name",
"Fontend": "Fontend", "Fontend": "Fontend",
"For GPU type, you need to install GPU drivers in the instance operating system.": "For GPU type, you need to install GPU drivers in the instance operating system.", "For GPU type, you need to install GPU drivers in the instance operating system.": "For GPU type, you need to install GPU drivers in the instance operating system.",
"For GRE networks, valid segmentation IDs are 1 to 4294967295": "For GRE networks, valid segmentation IDs are 1 to 4294967295", "For GRE networks, valid segmentation IDs are 1 to 4294967295": "For GRE networks, valid segmentation IDs are 1 to 4294967295",
@ -781,6 +813,7 @@
"Haiti": "Haiti", "Haiti": "Haiti",
"Hard Reboot": "Hard Reboot", "Hard Reboot": "Hard Reboot",
"Hard Rebooting": "Hard Rebooting", "Hard Rebooting": "Hard Rebooting",
"Hash": "Hash",
"Health Monitor Delay": "Health Monitor Delay", "Health Monitor Delay": "Health Monitor Delay",
"Health Monitor Detail": "Health Monitor Detail", "Health Monitor Detail": "Health Monitor Detail",
"Health Monitor Max Retries": "Health Monitor Max Retries", "Health Monitor Max Retries": "Health Monitor Max Retries",
@ -789,6 +822,7 @@
"Health Monitor Type": "Health Monitor Type", "Health Monitor Type": "Health Monitor Type",
"HealthMonitor": "HealthMonitor", "HealthMonitor": "HealthMonitor",
"HealthMonitor Type": "HealthMonitor Type", "HealthMonitor Type": "HealthMonitor Type",
"Healthy": "Healthy",
"Heartbeat Timestamp": "Heartbeat Timestamp", "Heartbeat Timestamp": "Heartbeat Timestamp",
"Heterogeneous Computing": "Heterogeneous Computing", "Heterogeneous Computing": "Heterogeneous Computing",
"Hide Advanced Options": "Hide Advanced Options", "Hide Advanced Options": "Hide Advanced Options",
@ -1010,6 +1044,10 @@
"Lao People's Democratic Republic": "Lao People's Democratic Republic", "Lao People's Democratic Republic": "Lao People's Democratic Republic",
"Large": "Large", "Large": "Large",
"Large(Optimal performance)": "Large(Optimal performance)", "Large(Optimal performance)": "Large(Optimal performance)",
"Last 2 Weeks": "Last 2 Weeks",
"Last 7 Days": "Last 7 Days",
"Last Day": "Last Day",
"Last Hour": "Last Hour",
"Last Updated": "Last Updated", "Last Updated": "Last Updated",
"Latvia": "Latvia", "Latvia": "Latvia",
"Leave Maintenance Mode": "Leave Maintenance Mode", "Leave Maintenance Mode": "Leave Maintenance Mode",
@ -1111,6 +1149,7 @@
"Memory Page": "Memory Page", "Memory Page": "Memory Page",
"Memory usage Num (GB": "Memory usage Num (GB", "Memory usage Num (GB": "Memory usage Num (GB",
"Message": "Message", "Message": "Message",
"Message Queue Service": "Message Queue Service",
"Metadata": "Metadata", "Metadata": "Metadata",
"Metadata Definitions": "Metadata Definitions", "Metadata Definitions": "Metadata Definitions",
"Metadata Detail": "Metadata Detail", "Metadata Detail": "Metadata Detail",
@ -1209,15 +1248,19 @@
"Not yet bound": "Not yet bound", "Not yet bound": "Not yet bound",
"Not yet selected": "Not yet selected", "Not yet selected": "Not yet selected",
"Note: Are you sure you need to modify the volume type?": "Note: Are you sure you need to modify the volume type?", "Note: Are you sure you need to modify the volume type?": "Note: Are you sure you need to modify the volume type?",
"Note: Please consider the container name carefully since it couldn't be changed after created.": "Note: Please consider the container name carefully since it couldn't be changed after created.",
"Note: The security group you use will act on all virtual adapters of the instance.": "Note: The security group you use will act on all virtual adapters of the instance.", "Note: The security group you use will act on all virtual adapters of the instance.": "Note: The security group you use will act on all virtual adapters of the instance.",
"Number Of Ports": "Number Of Ports", "Number Of Ports": "Number Of Ports",
"Number of GPU": "Number of GPU", "Number of GPU": "Number of GPU",
"Number of Usb Controller": "Number of Usb Controller", "Number of Usb Controller": "Number of Usb Controller",
"OK": "OK",
"OS": "OS", "OS": "OS",
"OS Admin": "OS Admin", "OS Admin": "OS Admin",
"OS Disk": "OS Disk", "OS Disk": "OS Disk",
"OS Version": "OS Version", "OS Version": "OS Version",
"OSPF": "OSPF", "OSPF": "OSPF",
"Object Count": "Object Count",
"Object Storage": "Object Storage",
"Off": "Off", "Off": "Off",
"Offline": "Offline", "Offline": "Offline",
"Oman": "Oman", "Oman": "Oman",
@ -1239,6 +1282,7 @@
"Orchestration Services": "Orchestration Services", "Orchestration Services": "Orchestration Services",
"Orchestration information": "Orchestration information", "Orchestration information": "Orchestration information",
"Orchestration service:": "Orchestration service:", "Orchestration service:": "Orchestration service:",
"Origin File Name": "Origin File Name",
"Original Password": "Original Password", "Original Password": "Original Password",
"Other Protocol": "Other Protocol", "Other Protocol": "Other Protocol",
"Others": "Others", "Others": "Others",
@ -1267,6 +1311,8 @@
"Password Type": "Password Type", "Password Type": "Password Type",
"Password changed successfully, please log in again.": "Password changed successfully, please log in again.", "Password changed successfully, please log in again.": "Password changed successfully, please log in again.",
"Password must be the same with confirm password.": "Password must be the same with confirm password.", "Password must be the same with confirm password.": "Password must be the same with confirm password.",
"Paste": "Paste",
"Paste File": "Paste File",
"Pause": "Pause", "Pause": "Pause",
"Pause Instance": "Pause Instance", "Pause Instance": "Pause Instance",
"Paused": "Paused", "Paused": "Paused",
@ -1313,6 +1359,7 @@
"Please input IPv4 or IPv6 cidr": "Please input IPv4 or IPv6 cidr", "Please input IPv4 or IPv6 cidr": "Please input IPv4 or IPv6 cidr",
"Please input IPv4 or IPv6 cidr, (e.g. 192.168.0.0/24, 2001:DB8::/48)": "Please input IPv4 or IPv6 cidr, (e.g. 192.168.0.0/24, 2001:DB8::/48)", "Please input IPv4 or IPv6 cidr, (e.g. 192.168.0.0/24, 2001:DB8::/48)": "Please input IPv4 or IPv6 cidr, (e.g. 192.168.0.0/24, 2001:DB8::/48)",
"Please input a valid ip!": "Please input a valid ip!", "Please input a valid ip!": "Please input a valid ip!",
"Please input at least 2 characters.": "Please input at least 2 characters.",
"Please input auth key": "Please input auth key", "Please input auth key": "Please input auth key",
"Please input cipher": "Please input cipher", "Please input cipher": "Please input cipher",
"Please input file name": "Please input file name", "Please input file name": "Please input file name",
@ -1356,6 +1403,7 @@
"Please select!": "Please select!", "Please select!": "Please select!",
"Please set CPU && Ram first.": "Please set CPU && Ram first.", "Please set CPU && Ram first.": "Please set CPU && Ram first.",
"Please set MUNA": "Please set MUNA", "Please set MUNA": "Please set MUNA",
"Please upload files smaller than { size }G on the page. It is recommended to upload files over { size }G using API.": "Please upload files smaller than { size }G on the page. It is recommended to upload files over { size }G using API.",
"Pleasse input a valid ip!": "Pleasse input a valid ip!", "Pleasse input a valid ip!": "Pleasse input a valid ip!",
"Pleasse select a network!": "Pleasse select a network!", "Pleasse select a network!": "Pleasse select a network!",
"Pleasse select a subnet!": "Pleasse select a subnet!", "Pleasse select a subnet!": "Pleasse select a subnet!",
@ -1424,6 +1472,7 @@
"Provision State": "Provision State", "Provision State": "Provision State",
"Provisioning Status": "Provisioning Status", "Provisioning Status": "Provisioning Status",
"Public": "Public", "Public": "Public",
"Public Access": "Public Access",
"Public Image": "Public Image", "Public Image": "Public Image",
"Public Key": "Public Key", "Public Key": "Public Key",
"Puerto Rico": "Puerto Rico", "Puerto Rico": "Puerto Rico",
@ -1484,6 +1533,8 @@
"Remove": "Remove", "Remove": "Remove",
"Remove Network": "Remove Network", "Remove Network": "Remove Network",
"Remove Router": "Remove Router", "Remove Router": "Remove Router",
"Rename": "Rename",
"Rename is to copy the current file to the new file address and delete the current file, which will affect the creation time of the file.": "Rename is to copy the current file to the new file address and delete the current file, which will affect the creation time of the file.",
"Republic Of The Congo": "Republic Of The Congo", "Republic Of The Congo": "Republic Of The Congo",
"Request ID": "Request ID", "Request ID": "Request ID",
"Require": "Require", "Require": "Require",
@ -1535,6 +1586,7 @@
"Rollback In Progress": "Rollback In Progress", "Rollback In Progress": "Rollback In Progress",
"Romania": "Romania", "Romania": "Romania",
"Root Disk": "Root Disk", "Root Disk": "Root Disk",
"Root directory": "Root directory",
"Router": "Router", "Router": "Router",
"Router Detail": "Router Detail", "Router Detail": "Router Detail",
"Router External": "Router External", "Router External": "Router External",
@ -1570,6 +1622,7 @@
"Security Groups": "Security Groups", "Security Groups": "Security Groups",
"Security Info": "Security Info", "Security Info": "Security Info",
"Segmentation ID": "Segmentation ID", "Segmentation ID": "Segmentation ID",
"Select File": "Select File",
"Select Projct Role": "Select Projct Role", "Select Projct Role": "Select Projct Role",
"Select Project": "Select Project", "Select Project": "Select Project",
"Select Snapshot": "Select Snapshot", "Select Snapshot": "Select Snapshot",
@ -1644,6 +1697,7 @@
"Solomon Islands": "Solomon Islands", "Solomon Islands": "Solomon Islands",
"Somalia": "Somalia", "Somalia": "Somalia",
"Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.", "Sorry, the page you visited does not exist.": "Sorry, the page you visited does not exist.",
"Source Path: {path}": "Source Path: {path}",
"Source Port/Port Range": "Source Port/Port Range", "Source Port/Port Range": "Source Port/Port Range",
"South Africa": "South Africa", "South Africa": "South Africa",
"South Korea": "South Korea", "South Korea": "South Korea",
@ -1685,6 +1739,7 @@
"Storage Capacity(GB)": "Storage Capacity(GB)", "Storage Capacity(GB)": "Storage Capacity(GB)",
"Storage IOPS": "Storage IOPS", "Storage IOPS": "Storage IOPS",
"Storage Interface": "Storage Interface", "Storage Interface": "Storage Interface",
"Storage Policy": "Storage Policy",
"Storage Types": "Storage Types", "Storage Types": "Storage Types",
"Sub User": "Sub User", "Sub User": "Sub User",
"Subnet": "Subnet", "Subnet": "Subnet",
@ -1752,6 +1807,7 @@
"The description can be up to 255 characters long.": "The description can be up to 255 characters long.", "The description can be up to 255 characters long.": "The description can be up to 255 characters long.",
"The entire inspection process takes 5 to 10 minutes, so you need to be patient. After the registration is completed, the node configuration status will return to the manageable status.": "The entire inspection process takes 5 to 10 minutes, so you need to be patient. After the registration is completed, the node configuration status will return to the manageable status.", "The entire inspection process takes 5 to 10 minutes, so you need to be patient. After the registration is completed, the node configuration status will return to the manageable status.": "The entire inspection process takes 5 to 10 minutes, so you need to be patient. After the registration is completed, the node configuration status will return to the manageable status.",
"The feasible configuration of cloud-init or cloudbase-init service in the image is not synced to image's properties, so the Login Name is unknown.": "The feasible configuration of cloud-init or cloudbase-init service in the image is not synced to image's properties, so the Login Name is unknown.", "The feasible configuration of cloud-init or cloudbase-init service in the image is not synced to image's properties, so the Login Name is unknown.": "The feasible configuration of cloud-init or cloudbase-init service in the image is not synced to image's properties, so the Login Name is unknown.",
"The file with the same name will be overwritten.": "The file with the same name will be overwritten.",
"The instance architecture diagram mainly shows the overall architecture composition of the instance. If you need to view the network topology of the instance, please go to: ": "The instance architecture diagram mainly shows the overall architecture composition of the instance. If you need to view the network topology of the instance, please go to: ", "The instance architecture diagram mainly shows the overall architecture composition of the instance. If you need to view the network topology of the instance, please go to: ": "The instance architecture diagram mainly shows the overall architecture composition of the instance. If you need to view the network topology of the instance, please go to: ",
"The instance deleted immediately cannot be restored": "The instance deleted immediately cannot be restored", "The instance deleted immediately cannot be restored": "The instance deleted immediately cannot be restored",
"The instance is not shut down, unable to restore.": "The instance is not shut down, unable to restore.", "The instance is not shut down, unable to restore.": "The instance is not shut down, unable to restore.",
@ -1768,6 +1824,7 @@
"The name should start with upper letter, lower letter or chinese, and be a string of 1 to 128, characters can only contain \"0-9, a-z, A-Z, \"-'_()[].:^\".": "The name should start with upper letter, lower letter or chinese, and be a string of 1 to 128, characters can only contain \"0-9, a-z, A-Z, \"-'_()[].:^\".", "The name should start with upper letter, lower letter or chinese, and be a string of 1 to 128, characters can only contain \"0-9, a-z, A-Z, \"-'_()[].:^\".": "The name should start with upper letter, lower letter or chinese, and be a string of 1 to 128, characters can only contain \"0-9, a-z, A-Z, \"-'_()[].:^\".",
"The name should start with upper letter, lower letter or chinese, and be a string of 1 to 128, characters can only contain \"0-9, a-z, A-Z, \"-'_.\".": "The name should start with upper letter, lower letter or chinese, and be a string of 1 to 128, characters can only contain \"0-9, a-z, A-Z, \"-'_.\".", "The name should start with upper letter, lower letter or chinese, and be a string of 1 to 128, characters can only contain \"0-9, a-z, A-Z, \"-'_.\".": "The name should start with upper letter, lower letter or chinese, and be a string of 1 to 128, characters can only contain \"0-9, a-z, A-Z, \"-'_.\".",
"The name should start with upper letter, lower letter or chinese, and be a string of 1 to 64, characters can only contain \"0-9, a-z, A-Z, \"-'_()[].^\".": "The name should start with upper letter, lower letter or chinese, and be a string of 1 to 64, characters can only contain \"0-9, a-z, A-Z, \"-'_()[].^\".", "The name should start with upper letter, lower letter or chinese, and be a string of 1 to 64, characters can only contain \"0-9, a-z, A-Z, \"-'_()[].^\".": "The name should start with upper letter, lower letter or chinese, and be a string of 1 to 64, characters can only contain \"0-9, a-z, A-Z, \"-'_()[].^\".",
"The name should start with upper letter, lower letter or chinese, and be a string of 3 to 63, characters can only contain \"0-9, a-z, A-Z, chinese, -, .\".": "The name should start with upper letter, lower letter or chinese, and be a string of 3 to 63, characters can only contain \"0-9, a-z, A-Z, chinese, -, .\".",
"The name should start with upper letter, lower letter, and be a string of 1 to 128, characters can only contain \"0-9, a-z, A-Z, -, _\".": "The name should start with upper letter, lower letter, and be a string of 1 to 128, characters can only contain \"0-9, a-z, A-Z, -, _\".", "The name should start with upper letter, lower letter, and be a string of 1 to 128, characters can only contain \"0-9, a-z, A-Z, -, _\".": "The name should start with upper letter, lower letter, and be a string of 1 to 128, characters can only contain \"0-9, a-z, A-Z, -, _\".",
"The name should start with upper letter, lower letter, and be a string of 2 to 255, characters can only contain \"0-9, a-z, A-Z, -, ., _\".": "The name should start with upper letter, lower letter, and be a string of 2 to 255, characters can only contain \"0-9, a-z, A-Z, -, ., _\".", "The name should start with upper letter, lower letter, and be a string of 2 to 255, characters can only contain \"0-9, a-z, A-Z, -, ., _\".": "The name should start with upper letter, lower letter, and be a string of 2 to 255, characters can only contain \"0-9, a-z, A-Z, -, ., _\".",
"The name should start with upper letter, lower letter, and be a string of 3 to 63, characters can only contain \"0-9, a-z, A-Z, -\".": "The name should start with upper letter, lower letter, and be a string of 3 to 63, characters can only contain \"0-9, a-z, A-Z, -\".", "The name should start with upper letter, lower letter, and be a string of 3 to 63, characters can only contain \"0-9, a-z, A-Z, -\".": "The name should start with upper letter, lower letter, and be a string of 3 to 63, characters can only contain \"0-9, a-z, A-Z, -\".",
@ -1802,8 +1859,10 @@
"There are resources that cannot {action} in the selected resources, such as:": "There are resources that cannot {action} in the selected resources, such as:", "There are resources that cannot {action} in the selected resources, such as:": "There are resources that cannot {action} in the selected resources, such as:",
"There are resources that cannot {action} in the selected resources.": "There are resources that cannot {action} in the selected resources.", "There are resources that cannot {action} in the selected resources.": "There are resources that cannot {action} in the selected resources.",
"There are resources under the project and cannot be deleted.": "There are resources under the project and cannot be deleted.", "There are resources under the project and cannot be deleted.": "There are resources under the project and cannot be deleted.",
"There is currently no file to paste.": "There is currently no file to paste.",
"This service will automatically query the configuration (CPU, memory, etc.) and mac address of the physical machine, and the ironic-inspector service will automatically register this information in the node information.": "This service will automatically query the configuration (CPU, memory, etc.) and mac address of the physical machine, and the ironic-inspector service will automatically register this information in the node information.", "This service will automatically query the configuration (CPU, memory, etc.) and mac address of the physical machine, and the ironic-inspector service will automatically register this information in the node information.": "This service will automatically query the configuration (CPU, memory, etc.) and mac address of the physical machine, and the ironic-inspector service will automatically register this information in the node information.",
"This will delete all child objects of the load balancer.": "This will delete all child objects of the load balancer.", "This will delete all child objects of the load balancer.": "This will delete all child objects of the load balancer.",
"Time Interval: ": "Time Interval: ",
"Timeout(Mininte)": "Timeout(Mininte)", "Timeout(Mininte)": "Timeout(Mininte)",
"Timeout(s)": "Timeout(s)", "Timeout(s)": "Timeout(s)",
"To open": "To open", "To open": "To open",
@ -1844,6 +1903,7 @@
"Unable to get {name}.": "Unable to get {name}.", "Unable to get {name}.": "Unable to get {name}.",
"Unable to get {title}, please go back to ": "Unable to get {title}, please go back to ", "Unable to get {title}, please go back to ": "Unable to get {title}, please go back to ",
"Unable to get {title}, please go to ": "Unable to get {title}, please go to ", "Unable to get {title}, please go to ": "Unable to get {title}, please go to ",
"Unable to paste into the same folder.": "Unable to paste into the same folder.",
"Unable to render form": "Unable to render form", "Unable to render form": "Unable to render form",
"Unable to {action} {name}.": "Unable to {action} {name}.", "Unable to {action} {name}.": "Unable to {action} {name}.",
"Unable to {action}, because : {reason}, instance: {name}.": "Unable to {action}, because : {reason}, instance: {name}.", "Unable to {action}, because : {reason}, instance: {name}.": "Unable to {action}, because : {reason}, instance: {name}.",
@ -1869,6 +1929,7 @@
"Unshelving": "Unshelving", "Unshelving": "Unshelving",
"Unused": "Unused", "Unused": "Unused",
"Up": "Up", "Up": "Up",
"Update Access": "Update Access",
"Update At": "Update At", "Update At": "Update At",
"Update Complete": "Update Complete", "Update Complete": "Update Complete",
"Update Failed": "Update Failed", "Update Failed": "Update Failed",
@ -1880,6 +1941,7 @@
"Updated At": "Updated At", "Updated At": "Updated At",
"Updating": "Updating", "Updating": "Updating",
"Updating Password": "Updating Password", "Updating Password": "Updating Password",
"Upload File": "Upload File",
"Upload progress": "Upload progress", "Upload progress": "Upload progress",
"Uploading": "Uploading", "Uploading": "Uploading",
"Uruguay": "Uruguay", "Uruguay": "Uruguay",
@ -1920,6 +1982,8 @@
"VCPU (Core)": "VCPU (Core)", "VCPU (Core)": "VCPU (Core)",
"VCPUs": "VCPUs", "VCPUs": "VCPUs",
"VDI - VirtualBox compatible image format": "VDI - VirtualBox compatible image format", "VDI - VirtualBox compatible image format": "VDI - VirtualBox compatible image format",
"VGPU": "VGPU",
"VGPU (Core)": "VGPU (Core)",
"VHD - VirtualPC compatible image format": "VHD - VirtualPC compatible image format", "VHD - VirtualPC compatible image format": "VHD - VirtualPC compatible image format",
"VIF Details": "VIF Details", "VIF Details": "VIF Details",
"VIF Type": "VIF Type", "VIF Type": "VIF Type",
@ -1969,6 +2033,7 @@
"Volume Type Detail": "Volume Type Detail", "Volume Type Detail": "Volume Type Detail",
"Wallis And Futuna Islands": "Wallis And Futuna Islands", "Wallis And Futuna Islands": "Wallis And Futuna Islands",
"Warn": "Warn", "Warn": "Warn",
"Warning": "Warning",
"Weight": "Weight", "Weight": "Weight",
"Weights": "Weights", "Weights": "Weights",
"Welcome": "Welcome", "Welcome": "Welcome",
@ -2016,6 +2081,8 @@
"backups": "backups", "backups": "backups",
"bare metal node": "bare metal node", "bare metal node": "bare metal node",
"bare metal nodes": "bare metal nodes", "bare metal nodes": "bare metal nodes",
"be copied": "be copied",
"be cut": "be cut",
"be deleted": "be deleted", "be deleted": "be deleted",
"be rebooted": "be rebooted", "be rebooted": "be rebooted",
"be recovered": "be recovered", "be recovered": "be recovered",
@ -2029,6 +2096,8 @@
"compute services": "compute services", "compute services": "compute services",
"confirm resize or migrate": "confirm resize or migrate", "confirm resize or migrate": "confirm resize or migrate",
"connect subnet": "connect subnet", "connect subnet": "connect subnet",
"container objects": "container objects",
"containers": "containers",
"create DSCP marking rule": "create DSCP marking rule", "create DSCP marking rule": "create DSCP marking rule",
"create a new network/subnet": "create a new network/subnet", "create a new network/subnet": "create a new network/subnet",
"create a new security group": "create a new security group", "create a new security group": "create a new security group",
@ -2059,6 +2128,7 @@
"delete backup": "delete backup", "delete backup": "delete backup",
"delete bandwidth egress rules": "delete bandwidth egress rules", "delete bandwidth egress rules": "delete bandwidth egress rules",
"delete bandwidth ingress rules": "delete bandwidth ingress rules", "delete bandwidth ingress rules": "delete bandwidth ingress rules",
"delete container": "delete container",
"delete default pool": "delete default pool", "delete default pool": "delete default pool",
"delete domain": "delete domain", "delete domain": "delete domain",
"delete dscp marking rules": "delete dscp marking rules", "delete dscp marking rules": "delete dscp marking rules",
@ -2147,6 +2217,7 @@
"neutron agents": "neutron agents", "neutron agents": "neutron agents",
"online resize": "online resize", "online resize": "online resize",
"open external gateway": "open external gateway", "open external gateway": "open external gateway",
"paste files to folder": "paste files to folder",
"pause instance": "pause instance", "pause instance": "pause instance",
"phone": "phone", "phone": "phone",
"please select network": "please select network", "please select network": "please select network",
@ -2197,6 +2268,7 @@
"subnets": "subnets", "subnets": "subnets",
"suspend instance": "suspend instance", "suspend instance": "suspend instance",
"the Republic of Abkhazia": "the Republic of Abkhazia", "the Republic of Abkhazia": "the Republic of Abkhazia",
"the folder is not empty": "the folder is not empty",
"the policy is in use": "the policy is in use", "the policy is in use": "the policy is in use",
"the router has connected subnet": "the router has connected subnet", "the router has connected subnet": "the router has connected subnet",
"the vpn gateway is in use": "the vpn gateway is in use", "the vpn gateway is in use": "the vpn gateway is in use",

View File

@ -7,15 +7,22 @@
"1. The name of the custom resource class property should start with CUSTOM_, can only contain uppercase letters A ~ Z, numbers 0 ~ 9 or underscores, and the length should not exceed 255 characters (for example: CUSTOM_BAREMETAL_SMALL).": "1. 自定义资源属性的命名应该以 CUSTOM_ 开头、只能包含大写字母A ~ Z、数字0 ~ 9或下划线、长度不超过255个字符比如CUSTOM_BAREMETAL_SMALL。", "1. The name of the custom resource class property should start with CUSTOM_, can only contain uppercase letters A ~ Z, numbers 0 ~ 9 or underscores, and the length should not exceed 255 characters (for example: CUSTOM_BAREMETAL_SMALL).": "1. 自定义资源属性的命名应该以 CUSTOM_ 开头、只能包含大写字母A ~ Z、数字0 ~ 9或下划线、长度不超过255个字符比如CUSTOM_BAREMETAL_SMALL。",
"1. The name of the trait should start with CUSTOM_, can only contain uppercase letters A ~ Z, numbers 0 ~ 9 or underscores, and the length should not exceed 255 characters (for example: CUSTOM_TRAIT1).": "1. 特性的命名应该以 CUSTOM_ 开头、只能包含大写字母A ~ Z、数字0 ~ 9或下划线、长度不超过255个字符比如CUSTOM_TRAIT1。", "1. The name of the trait should start with CUSTOM_, can only contain uppercase letters A ~ Z, numbers 0 ~ 9 or underscores, and the length should not exceed 255 characters (for example: CUSTOM_TRAIT1).": "1. 特性的命名应该以 CUSTOM_ 开头、只能包含大写字母A ~ Z、数字0 ~ 9或下划线、长度不超过255个字符比如CUSTOM_TRAIT1。",
"1. The volume associated with the backup is available.": "1. 备份关联的云硬盘处于可用状态。", "1. The volume associated with the backup is available.": "1. 备份关联的云硬盘处于可用状态。",
"10s": "",
"1D": "",
"1H": "",
"1min": "",
"2. The trait of the scheduled node needs to correspond to the trait of the flavor used by the ironic instance; by injecting the necessary traits into the ironic instance, the computing service will only schedule the instance to the bare metal node with all the necessary traits (for example, the ironic instance which use the flavor that has CUSTOM_TRAIT1 as a necessary trait, can be scheduled to the node which has the trait of CUSTOM_TRAIT1).": "2. 被调度节点的特性需要与裸机实例使用的云主机类型的特性对应;通过给裸机实例注入必需特性,计算服务将只调度实例到具有所有必需特性的裸金属节点(比如:调度节点的有 CUSTOM_TRAIT1 特性, 云主机类型添加CUSTOM_TRAIT1为必要特性可以调度到此节点。", "2. The trait of the scheduled node needs to correspond to the trait of the flavor used by the ironic instance; by injecting the necessary traits into the ironic instance, the computing service will only schedule the instance to the bare metal node with all the necessary traits (for example, the ironic instance which use the flavor that has CUSTOM_TRAIT1 as a necessary trait, can be scheduled to the node which has the trait of CUSTOM_TRAIT1).": "2. 被调度节点的特性需要与裸机实例使用的云主机类型的特性对应;通过给裸机实例注入必需特性,计算服务将只调度实例到具有所有必需特性的裸金属节点(比如:调度节点的有 CUSTOM_TRAIT1 特性, 云主机类型添加CUSTOM_TRAIT1为必要特性可以调度到此节点。",
"2. The volume associated with the backup has been mounted, and the instance is shut down.": "2. 备份关联的云硬盘已被挂载,且云主机处于关机状态。", "2. The volume associated with the backup has been mounted, and the instance is shut down.": "2. 备份关联的云硬盘已被挂载,且云主机处于关机状态。",
"2. To ensure the integrity of the data, it is recommended that you suspend the write operation of all files when creating a backup.": "2. 为了保证数据的完整性,建议您在创建备份时暂停所有文件的写操作。", "2. To ensure the integrity of the data, it is recommended that you suspend the write operation of all files when creating a backup.": "2. 为了保证数据的完整性,建议您在创建备份时暂停所有文件的写操作。",
"2. You can customize the resource class name of the flavor, but it needs to correspond to the resource class of the scheduled node (for example, the resource class name of the scheduling node is baremetal.with-GPU, and the custom resource class name of the flavor is CUSTOM_BAREMETAL_WITH_GPU=1).": "2. 你可以自定义云主机类型的资源类名称,但需要与被调度节点的资源类对应;(比如:调度节点的资源类名称为 baremetal.with-GPU云主机类型的自定义资源类名称为CUSTOM_BAREMETAL_WITH_GPU。", "2. You can customize the resource class name of the flavor, but it needs to correspond to the resource class of the scheduled node (for example, the resource class name of the scheduling node is baremetal.with-GPU, and the custom resource class name of the flavor is CUSTOM_BAREMETAL_WITH_GPU=1).": "2. 你可以自定义云主机类型的资源类名称,但需要与被调度节点的资源类对应;(比如:调度节点的资源类名称为 baremetal.with-GPU云主机类型的自定义资源类名称为CUSTOM_BAREMETAL_WITH_GPU。",
"5min": "",
"8 to 16 characters, at least one uppercase letter, one lowercase letter, one number and one special character.": "8个到16个字符至少一个大写字母一个小写字母一个数字和一个特殊字符。", "8 to 16 characters, at least one uppercase letter, one lowercase letter, one number and one special character.": "8个到16个字符至少一个大写字母一个小写字母一个数字和一个特殊字符。",
"8 to 16 characters, at least one uppercase letter, one lowercase letter, one number.": "8个到16个字符至少一个大写字母一个小写字母一个数字。", "8 to 16 characters, at least one uppercase letter, one lowercase letter, one number.": "8个到16个字符至少一个大写字母一个小写字母一个数字。",
"A DNAT rule has been created for this port of this IP, please choose another port.": "此IP的这个端口已经创建了DNAT规则请选择另一个端口。", "A DNAT rule has been created for this port of this IP, please choose another port.": "此IP的这个端口已经创建了DNAT规则请选择另一个端口。",
"A container with the same name already exists": "已存在同名容器",
"A dynamic scheduling algorithm that estimates the server load based on the number of currently active connections. The system allocates new connection requests to the server with the least number of current connections. Commonly used for long connection services, such as database connections and other services.": "通过当前活跃的连接数来估计服务器负载情况的一种动态调度算法,系统把新的连接请求分配给当前连接数目最少的服务器。常用于长连接服务,例如数据库连接等服务。", "A dynamic scheduling algorithm that estimates the server load based on the number of currently active connections. The system allocates new connection requests to the server with the least number of current connections. Commonly used for long connection services, such as database connections and other services.": "通过当前活跃的连接数来估计服务器负载情况的一种动态调度算法,系统把新的连接请求分配给当前连接数目最少的服务器。常用于长连接服务,例如数据库连接等服务。",
"A host aggregate can be associated with at most one AZ. Once the association is established, the AZ cannot be disassociated.": "一个主机集合最多可以与一个AZ建立关联一旦建立了关联无法再取消关联AZ。", "A host aggregate can be associated with at most one AZ. Once the association is established, the AZ cannot be disassociated.": "一个主机集合最多可以与一个AZ建立关联一旦建立了关联无法再取消关联AZ。",
"A public container will allow anyone to use the objects in your container through a public URL.": "一个公有容器会允许任何人通过公共 URL 去使用您容器里面的对象。",
"A snapshot is an image which preserves the disk state of a running instance, which can be used to start a new instance.": "云主机当前状态的磁盘数据保存,创建镜像文件,以备将来启动新的云主机使用。", "A snapshot is an image which preserves the disk state of a running instance, which can be used to start a new instance.": "云主机当前状态的磁盘数据保存,创建镜像文件,以备将来启动新的云主机使用。",
"A template is a YAML file that contains configuration information, please enter the correct format.": "模板是包含配置信息的YAML文件 请输入正确的格式。", "A template is a YAML file that contains configuration information, please enter the correct format.": "模板是包含配置信息的YAML文件 请输入正确的格式。",
"A template is a YAML file that contains configuration information.": "模板是包含配置信息的YAML文件。", "A template is a YAML file that contains configuration information.": "模板是包含配置信息的YAML文件。",
@ -89,6 +96,7 @@
"Allocation Pools": "分配地址池", "Allocation Pools": "分配地址池",
"Allowed Address Pair": "可用地址对", "Allowed Address Pair": "可用地址对",
"American Samoa": "萨摩亚", "American Samoa": "萨摩亚",
"An object with the same name already exists": "已存在同名对象",
"Andorra": "安道尔共和国", "Andorra": "安道尔共和国",
"Angola": "安哥拉", "Angola": "安哥拉",
"Anguilla": "安圭拉岛", "Anguilla": "安圭拉岛",
@ -236,6 +244,7 @@
"CPU value is { cpu }, NUMA CPU value is { totalCpu }, need to be equal. ": "CPU核数是 { cpu }NUMA节点的CPU核数是{ totalCpu },需要一致。", "CPU value is { cpu }, NUMA CPU value is { totalCpu }, need to be equal. ": "CPU核数是 { cpu }NUMA节点的CPU核数是{ totalCpu },需要一致。",
"CPU(Core)": "CPU核数", "CPU(Core)": "CPU核数",
"CPU/memory can only be increased or expanded online, and cannot be decreased or reduced online.": "CPU/内存均只能在线增或扩展,不能在线减少或缩小。", "CPU/memory can only be increased or expanded online, and cannot be decreased or reduced online.": "CPU/内存均只能在线增或扩展,不能在线减少或缩小。",
"Cache Service": "",
"Cameroon": "喀麦隆", "Cameroon": "喀麦隆",
"Can add { number } {name}": "还可添加 { number } {name}", "Can add { number } {name}": "还可添加 { number } {name}",
"Canada": "加拿大", "Canada": "加拿大",
@ -275,6 +284,7 @@
"Clean Wait": "等待清除", "Clean Wait": "等待清除",
"Cleaning": "清除中", "Cleaning": "清除中",
"Clear Gateway": "", "Clear Gateway": "",
"Click To View": "点击查看",
"Click here for filters.": "筛选", "Click here for filters.": "筛选",
"Click to Upload": "点击上传文件", "Click to Upload": "点击上传文件",
"Click to show detail": "点击查看详情", "Click to show detail": "点击查看详情",
@ -330,10 +340,15 @@
"Console Interface": "Console接口", "Console Interface": "Console接口",
"Consumer": "消费者", "Consumer": "消费者",
"Container": "容器集群", "Container": "容器集群",
"Container Detail": "容器详情",
"Container Format": "容器格式", "Container Format": "容器格式",
"Container Name": "容器名称",
"Content": "内容", "Content": "内容",
"Content Type": "内容类型",
"Control Location": "控制端", "Control Location": "控制端",
"Cook Islands": "库克群岛", "Cook Islands": "库克群岛",
"Copy": "复制",
"Copy File": "复制文件",
"CoreOS": "", "CoreOS": "",
"Costa Rica": "哥斯达黎加", "Costa Rica": "哥斯达黎加",
"Cote D'Ivoire": "科特迪瓦", "Cote D'Ivoire": "科特迪瓦",
@ -346,6 +361,7 @@
"Create Bandwidth Limit Rule": "创建带宽限制规则", "Create Bandwidth Limit Rule": "创建带宽限制规则",
"Create Bare Metal Node": "创建裸机节点", "Create Bare Metal Node": "创建裸机节点",
"Create Complete": "创建完成", "Create Complete": "创建完成",
"Create Container": "创建容器",
"Create DNAT Rule": "创建DNAT规则", "Create DNAT Rule": "创建DNAT规则",
"Create DNAT rule": "创建DNAT规则", "Create DNAT rule": "创建DNAT规则",
"Create DSCP Marking Rule": "创建DSCP标记规则", "Create DSCP Marking Rule": "创建DSCP标记规则",
@ -355,6 +371,7 @@
"Create Extra Specs": "创建额外规格", "Create Extra Specs": "创建额外规格",
"Create Failed": "创建失败", "Create Failed": "创建失败",
"Create Flavor": "创建云主机类型", "Create Flavor": "创建云主机类型",
"Create Folder": "创建文件夹",
"Create Host Aggregate": "创建主机集合", "Create Host Aggregate": "创建主机集合",
"Create IPsec Site Connection": "创建IPsec站点连接", "Create IPsec Site Connection": "创建IPsec站点连接",
"Create Image": "创建镜像", "Create Image": "创建镜像",
@ -419,6 +436,7 @@
"Current Host": "当前主机", "Current Host": "当前主机",
"Current Interface": "当前接口", "Current Interface": "当前接口",
"Current Password": "原密码", "Current Password": "原密码",
"Current Path: ": "当前路径:",
"Current Project": "当前项目", "Current Project": "当前项目",
"Current Project Image": "本项目镜像", "Current Project Image": "本项目镜像",
"Current Project Network": "当前项目网络", "Current Project Network": "当前项目网络",
@ -433,6 +451,8 @@
"Custom TCP Rule": "定制TCP规则", "Custom TCP Rule": "定制TCP规则",
"Custom Trait": "自定义特性", "Custom Trait": "自定义特性",
"Custom UDP Rule": "定制UDP规则", "Custom UDP Rule": "定制UDP规则",
"Cut": "剪切",
"Cut File": "剪切文件",
"Cyprus": "塞浦路斯", "Cyprus": "塞浦路斯",
"Czech Republic": "捷克", "Czech Republic": "捷克",
"DCCP": "", "DCCP": "",
@ -455,6 +475,7 @@
"Data Source Type": "数据源类型", "Data Source Type": "数据源类型",
"Database": "数据库", "Database": "数据库",
"Database Instance": "数据库实例", "Database Instance": "数据库实例",
"Database Service": "",
"Deactivated": "已取消激活", "Deactivated": "已取消激活",
"Debian": "", "Debian": "",
"Dedicated": "专用", "Dedicated": "专用",
@ -468,6 +489,7 @@
"Delete Bandwidth Egress Rules": "删除带宽出方向限制", "Delete Bandwidth Egress Rules": "删除带宽出方向限制",
"Delete Bandwidth Ingress Rules": "删除带宽入方向限制", "Delete Bandwidth Ingress Rules": "删除带宽入方向限制",
"Delete Complete": "删除完成", "Delete Complete": "删除完成",
"Delete Container": "删除容器",
"Delete DNAT Rule": "删除DNAT规则", "Delete DNAT Rule": "删除DNAT规则",
"Delete DSCP Marking Rules": "删除DSCP标记规则", "Delete DSCP Marking Rules": "删除DSCP标记规则",
"Delete Default Pool": "删除资源池", "Delete Default Pool": "删除资源池",
@ -475,7 +497,9 @@
"Delete Encryption": "删除加密", "Delete Encryption": "删除加密",
"Delete Extra Specs": "删除额外规格", "Delete Extra Specs": "删除额外规格",
"Delete Failed": "删除失败", "Delete Failed": "删除失败",
"Delete File": "删除文件",
"Delete Flavor": "删除云主机类型", "Delete Flavor": "删除云主机类型",
"Delete Folder": "删除文件夹",
"Delete Group": "删除用户组", "Delete Group": "删除用户组",
"Delete Host Aggregate": "删除主机集合", "Delete Host Aggregate": "删除主机集合",
"Delete IPsec Site Connection": "删除IPsec站点连接", "Delete IPsec Site Connection": "删除IPsec站点连接",
@ -523,6 +547,7 @@
"Deploying": "部署中", "Deploying": "部署中",
"Deployment Parameters": "部署参数", "Deployment Parameters": "部署参数",
"Description": "描述", "Description": "描述",
"Dest Folder": "目标文件夹",
"Destination CIDR": "目的网络地址", "Destination CIDR": "目的网络地址",
"Detach": "解绑", "Detach": "解绑",
"Detach Instance": "从云主机解绑", "Detach Instance": "从云主机解绑",
@ -532,6 +557,7 @@
"Detach interface": "卸载网卡", "Detach interface": "卸载网卡",
"Detaching": "卸载中", "Detaching": "卸载中",
"Detail": "详情", "Detail": "详情",
"Detail Info": "详情信息",
"Details": "详情", "Details": "详情",
"Device ID": "设备ID", "Device ID": "设备ID",
"Device Owner": "设备所属者", "Device Owner": "设备所属者",
@ -558,6 +584,7 @@
"Disk size is limited by the min disk of flavor, image, etc.": "根磁盘大小受云主机类型、镜像等的最小磁盘限制。", "Disk size is limited by the min disk of flavor, image, etc.": "根磁盘大小受云主机类型、镜像等的最小磁盘限制。",
"Djibouti": "吉布提", "Djibouti": "吉布提",
"Do Build And Run Instance": "构建并运行实例", "Do Build And Run Instance": "构建并运行实例",
"Do HH:mm": "",
"Do not reset the normally mounted volume to the \"available\"、\"maintenance\" or \"error\" status. The reset state does not remove the volume from the instance. If you need to remove the volume from the instance, please go to the console of the corresponding project and use the \"detach\" operation.": "请勿将正常的挂载中的云硬盘重置为“可用”、“维护”或”错误“状态。重置状态并不会将云硬盘从云主机上卸载下来。如果您需要将云硬盘从云主机上移除,请进入相应项目的控制台使用“解绑”操作。", "Do not reset the normally mounted volume to the \"available\"、\"maintenance\" or \"error\" status. The reset state does not remove the volume from the instance. If you need to remove the volume from the instance, please go to the console of the corresponding project and use the \"detach\" operation.": "请勿将正常的挂载中的云硬盘重置为“可用”、“维护”或”错误“状态。重置状态并不会将云硬盘从云主机上卸载下来。如果您需要将云硬盘从云主机上移除,请进入相应项目的控制台使用“解绑”操作。",
"Do not set with a backend": "不设置后端", "Do not set with a backend": "不设置后端",
"Domain": "域", "Domain": "域",
@ -569,6 +596,7 @@
"Domains": "域", "Domains": "域",
"Dominica": "多米尼克国", "Dominica": "多米尼克国",
"Down": "停止", "Down": "停止",
"Download File": "下载文件",
"Download all data": "下载所有数据", "Download all data": "下载所有数据",
"Download canceled!": "下载已取消!", "Download canceled!": "下载已取消!",
"Download current data": "下载当前数据", "Download current data": "下载当前数据",
@ -623,6 +651,7 @@
"Edit host aggregate": "编辑主机集合", "Edit host aggregate": "编辑主机集合",
"Edit metadata": "编辑元数据", "Edit metadata": "编辑元数据",
"Edit quota": "编辑配额", "Edit quota": "编辑配额",
"Editing only changes the content of the file, not the file name.": "编辑只改变文件内容,而不会改变文件名称。",
"Effective Mode": "生效模式", "Effective Mode": "生效模式",
"Effective mode after configuration changes": "配置变更后的生效模式", "Effective mode after configuration changes": "配置变更后的生效模式",
"Egress": "出口", "Egress": "出口",
@ -699,6 +728,7 @@
"Fiji": "斐济", "Fiji": "斐济",
"File": "文件", "File": "文件",
"Filename": "文件名", "Filename": "文件名",
"Files: {names}": "文件:{names}",
"Fill In The Parameters": "参数填写", "Fill In The Parameters": "参数填写",
"Filter Project": "筛选项目", "Filter Project": "筛选项目",
"Fingerprint": "指纹", "Fingerprint": "指纹",
@ -720,6 +750,8 @@
"Floating Ip Address": "浮动IP地址", "Floating Ip Address": "浮动IP地址",
"Floating Ip Detail": "浮动IP详情", "Floating Ip Detail": "浮动IP详情",
"Floating ip has already been associate, Please check Force release": "浮动IP已经被关联使用请选择强制释放", "Floating ip has already been associate, Please check Force release": "浮动IP已经被关联使用请选择强制释放",
"Folder Detail": "文件夹详情",
"Folder Name": "文件夹名称",
"Fontend": "前端", "Fontend": "前端",
"For GPU type, you need to install GPU drivers in the instance operating system.": "对于GPU类型的云主机您需要在云主机操作系统中安装GPU驱动等。", "For GPU type, you need to install GPU drivers in the instance operating system.": "对于GPU类型的云主机您需要在云主机操作系统中安装GPU驱动等。",
"For GRE networks, valid segmentation IDs are 1 to 4294967295": "对于GRE网络有效的段ID范围是从1到4294967295", "For GRE networks, valid segmentation IDs are 1 to 4294967295": "对于GRE网络有效的段ID范围是从1到4294967295",
@ -781,6 +813,7 @@
"Haiti": "海地", "Haiti": "海地",
"Hard Reboot": "硬重启", "Hard Reboot": "硬重启",
"Hard Rebooting": "硬重启中", "Hard Rebooting": "硬重启中",
"Hash": "",
"Health Monitor Delay": "检查间隔(秒)", "Health Monitor Delay": "检查间隔(秒)",
"Health Monitor Detail": "健康检查器详情", "Health Monitor Detail": "健康检查器详情",
"Health Monitor Max Retries": "最大重试次数", "Health Monitor Max Retries": "最大重试次数",
@ -789,6 +822,7 @@
"Health Monitor Type": "健康检查器类型", "Health Monitor Type": "健康检查器类型",
"HealthMonitor": "健康检查器", "HealthMonitor": "健康检查器",
"HealthMonitor Type": "健康检查类型", "HealthMonitor Type": "健康检查类型",
"Healthy": "",
"Heartbeat Timestamp": "心跳时间戳", "Heartbeat Timestamp": "心跳时间戳",
"Heterogeneous Computing": "异构计算", "Heterogeneous Computing": "异构计算",
"Hide Advanced Options": "隐藏高级选项", "Hide Advanced Options": "隐藏高级选项",
@ -1010,6 +1044,10 @@
"Lao People's Democratic Republic": "老挝", "Lao People's Democratic Republic": "老挝",
"Large": "大", "Large": "大",
"Large(Optimal performance)": "大(性能最优)", "Large(Optimal performance)": "大(性能最优)",
"Last 2 Weeks": "",
"Last 7 Days": "",
"Last Day": "",
"Last Hour": "",
"Last Updated": "最近更新", "Last Updated": "最近更新",
"Latvia": "拉脱维亚", "Latvia": "拉脱维亚",
"Leave Maintenance Mode": "退出维护模式", "Leave Maintenance Mode": "退出维护模式",
@ -1111,6 +1149,7 @@
"Memory Page": "内存页", "Memory Page": "内存页",
"Memory usage Num (GB": "内存用量 (GB)", "Memory usage Num (GB": "内存用量 (GB)",
"Message": "消息", "Message": "消息",
"Message Queue Service": "",
"Metadata": "元数据", "Metadata": "元数据",
"Metadata Definitions": "元数据定义", "Metadata Definitions": "元数据定义",
"Metadata Detail": "元数据详情", "Metadata Detail": "元数据详情",
@ -1209,15 +1248,19 @@
"Not yet bound": "尚未绑定", "Not yet bound": "尚未绑定",
"Not yet selected": "尚未选择", "Not yet selected": "尚未选择",
"Note: Are you sure you need to modify the volume type?": "注意:确定需要修改云硬盘类型?", "Note: Are you sure you need to modify the volume type?": "注意:确定需要修改云硬盘类型?",
"Note: Please consider the container name carefully since it couldn't be changed after created.": "注意:为容器取名需谨慎,因为创建后不可修改。",
"Note: The security group you use will act on all virtual adapters of the instance.": "注:您所用的安全组将作用于云主机的全部虚拟网卡。", "Note: The security group you use will act on all virtual adapters of the instance.": "注:您所用的安全组将作用于云主机的全部虚拟网卡。",
"Number Of Ports": "端口数量", "Number Of Ports": "端口数量",
"Number of GPU": "GPU数量", "Number of GPU": "GPU数量",
"Number of Usb Controller": "USB控制器数量", "Number of Usb Controller": "USB控制器数量",
"OK": "",
"OS": "操作系统", "OS": "操作系统",
"OS Admin": "镜像默认用户", "OS Admin": "镜像默认用户",
"OS Disk": "系统盘", "OS Disk": "系统盘",
"OS Version": "系统版本", "OS Version": "系统版本",
"OSPF": "", "OSPF": "",
"Object Count": "对象数量",
"Object Storage": "对象存储",
"Off": "关", "Off": "关",
"Offline": "离线", "Offline": "离线",
"Oman": "阿曼", "Oman": "阿曼",
@ -1239,6 +1282,7 @@
"Orchestration Services": "编排服务", "Orchestration Services": "编排服务",
"Orchestration information": "编排信息", "Orchestration information": "编排信息",
"Orchestration service:": "编排服务(heat):", "Orchestration service:": "编排服务(heat):",
"Origin File Name": "原始文件名称",
"Original Password": "原始密码", "Original Password": "原始密码",
"Other Protocol": "其他协议", "Other Protocol": "其他协议",
"Others": "其他", "Others": "其他",
@ -1267,6 +1311,8 @@
"Password Type": "密码类型", "Password Type": "密码类型",
"Password changed successfully, please log in again.": "密码修改成功,请重新登录。", "Password changed successfully, please log in again.": "密码修改成功,请重新登录。",
"Password must be the same with confirm password.": "密码和确认密码必须一致。", "Password must be the same with confirm password.": "密码和确认密码必须一致。",
"Paste": "粘贴",
"Paste File": "粘贴文件",
"Pause": "暂停", "Pause": "暂停",
"Pause Instance": "暂停云主机", "Pause Instance": "暂停云主机",
"Paused": "已暂停", "Paused": "已暂停",
@ -1313,6 +1359,7 @@
"Please input IPv4 or IPv6 cidr": "请输入IPv4或IPv6网段地址", "Please input IPv4 or IPv6 cidr": "请输入IPv4或IPv6网段地址",
"Please input IPv4 or IPv6 cidr, (e.g. 192.168.0.0/24, 2001:DB8::/48)": "请输入IPv4或IPv6网段地址192.168.0.0/24, 2001:DB8::/48", "Please input IPv4 or IPv6 cidr, (e.g. 192.168.0.0/24, 2001:DB8::/48)": "请输入IPv4或IPv6网段地址192.168.0.0/24, 2001:DB8::/48",
"Please input a valid ip!": "请输入一个有效的IP", "Please input a valid ip!": "请输入一个有效的IP",
"Please input at least 2 characters.": "请输入至少2个字符",
"Please input auth key": "请输入密钥", "Please input auth key": "请输入密钥",
"Please input cipher": "请输入cipher", "Please input cipher": "请输入cipher",
"Please input file name": "请输入文件名称", "Please input file name": "请输入文件名称",
@ -1356,6 +1403,7 @@
"Please select!": "请选择!", "Please select!": "请选择!",
"Please set CPU && Ram first.": "请先设置CPU、内存。", "Please set CPU && Ram first.": "请先设置CPU、内存。",
"Please set MUNA": "请设置NUMA节点", "Please set MUNA": "请设置NUMA节点",
"Please upload files smaller than { size }G on the page. It is recommended to upload files over { size }G using API.": "页面请上传小于{ size }G的文件超过{ size }G的文件建议使用API上传。",
"Pleasse input a valid ip!": "请输入正确的IP地址", "Pleasse input a valid ip!": "请输入正确的IP地址",
"Pleasse select a network!": "请选择网络!", "Pleasse select a network!": "请选择网络!",
"Pleasse select a subnet!": "请选择子网!", "Pleasse select a subnet!": "请选择子网!",
@ -1424,6 +1472,7 @@
"Provision State": "配置状态", "Provision State": "配置状态",
"Provisioning Status": "配置状态", "Provisioning Status": "配置状态",
"Public": "公有", "Public": "公有",
"Public Access": "公开访问",
"Public Image": "公有镜像", "Public Image": "公有镜像",
"Public Key": "公钥", "Public Key": "公钥",
"Puerto Rico": "波多黎各", "Puerto Rico": "波多黎各",
@ -1484,6 +1533,8 @@
"Remove": "移除", "Remove": "移除",
"Remove Network": "移除网络", "Remove Network": "移除网络",
"Remove Router": "移除路由器", "Remove Router": "移除路由器",
"Rename": "重命名",
"Rename is to copy the current file to the new file address and delete the current file, which will affect the creation time of the file.": "重命名是把当前文件复制到新文件地址,并删除当前文件,会影响文件的创建时间。",
"Republic Of The Congo": "刚果共和国", "Republic Of The Congo": "刚果共和国",
"Request ID": "请求ID", "Request ID": "请求ID",
"Require": "强制", "Require": "强制",
@ -1535,6 +1586,7 @@
"Rollback In Progress": "回滚中", "Rollback In Progress": "回滚中",
"Romania": "罗马尼亚", "Romania": "罗马尼亚",
"Root Disk": "系统盘", "Root Disk": "系统盘",
"Root directory": "根目录",
"Router": "路由器", "Router": "路由器",
"Router Detail": "路由器详情", "Router Detail": "路由器详情",
"Router External": "外部网关", "Router External": "外部网关",
@ -1570,6 +1622,7 @@
"Security Groups": "安全组", "Security Groups": "安全组",
"Security Info": "安全信息", "Security Info": "安全信息",
"Segmentation ID": "段ID", "Segmentation ID": "段ID",
"Select File": "选择文件",
"Select Projct Role": "选择项目角色", "Select Projct Role": "选择项目角色",
"Select Project": "选择项目", "Select Project": "选择项目",
"Select Snapshot": "选择快照", "Select Snapshot": "选择快照",
@ -1644,6 +1697,7 @@
"Solomon Islands": "索罗门群岛", "Solomon Islands": "索罗门群岛",
"Somalia": "索马里", "Somalia": "索马里",
"Sorry, the page you visited does not exist.": "抱歉,您访问的页面不存在。", "Sorry, the page you visited does not exist.": "抱歉,您访问的页面不存在。",
"Source Path: {path}": "原路径:{path}",
"Source Port/Port Range": "源端口/端口范围", "Source Port/Port Range": "源端口/端口范围",
"South Africa": "南非", "South Africa": "南非",
"South Korea": "韩国", "South Korea": "韩国",
@ -1685,6 +1739,7 @@
"Storage Capacity(GB)": "存储容量(GB)", "Storage Capacity(GB)": "存储容量(GB)",
"Storage IOPS": "存储IOPS", "Storage IOPS": "存储IOPS",
"Storage Interface": "Storage接口", "Storage Interface": "Storage接口",
"Storage Policy": "存储权限",
"Storage Types": "存储类型", "Storage Types": "存储类型",
"Sub User": "组内用户列表", "Sub User": "组内用户列表",
"Subnet": "子网", "Subnet": "子网",
@ -1752,6 +1807,7 @@
"The description can be up to 255 characters long.": "描述最长为255字符", "The description can be up to 255 characters long.": "描述最长为255字符",
"The entire inspection process takes 5 to 10 minutes, so you need to be patient. After the registration is completed, the node configuration status will return to the manageable status.": "检查的整个过程需要耗费 5 到 10 分钟时间,您需要耐心等待。在完成注册后,节点配置状态会重新回到可管理状态。", "The entire inspection process takes 5 to 10 minutes, so you need to be patient. After the registration is completed, the node configuration status will return to the manageable status.": "检查的整个过程需要耗费 5 到 10 分钟时间,您需要耐心等待。在完成注册后,节点配置状态会重新回到可管理状态。",
"The feasible configuration of cloud-init or cloudbase-init service in the image is not synced to image's properties, so the Login Name is unknown.": "镜像中的cloud-init或cloudbase-init服务的预制配置未同步至镜像属性, 登录名未知", "The feasible configuration of cloud-init or cloudbase-init service in the image is not synced to image's properties, so the Login Name is unknown.": "镜像中的cloud-init或cloudbase-init服务的预制配置未同步至镜像属性, 登录名未知",
"The file with the same name will be overwritten.": "对同名文件将会进行文件覆盖操作。",
"The instance architecture diagram mainly shows the overall architecture composition of the instance. If you need to view the network topology of the instance, please go to: ": "云主机架构图主要展示云主机的总体架构组成。如果需要查看云主机的网络拓扑,请转到:", "The instance architecture diagram mainly shows the overall architecture composition of the instance. If you need to view the network topology of the instance, please go to: ": "云主机架构图主要展示云主机的总体架构组成。如果需要查看云主机的网络拓扑,请转到:",
"The instance deleted immediately cannot be restored": "立即删除的云主机无法恢复", "The instance deleted immediately cannot be restored": "立即删除的云主机无法恢复",
"The instance is not shut down, unable to restore.": "云主机不处于关机状态,不支持恢复备份操作。", "The instance is not shut down, unable to restore.": "云主机不处于关机状态,不支持恢复备份操作。",
@ -1768,6 +1824,7 @@
"The name should start with upper letter, lower letter or chinese, and be a string of 1 to 128, characters can only contain \"0-9, a-z, A-Z, \"-'_()[].:^\".": "名称应以大写字母小写字母或中文开头最长为128字符且只包含“0-9, a-z, A-Z, \"'-_()[].:^”。", "The name should start with upper letter, lower letter or chinese, and be a string of 1 to 128, characters can only contain \"0-9, a-z, A-Z, \"-'_()[].:^\".": "名称应以大写字母小写字母或中文开头最长为128字符且只包含“0-9, a-z, A-Z, \"'-_()[].:^”。",
"The name should start with upper letter, lower letter or chinese, and be a string of 1 to 128, characters can only contain \"0-9, a-z, A-Z, \"-'_.\".": "名称应以大写字母小写字母或中文开头最长为128字符且只包含“0-9, a-z, A-Z, \"-'_.”。", "The name should start with upper letter, lower letter or chinese, and be a string of 1 to 128, characters can only contain \"0-9, a-z, A-Z, \"-'_.\".": "名称应以大写字母小写字母或中文开头最长为128字符且只包含“0-9, a-z, A-Z, \"-'_.”。",
"The name should start with upper letter, lower letter or chinese, and be a string of 1 to 64, characters can only contain \"0-9, a-z, A-Z, \"-'_()[].^\".": "名称应以大写字母小写字母或中文开头最长为64字符且只包含“0-9, a-z, A-Z, \"'-_()[].^”。", "The name should start with upper letter, lower letter or chinese, and be a string of 1 to 64, characters can only contain \"0-9, a-z, A-Z, \"-'_()[].^\".": "名称应以大写字母小写字母或中文开头最长为64字符且只包含“0-9, a-z, A-Z, \"'-_()[].^”。",
"The name should start with upper letter, lower letter or chinese, and be a string of 3 to 63, characters can only contain \"0-9, a-z, A-Z, chinese, -, .\".": "名称应以大写字母小写字母或中文开头长度为3-63字符且只包含“0-9, a-z, A-Z, 中文, -, .”。",
"The name should start with upper letter, lower letter, and be a string of 1 to 128, characters can only contain \"0-9, a-z, A-Z, -, _\".": "名称应以大写字母或小写字母开头最长为128字符且只包含“0-9, a-z, A-Z, -, _”。", "The name should start with upper letter, lower letter, and be a string of 1 to 128, characters can only contain \"0-9, a-z, A-Z, -, _\".": "名称应以大写字母或小写字母开头最长为128字符且只包含“0-9, a-z, A-Z, -, _”。",
"The name should start with upper letter, lower letter, and be a string of 2 to 255, characters can only contain \"0-9, a-z, A-Z, -, ., _\".": "名称应以大写字母或小写字母开头最长为255字符且只包含“0-9, a-z, A-Z, -, ., _”。", "The name should start with upper letter, lower letter, and be a string of 2 to 255, characters can only contain \"0-9, a-z, A-Z, -, ., _\".": "名称应以大写字母或小写字母开头最长为255字符且只包含“0-9, a-z, A-Z, -, ., _”。",
"The name should start with upper letter, lower letter, and be a string of 3 to 63, characters can only contain \"0-9, a-z, A-Z, -\".": "名称应以大写字母小写字母开头长度为3-63字符且只包含“0-9, a-z, A-Z, -”。", "The name should start with upper letter, lower letter, and be a string of 3 to 63, characters can only contain \"0-9, a-z, A-Z, -\".": "名称应以大写字母小写字母开头长度为3-63字符且只包含“0-9, a-z, A-Z, -”。",
@ -1802,8 +1859,10 @@
"There are resources that cannot {action} in the selected resources, such as:": "您选中的资源中有无法{action}的资源,如:", "There are resources that cannot {action} in the selected resources, such as:": "您选中的资源中有无法{action}的资源,如:",
"There are resources that cannot {action} in the selected resources.": "您选中的资源中有无法{action}的资源。", "There are resources that cannot {action} in the selected resources.": "您选中的资源中有无法{action}的资源。",
"There are resources under the project and cannot be deleted.": "项目下存在资源,无法执行删除操作。", "There are resources under the project and cannot be deleted.": "项目下存在资源,无法执行删除操作。",
"There is currently no file to paste.": "当前没有需要粘贴的文件。",
"This service will automatically query the configuration (CPU, memory, etc.) and mac address of the physical machine, and the ironic-inspector service will automatically register this information in the node information.": "此服务将对在对物理机的配置CPU、内存等和 mac 地址进行自动查询, 并且 ironic-inspector 服务会将这些信息自动注册入节点信息中。", "This service will automatically query the configuration (CPU, memory, etc.) and mac address of the physical machine, and the ironic-inspector service will automatically register this information in the node information.": "此服务将对在对物理机的配置CPU、内存等和 mac 地址进行自动查询, 并且 ironic-inspector 服务会将这些信息自动注册入节点信息中。",
"This will delete all child objects of the load balancer.": "这会删除所有LB下的资源", "This will delete all child objects of the load balancer.": "这会删除所有LB下的资源",
"Time Interval: ": "",
"Timeout(Mininte)": "创建超时(分钟)", "Timeout(Mininte)": "创建超时(分钟)",
"Timeout(s)": "检查超时时间(秒)", "Timeout(s)": "检查超时时间(秒)",
"To open": "去开通", "To open": "去开通",
@ -1844,6 +1903,7 @@
"Unable to get {name}.": "无法获取{name}。", "Unable to get {name}.": "无法获取{name}。",
"Unable to get {title}, please go back to ": "无法获取{title},请返回", "Unable to get {title}, please go back to ": "无法获取{title},请返回",
"Unable to get {title}, please go to ": "无法获取{title},请访问", "Unable to get {title}, please go to ": "无法获取{title},请访问",
"Unable to paste into the same folder.": "无法粘贴到同一文件夹下。",
"Unable to render form": "无法生成表单", "Unable to render form": "无法生成表单",
"Unable to {action} {name}.": "无法{ action }{name}。", "Unable to {action} {name}.": "无法{ action }{name}。",
"Unable to {action}, because : {reason}, instance: {name}.": "无法{action},原因:{reason},实例名称:{name}。", "Unable to {action}, because : {reason}, instance: {name}.": "无法{action},原因:{reason},实例名称:{name}。",
@ -1869,6 +1929,7 @@
"Unshelving": "", "Unshelving": "",
"Unused": "未用", "Unused": "未用",
"Up": "正常", "Up": "正常",
"Update Access": "访问控制",
"Update At": "更新于", "Update At": "更新于",
"Update Complete": "更新完成", "Update Complete": "更新完成",
"Update Failed": "更新失败", "Update Failed": "更新失败",
@ -1880,6 +1941,7 @@
"Updated At": "更新于", "Updated At": "更新于",
"Updating": "更新中", "Updating": "更新中",
"Updating Password": "更新密码中", "Updating Password": "更新密码中",
"Upload File": "上传文件",
"Upload progress": "上传进度", "Upload progress": "上传进度",
"Uploading": "上传中", "Uploading": "上传中",
"Uruguay": "乌拉圭", "Uruguay": "乌拉圭",
@ -1920,6 +1982,8 @@
"VCPU (Core)": "", "VCPU (Core)": "",
"VCPUs": "虚拟CPU", "VCPUs": "虚拟CPU",
"VDI - VirtualBox compatible image format": "VDI - VirtualBox 兼容的图像格式", "VDI - VirtualBox compatible image format": "VDI - VirtualBox 兼容的图像格式",
"VGPU": "",
"VGPU (Core)": "",
"VHD - VirtualPC compatible image format": "VHD - VirtualPC 兼容的图像格式", "VHD - VirtualPC compatible image format": "VHD - VirtualPC 兼容的图像格式",
"VIF Details": "VIF详情", "VIF Details": "VIF详情",
"VIF Type": "VIF类型", "VIF Type": "VIF类型",
@ -1969,6 +2033,7 @@
"Volume Type Detail": "云硬盘类型详情", "Volume Type Detail": "云硬盘类型详情",
"Wallis And Futuna Islands": "沃利斯和富图纳群岛", "Wallis And Futuna Islands": "沃利斯和富图纳群岛",
"Warn": "警告", "Warn": "警告",
"Warning": "",
"Weight": "权重", "Weight": "权重",
"Weights": "权重", "Weights": "权重",
"Welcome": "欢迎", "Welcome": "欢迎",
@ -2016,6 +2081,8 @@
"backups": "备份", "backups": "备份",
"bare metal node": "裸机节点", "bare metal node": "裸机节点",
"bare metal nodes": "裸机节点", "bare metal nodes": "裸机节点",
"be copied": "复制",
"be cut": "剪切",
"be deleted": "删除", "be deleted": "删除",
"be rebooted": "重启", "be rebooted": "重启",
"be recovered": "恢复", "be recovered": "恢复",
@ -2029,6 +2096,8 @@
"compute services": "计算服务", "compute services": "计算服务",
"confirm resize or migrate": "确认修改配置/迁移", "confirm resize or migrate": "确认修改配置/迁移",
"connect subnet": "连接子网", "connect subnet": "连接子网",
"container objects": "容器对象",
"containers": "容器",
"create DSCP marking rule": "创建DSCP标记规则", "create DSCP marking rule": "创建DSCP标记规则",
"create a new network/subnet": "新建网络/子网", "create a new network/subnet": "新建网络/子网",
"create a new security group": "新建安全组", "create a new security group": "新建安全组",
@ -2059,6 +2128,7 @@
"delete backup": "删除备份", "delete backup": "删除备份",
"delete bandwidth egress rules": "删除出方向带宽限制规则", "delete bandwidth egress rules": "删除出方向带宽限制规则",
"delete bandwidth ingress rules": "删除入方向带宽限制规则", "delete bandwidth ingress rules": "删除入方向带宽限制规则",
"delete container": "删除容器",
"delete default pool": "删除资源池", "delete default pool": "删除资源池",
"delete domain": "删除域", "delete domain": "删除域",
"delete dscp marking rules": "删除DSCP标记规则", "delete dscp marking rules": "删除DSCP标记规则",
@ -2147,6 +2217,7 @@
"neutron agents": "网络服务", "neutron agents": "网络服务",
"online resize": "在线修改配置", "online resize": "在线修改配置",
"open external gateway": "开启公网网关", "open external gateway": "开启公网网关",
"paste files to folder": "粘贴文件到文件夹下",
"pause instance": "暂停云主机", "pause instance": "暂停云主机",
"phone": "手机", "phone": "手机",
"please select network": "请选择网络", "please select network": "请选择网络",
@ -2197,6 +2268,7 @@
"subnets": "子网", "subnets": "子网",
"suspend instance": "挂起云主机", "suspend instance": "挂起云主机",
"the Republic of Abkhazia": "阿布哈兹", "the Republic of Abkhazia": "阿布哈兹",
"the folder is not empty": "文件夹非空",
"the policy is in use": "策略正在使用中", "the policy is in use": "策略正在使用中",
"the router has connected subnet": "路由器有连接的子网", "the router has connected subnet": "路由器有连接的子网",
"the vpn gateway is in use": "VPN网关正在使用中", "the vpn gateway is in use": "VPN网关正在使用中",

View File

@ -0,0 +1,60 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import { ConfirmAction } from 'containers/Action';
import globalObjectStore from 'stores/swift/object';
import { allCanChangePolicy } from 'resources/policy';
import { isFile } from 'resources/container';
export default class CopyFile extends ConfirmAction {
get id() {
return 'CopyFile';
}
get title() {
return t('Copy File');
}
get name() {
return this.title;
}
get buttonText() {
return t('Copy');
}
get passiveAction() {
return t('be copied');
}
get actionName() {
return this.title;
}
getItemName = (item) => item.shortName;
policy = allCanChangePolicy;
allowedCheckFunc = (item) => isFile(item);
onSubmit = (value, containerProps, isBatch, index, values) => {
if (!isBatch) {
return globalObjectStore.copyFiles([value]);
}
if (index === 0) {
return globalObjectStore.copyFiles(values);
}
return Promise.resolve();
};
}

View File

@ -0,0 +1,93 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import { inject, observer } from 'mobx-react';
import { ModalAction } from 'containers/Action';
import globalObjectStore from 'stores/swift/object';
import { allCanChangePolicy } from 'resources/policy';
@inject('rootStore')
@observer
export default class CreateFolder extends ModalAction {
static id = 'create';
static title = t('Create Folder');
static policy = allCanChangePolicy;
init() {
this.store = globalObjectStore;
}
static allowed = (_, containerProps) => {
const { isAdminPage } = containerProps;
return Promise.resolve(!isAdminPage);
};
get name() {
return t('Create Folder');
}
get instanceName() {
return this.values.folder_name;
}
get defaultValue() {
const { name, folder } = this.store.container || {};
return {
container: name,
dest_folder: folder,
};
}
get formItems() {
const { folder } = this.store.container || {};
return [
{
name: 'container',
label: t('Container Name'),
type: 'label',
},
{
name: 'dest_folder',
label: t('Dest Folder'),
type: 'label',
hidden: !folder,
},
{
name: 'folder_name',
label: t('Folder Name'),
type: 'input-name',
required: true,
isSwiftFile: true,
maxLength: 63,
validator: (rule, value) => {
if (value.length < 2) {
return Promise.reject(
new Error(
`${t('Invalid: ')}${t('Please input at least 2 characters.')}`
)
);
}
return Promise.resolve();
},
},
];
}
onSubmit = async (values) => {
const { container, ...rest } = values;
return globalObjectStore.createFolder(container, rest);
};
}

View File

@ -0,0 +1,60 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import { ConfirmAction } from 'containers/Action';
import globalObjectStore from 'stores/swift/object';
import { allCanChangePolicy } from 'resources/policy';
import { isFile } from 'resources/container';
export default class CutFile extends ConfirmAction {
get id() {
return 'CutFile';
}
get title() {
return t('Cut File');
}
get name() {
return this.title;
}
get buttonText() {
return t('Cut');
}
get actionName() {
return this.title;
}
get passiveAction() {
return t('be cut');
}
getItemName = (item) => item.shortName;
policy = allCanChangePolicy;
allowedCheckFunc = (item) => isFile(item);
onSubmit = (value, containerProps, isBatch, index, values) => {
if (!isBatch) {
return globalObjectStore.cutFiles([value]);
}
if (index === 0) {
return globalObjectStore.cutFiles(values);
}
return Promise.resolve();
};
}

View File

@ -0,0 +1,79 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import { ConfirmAction } from 'containers/Action';
import globalObjectStore, { ObjectStore } from 'stores/swift/object';
import { allCanChangePolicy } from 'resources/policy';
import { isFile } from 'resources/container';
export default class Delete extends ConfirmAction {
get id() {
return 'delete';
}
get isFile() {
if (!this.item) {
return true;
}
return isFile(this.item);
}
get title() {
return this.isFile ? t('Delete File') : t('Delete Folder');
}
get name() {
return this.title;
}
get buttonType() {
return 'danger';
}
get buttonText() {
return t('Delete');
}
get actionName() {
return this.title;
}
getItemName = (item) => item.shortName;
policy = allCanChangePolicy;
onSubmit = async (data) => {
if (isFile(data)) {
this.showConfirmErrorBeforeSubmit = false;
return globalObjectStore.delete(data);
}
const store = new ObjectStore();
const { container, name: folder } = data;
const records = await store.fetchListByPage({ container, folder });
if (records.length > 0) {
this.showConfirmErrorBeforeSubmit = true;
this.confirmErrorMessageBeforeSubmit = t(
'Unable to {action}, because : {reason}, instance: {name}.',
{
action: this.actionName || this.title,
name: this.item.name,
reason: t('the folder is not empty'),
}
);
return Promise.reject();
}
this.showConfirmErrorBeforeSubmit = false;
return globalObjectStore.delete(data);
};
}

View File

@ -0,0 +1,53 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import { ConfirmAction } from 'containers/Action';
import { isFile } from 'resources/container';
import globalObjectStore from 'stores/swift/object';
import { allCanChangePolicy } from 'resources/policy';
import FileSaver from 'file-saver';
export default class Download extends ConfirmAction {
get id() {
return 'download';
}
get title() {
return t('Download File');
}
get name() {
return t('Download File');
}
get actionName() {
return t('Download File');
}
policy = allCanChangePolicy;
getItemName = (item) => item.shortName;
allowedCheckFunc = (item) => isFile(item);
onSubmit = async (values) => {
return globalObjectStore.downloadFile(values).then((res) => {
const { shortName } = values;
if (res.data) {
return FileSaver.saveAs(res.data, shortName);
}
return FileSaver.saveAs(res, shortName);
});
};
}

View File

@ -0,0 +1,117 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import { inject, observer } from 'mobx-react';
import { ModalAction } from 'containers/Action';
import globalObjectStore, { ObjectStore } from 'stores/swift/object';
import { allCanChangePolicy } from 'resources/policy';
import { isFile } from 'resources/container';
@inject('rootStore')
@observer
export default class Edit extends ModalAction {
static id = 'edit-file';
static title = t('Edit');
static policy = allCanChangePolicy;
init() {
this.store = new ObjectStore();
this.maxSize = 1;
}
static allowed = (item, containerProps) => {
const { isAdminPage } = containerProps;
return Promise.resolve(isFile(item) && !isAdminPage);
};
get name() {
return t('Upload File');
}
get instanceName() {
return this.item.shortName;
}
get hasRequestCancelCallback() {
return true;
}
get tips() {
return t(
'Editing only changes the content of the file, not the file name.'
);
}
get defaultValue() {
const { folder, shortName, container } = this.item;
return {
container,
dest_folder: folder,
shortName,
};
}
sizeValidate = (rule, value) => {
if (!value) {
return Promise.reject(t('Please select a file'));
}
const { size } = value;
if (size <= this.maxSize * 1024 * 1024 * 1024) {
return Promise.resolve();
}
return Promise.reject(
t(
'Please upload files smaller than { size }G on the page. It is recommended to upload files over { size }G using API.',
{ size: this.maxSize }
)
);
};
get formItems() {
const { folder } = globalObjectStore.container || {};
return [
{
name: 'container',
label: t('Container Name'),
type: 'label',
},
{
name: 'dest_folder',
label: t('Folder Name'),
type: 'label',
hidden: !folder,
},
{
name: 'shortName',
label: t('Filename'),
type: 'label',
},
{
name: 'file',
label: t('Select File'),
type: 'upload',
required: true,
validator: this.sizeValidate,
},
];
}
onSubmit = async (values) => {
const { container, file } = values;
const config = this.getUploadRequestConf();
return this.store.updateFile(container, file, this.item.name, config);
};
}

View File

@ -0,0 +1,136 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import React from 'react';
import { ConfirmAction } from 'containers/Action';
import globalObjectStore from 'stores/swift/object';
import { allCanChangePolicy } from 'resources/policy';
import { isFolder } from 'resources/container';
export default class PasteFile extends ConfirmAction {
get id() {
return 'PasteFile';
}
get title() {
return t('Paste File');
}
get name() {
return this.title;
}
get buttonText() {
return t('Paste');
}
get actionName() {
return t('paste files to folder');
}
get copiedFiles() {
const { copiedFiles = [] } = globalObjectStore;
return copiedFiles;
}
get folderInStore() {
const { container: { folder } = {} } = globalObjectStore;
return folder;
}
get containerInStore() {
const { container: { name } = {} } = globalObjectStore;
return name;
}
getFileNames() {
return this.copiedFiles.map((it) => it.shortName).join(', ');
}
getSourcePath() {
const { container, folder } = this.copiedFiles[0] || {};
return `${container}/${folder}`;
}
getItemName = (item) => {
if (item) {
return item.shortName;
}
return this.folderInStore || t('Root directory');
};
policy = allCanChangePolicy;
confirmContext = (data) => {
const name = this.getName(data);
return (
<div>
<p>
{this.unescape(
t('Are you sure to {action} (instance: {name})?', {
action: this.actionNameDisplay || this.title,
name,
})
)}
</p>
<p>
{this.unescape(
t('Source Path: {path}', { path: this.getSourcePath() })
)}
</p>
<p>
{this.unescape(t('Files: {names}', { names: this.getFileNames() }))}
</p>
<p>{t('The file with the same name will be overwritten.')}</p>
</div>
);
};
allowedCheckFunc = (item) => {
if (!item) {
const { hasCopy } = globalObjectStore;
return hasCopy && this.checkFolder();
}
return isFolder(item) && item.hasCopy && this.checkFolder(item);
};
checkFolder = (item) => {
const { container, folder } = this.copiedFiles[0] || {};
if (item) {
return item.container !== container || item.name !== folder;
}
return this.containerInStore !== container || this.folderInStore !== folder;
};
performErrorMsg = (failedItems) => {
if (!globalObjectStore.hasCopy) {
return t('There is currently no file to paste.');
}
if (!this.checkFolder(failedItems)) {
return t('Unable to paste into the same folder.');
}
const name = this.getName(failedItems);
return t('You are not allowed to {action}, instance: {name}.', {
action: this.actionNameDisplay || this.title,
name,
});
};
onSubmit = (data) => {
if (data) {
return globalObjectStore.pasteFiles(data);
}
return globalObjectStore.pasteFiles();
};
}

View File

@ -0,0 +1,95 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import { inject, observer } from 'mobx-react';
import { ModalAction } from 'containers/Action';
import globalObjectStore from 'stores/swift/object';
import { allCanChangePolicy } from 'resources/policy';
import { isFile } from 'resources/container';
@inject('rootStore')
@observer
export default class Rename extends ModalAction {
static id = 'rename';
static title = t('Rename');
static policy = allCanChangePolicy;
init() {
this.store = globalObjectStore;
}
static allowed = (item) => Promise.resolve(isFile(item));
get name() {
return t('Rename');
}
get instanceName() {
return this.item.shortName;
}
get defaultValue() {
const { folder, shortName, container } = this.item;
return {
container,
dest_folder: folder,
shortName,
};
}
get tip() {
return t(
'Rename is to copy the current file to the new file address and delete the current file, which will affect the creation time of the file.'
);
}
get formItems() {
const { folder } = globalObjectStore.container || {};
return [
{
name: 'container',
label: t('Container Name'),
type: 'label',
},
{
name: 'dest_folder',
label: t('Folder Name'),
type: 'label',
hidden: !folder,
},
{
name: 'shortName',
label: t('Filename'),
type: 'label',
},
{
name: 'newname',
label: t('Rename'),
type: 'input-name',
isSwiftFile: true,
required: true,
maxLength: 63,
},
];
}
onSubmit = async (values) => {
const { container, folder, name } = this.item;
const { newname } = values;
const newName = folder ? `${folder}/${newname}` : newname;
return globalObjectStore.rename(container, name, newName);
};
}

View File

@ -0,0 +1,104 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import { inject, observer } from 'mobx-react';
import { ModalAction } from 'containers/Action';
import globalObjectStore, { ObjectStore } from 'stores/swift/object';
import { allCanChangePolicy } from 'resources/policy';
@inject('rootStore')
@observer
export default class UploadFile extends ModalAction {
static id = 'upload-file';
static title = t('Upload File');
static policy = allCanChangePolicy;
init() {
this.store = new ObjectStore();
this.maxSize = 1;
}
static allowed = (_, containerProps) => {
const { isAdminPage } = containerProps;
return Promise.resolve(!isAdminPage);
};
get name() {
return t('Upload File');
}
get instanceName() {
return this.values.file.name;
}
get hasRequestCancelCallback() {
return true;
}
get defaultValue() {
const { name, folder } = globalObjectStore.container || {};
return {
container: name,
dest_folder: folder,
};
}
sizeValidate = (rule, value) => {
if (!value) {
return Promise.reject(t('Please select a file'));
}
const { size } = value;
if (size <= this.maxSize * 1024 * 1024 * 1024) {
return Promise.resolve();
}
return Promise.reject(
t(
'Please upload files smaller than { size }G on the page. It is recommended to upload files over { size }G using API.',
{ size: this.maxSize }
)
);
};
get formItems() {
const { folder } = globalObjectStore.container || {};
return [
{
name: 'container',
label: t('Container Name'),
type: 'label',
},
{
name: 'dest_folder',
label: t('Dest Folder'),
type: 'label',
hidden: !folder,
},
{
name: 'file',
label: t('Select File'),
type: 'upload',
required: true,
validator: this.sizeValidate,
},
];
}
onSubmit = async (values) => {
const { container, ...rest } = values;
const config = this.getUploadRequestConf();
return this.store.createFile(container, rest, config);
};
}

View File

@ -0,0 +1,53 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import CreateFolder from './CreateFolder';
import Delete from './Delete';
import UploadFile from './UploadFile';
import Download from './Download';
import CopyFile from './CopyFile';
import CutFile from './CutFile';
import Edit from './Edit';
import PasteFile from './PasteFile';
import Rename from './Rename';
const actionConfigs = {
rowActions: {
firstAction: Delete,
moreActions: [
{
action: Edit,
},
{
action: Download,
},
{
action: CopyFile,
},
{
action: CutFile,
},
{
action: PasteFile,
},
{
action: Rename,
},
],
},
batchActions: [Delete, CopyFile, CutFile],
primaryActions: [CreateFolder, UploadFile, PasteFile],
};
export default actionConfigs;

View File

@ -0,0 +1,309 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import React, { useEffect, useState } from 'react';
import { observer, inject } from 'mobx-react';
import { Popover, Col, Row, Skeleton } from 'antd';
import Base from 'containers/List';
import globalObjectStore, { ObjectStore } from 'stores/swift/object';
import { toLocalTimeFilter, bytesFitler } from 'utils/index';
import { allCanReadPolicy } from 'resources/policy';
import { toJS } from 'mobx';
import { isEqual } from 'lodash';
import { isFolder } from 'resources/container';
import { getStrFromTimestamp } from 'utils/time';
import styles from './index.less';
import actionConfigs from './actions';
function PopUpContent({ item }) {
const { container, name, shortName } = item;
const [data, setData] = useState([]);
const [loading, setLoaidng] = useState(false);
useEffect(() => {
let timeout = null;
(async function () {
setLoaidng(true);
const cb = await new ObjectStore().fetchDetail({ container, name });
timeout = setTimeout(() => {
setLoaidng(false);
setData(cb);
}, 200);
})();
return () => {
clearTimeout(timeout);
};
}, []);
const content = loading ? (
<Skeleton loading={loading} />
) : (
<>
<Row>
<Col span={8}>{t('Name')}</Col>
<Col span={12} style={{ wordBreak: 'break-all' }}>
{shortName}
</Col>
</Row>
{data.etag && (
<Row>
<Col span={8}>{t('Hash')}</Col>
<Col span={12}>{data.etag}</Col>
</Row>
)}
<Row>
<Col span={8}>{t('Content Type')}</Col>
<Col span={12} style={{ wordBreak: 'break-all' }}>
{data.contentType}
</Col>
</Row>
<Row>
<Col span={8}>{t('Created At')}</Col>
<Col span={12}>{getStrFromTimestamp(data.timestamp)}</Col>
</Row>
<Row>
<Col span={8}>{t('Size')}</Col>
<Col span={12}>{bytesFitler(data.size || item.bytes)}</Col>
</Row>
{!isFolder(item) && (
<Row>
<Col span={8}>{t('Origin File Name')}</Col>
<Col span={12} style={{ wordBreak: 'break-all' }}>
{decodeURIComponent(data.originFileName)}
</Col>
</Row>
)}
</>
);
return (
<div key={`object_${name}`} style={{ width: 300 }}>
{content}
</div>
);
}
@inject('rootStore')
@observer
export default class ContainerObject extends Base {
init() {
this.store = globalObjectStore;
}
get policy() {
return allCanReadPolicy;
}
get name() {
return t('container objects');
}
get rowKey() {
return 'name';
}
get actionConfigs() {
return actionConfigs;
}
get clearListUnmount() {
return true;
}
get hasTab() {
return true;
}
get hideDownload() {
return true;
}
get isInFolder() {
const { folder } = this.params;
return !!folder;
}
get ableAutoFresh() {
return false;
}
get primaryActionsExtra() {
const { hasCopy, container } = this.store;
return {
hasCopy,
container,
};
}
getCheckboxProps(record) {
if (isFolder(record)) {
return {
disabled: true,
name: record.shortName,
};
}
}
componentDidUpdate(prevProps) {
if (!isEqual(this.props.match.params, prevProps.match.params)) {
this.handleRefresh(true);
}
}
getRequestFolder = (folder) => {
if (!folder) {
return '';
}
const str = decodeURIComponent(folder);
if (str[str.length - 1] !== '/') {
return `${str}/`;
}
return str;
};
updateFetchParams = (params) => {
const { folder } = this.params;
return {
...params,
folder: this.getRequestFolder(folder),
};
};
getColumns = () => [
{
title: t('Name'),
dataIndex: 'shortName',
stringify: (name, record) => name || record.id,
render: (name, record) => {
const { type, container } = record;
if (type === 'folder') {
const str = encodeURIComponent(record.name);
return this.getLinkRender('folderDetail', name, {
container,
folder: str,
});
}
return name;
},
},
{
title: t('Size'),
dataIndex: 'bytes',
isHideable: true,
valueRender: 'formatSize',
render: (value, data) => {
if (data.type === 'folder') {
return '-';
}
return bytesFitler(value);
},
},
{
title: t('Last Updated'),
dataIndex: 'last_modified',
isHideable: true,
valueRender: 'sinceTime',
stringify: (value) => toLocalTimeFilter(value),
},
{
title: t('Detail Info'),
dataIndex: 'detail',
isHideable: true,
render: (_, data) => {
const content = <PopUpContent item={data} />;
return (
<Popover content={content} destroyTooltipOnHide trigger="click">
<span className="linkClass">{t('Detail Info')}</span>
</Popover>
);
},
},
];
get searchFilters() {
return [
{
label: t('Name'),
name: 'shortName',
},
];
}
handleRefresh = (force) => {
const { inAction, inSelect } = this;
if (inAction || (inSelect && !force)) {
return;
}
if (!force && this.autoRefreshCount >= this.autoRefreshCountMax) {
return;
}
if (force) {
this.autoRefreshCount = 0;
}
const { page, limit, sortKey, sortOrder, filters } = this.list;
const params = {
page,
limit,
sortKey,
sortOrder,
...toJS(filters),
silent: !force,
};
if (force) {
params.page = 1;
}
this.handleFetch(params, true);
if (this.inDetailPage && force && this.shouldRefreshDetail) {
this.refreshDetailData();
}
};
renderHeader() {
const { container = '', folder = '' } = this.params || {};
const folders = decodeURIComponent(folder)
.split('/')
.filter((it) => !!it);
const containerLink = {
path: this.getRoutePath('containerDetail', { id: container }),
link: this.getLinkRender('containerDetail', container, { id: container }),
};
const items = [containerLink];
const folderLinks = folders.map((it, index) => {
const path = folders.slice(0, index + 1).join('/');
return {
path: this.getRoutePath('folderDetail', {
container,
folder: encodeURIComponent(path),
}),
link: this.getLinkRender('folderDetail', it, {
container,
folder: encodeURIComponent(path),
}),
};
});
items.push(...folderLinks);
const next = <span className={styles['item-next']}>&gt;</span>;
const itemLinks = items.map((it, index) => {
return (
<span key={it.path}>
{it.link} {index < items.length - 1 && next}
</span>
);
});
return (
<div className={styles['link-header']}>
<span className={styles['link-title']}>{t('Current Path: ')}</span>
{itemLinks}
</div>
);
}
}

View File

@ -0,0 +1,14 @@
.link-header {
line-height: 44px;
color: rgba(0, 0, 0, 0.85);
background-color: white;
margin-bottom: 8px;
padding-left: 8px;
}
.link-title {
margin-right: 8px;
}
.item-next {
margin-left: 8px;
margin-right: 8px;
}

View File

@ -0,0 +1,72 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import { inject, observer } from 'mobx-react';
import { ModalAction } from 'containers/Action';
import globalContainerStore, { ContainerStore } from 'stores/swift/container';
import { allCanChangePolicy } from 'resources/policy';
@inject('rootStore')
@observer
export default class Access extends ModalAction {
static id = 'access';
static title = t('Update Access');
static policy = allCanChangePolicy;
static allowed = (_, containerProps) => {
const { isAdminPage } = containerProps;
return Promise.resolve(!isAdminPage);
};
get name() {
return t('Update Access');
}
init() {
this.detailStore = new ContainerStore();
this.fetchDetail();
}
async fetchDetail() {
await this.detailStore.fetchDetail({ name: this.item.name });
this.updateDefaultValue();
}
get defaultValue() {
const { is_public } = this.detailStore.detail || {};
return {
isPublic: is_public || false,
};
}
get formItems() {
return [
{
name: 'isPublic',
label: t('Public Access'),
type: 'switch',
tip: t(
'A public container will allow anyone to use the objects in your container through a public URL.'
),
},
];
}
onSubmit = async (value) => {
const { isPublic = false } = value;
return globalContainerStore.updatePublic(this.item.name, isPublic);
};
}

View File

@ -0,0 +1,68 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import { inject, observer } from 'mobx-react';
import { ModalAction } from 'containers/Action';
import globalContainerStore from 'stores/swift/container';
import { allCanChangePolicy } from 'resources/policy';
@inject('rootStore')
@observer
export default class Create extends ModalAction {
static id = 'create';
static title = t('Create Container');
static policy = allCanChangePolicy;
static allowed = (_, containerProps) => {
const { isAdminPage } = containerProps;
return Promise.resolve(!isAdminPage);
};
get name() {
return t('Create Container');
}
get tips() {
return t(
"Note: Please consider the container name carefully since it couldn't be changed after created."
);
}
get formItems() {
return [
{
name: 'name',
label: t('Name'),
type: 'input-name',
required: true,
isSwiftFile: true,
maxLength: 63,
},
{
name: 'isPublic',
label: t('Public Access'),
type: 'switch',
tip: t(
'A public container will allow anyone to use the objects in your container through a public URL.'
),
},
];
}
onSubmit = async (value) => {
return globalContainerStore.create(value);
};
}

View File

@ -0,0 +1,47 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import { ConfirmAction } from 'containers/Action';
import globalContainerStore from 'stores/swift/container';
import { allCanChangePolicy } from 'resources/policy';
export default class Delete extends ConfirmAction {
get id() {
return 'delete';
}
get title() {
return t('Delete Container');
}
get name() {
return t('Delete Container');
}
get buttonType() {
return 'danger';
}
get buttonText() {
return t('Delete');
}
get actionName() {
return t('delete container');
}
policy = allCanChangePolicy;
onSubmit = ({ id }) => globalContainerStore.delete({ id });
}

View File

@ -0,0 +1,32 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import Create from './Create';
import Delete from './Delete';
import Access from './Access';
const actionConfigs = {
rowActions: {
firstAction: Access,
moreActions: [
{
action: Delete,
},
],
},
batchActions: [Delete],
primaryActions: [Create],
};
export default actionConfigs;

View File

@ -0,0 +1,168 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import React, { useEffect, useState } from 'react';
import { observer, inject } from 'mobx-react';
import { Popover, Col, Row, Skeleton } from 'antd';
import Base from 'containers/List';
import globalContainerStore, { ContainerStore } from 'stores/swift/container';
import { toLocalTimeFilter, bytesFitler } from 'utils/index';
import { allCanChangePolicy } from 'resources/policy';
import { getStrFromTimestamp } from 'utils/time';
import { swiftEndpoint } from 'client/client/constants';
import actionConfigs from './actions';
function PopUpContent({ name }) {
const [data, setData] = useState([]);
const [loading, setLoaidng] = useState(false);
useEffect(() => {
let timeout = null;
(async function () {
setLoaidng(true);
const cb = await new ContainerStore().fetchDetail({ name });
timeout = setTimeout(() => {
setLoaidng(false);
setData(cb);
}, 200);
})();
return () => {
clearTimeout(timeout);
};
}, []);
const content = loading ? (
<Skeleton loading={loading} />
) : (
<>
<Row>
<Col span={8}>{t('Object Count')}</Col>
<Col span={12}>{data.object_count}</Col>
</Row>
<Row>
<Col span={8}>{t('Size')}</Col>
<Col span={12}>{bytesFitler(data.used)}</Col>
</Row>
<Row>
<Col span={8}>{t('Created At')}</Col>
<Col span={12}>{getStrFromTimestamp(data.timestamp)}</Col>
</Row>
<Row>
<Col span={8}>{t('Storage Policy')}</Col>
<Col span={12}>{data.storage_policy}</Col>
</Row>
<Row>
<Col span={8}>{t('Public Access')}</Col>
<Col span={12}>
{data.link ? (
<a type="link" href={data.link} target="_blank" rel="noreferrer">
{t('Click To View')}
</a>
) : (
t('Private')
)}
</Col>
</Row>
</>
);
return (
<div key={`container_${name}`} style={{ width: 300 }}>
{content}
</div>
);
}
@inject('rootStore')
@observer
export default class Container extends Base {
init() {
this.store = globalContainerStore;
}
get policy() {
return allCanChangePolicy;
}
get checkEndpoint() {
return true;
}
get endpoint() {
return swiftEndpoint();
}
get name() {
return t('containers');
}
get actionConfigs() {
return actionConfigs;
}
get hideCustom() {
return true;
}
get rowKey() {
return 'name';
}
getColumns = () => {
const columns = [
{
title: t('Name'),
dataIndex: 'name',
render: (name, record) =>
this.getLinkRender('containerDetail', name || record.id, {
id: record.id,
}),
stringify: (name, record) => name || record.id,
},
{
title: t('Size'),
dataIndex: 'bytes',
valueRender: 'bytes',
},
{
title: t('Last Updated'),
dataIndex: 'last_modified',
isHideable: true,
valueRender: 'sinceTime',
stringify: (value) => toLocalTimeFilter(value),
},
{
title: t('Detail Info'),
dataIndex: 'detail',
isHideable: true,
render: (_, data) => {
const content = <PopUpContent name={data.name} />;
return (
<Popover content={content} destroyTooltipOnHide trigger="click">
<span className="linkClass">{t('Detail Info')}</span>
</Popover>
);
},
},
];
return columns;
};
get searchFilters() {
return [
{
label: t('Name'),
name: 'name',
},
];
}
}

View File

@ -25,6 +25,8 @@ import VolumeType from '../containers/VolumeType';
import VolumeTypeDetail from '../containers/VolumeType/VolumeType/Detail'; import VolumeTypeDetail from '../containers/VolumeType/VolumeType/Detail';
import QosDetail from '../containers/VolumeType/QosSpec/Detail'; import QosDetail from '../containers/VolumeType/QosSpec/Detail';
import Storage from '../containers/Storage'; import Storage from '../containers/Storage';
import Container from '../containers/Container';
import ContainerObject from '../containers/Container/Detail';
const PATH = '/storage'; const PATH = '/storage';
export default [ export default [
@ -81,6 +83,17 @@ export default [
exact: true, exact: true,
}, },
{ path: `${PATH}/storage-admin`, component: Storage, exact: true }, { path: `${PATH}/storage-admin`, component: Storage, exact: true },
{ path: `${PATH}/container`, component: Container, exact: true },
{
path: `${PATH}/container/detail/:container`,
component: ContainerObject,
exact: true,
},
{
path: `${PATH}/container/detail/:container/:folder`,
component: ContainerObject,
exact: true,
},
{ path: '*', component: E404 }, { path: '*', component: E404 },
], ],
}, },

View File

@ -0,0 +1,16 @@
// Copyright 2021 99cloud
//
// Licensed 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.
export const isFile = (item) => item && item.type === 'file';
export const isFolder = (item) => item && item.type === 'folder';

View File

@ -0,0 +1,97 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import { action } from 'mobx';
import client from 'client';
import Base from '../base';
export class ContainerStore extends Base {
get client() {
return client.swift.container;
}
get listResponseKey() {
return '';
}
get mapper() {
return (data) => ({
...data,
id: data.name,
});
}
async detailFetchByClient(resourceParams) {
const { name } = resourceParams;
const result = await this.client.showMetadata(name);
const { headers = {} } = result;
const isPublic = !!headers['x-container-read'];
let link = null;
if (isPublic) {
link = this.client.url(name);
}
const data = {
used: headers['x-container-bytes-used'],
object_count: headers['x-container-object-count'],
storage_policy: headers['x-storage-policy'],
timestamp: headers['x-timestamp'],
is_public: isPublic,
link,
};
return data;
}
@action
checkName = async (name) => {
try {
await this.client.showMetadata(name);
const err = {
response: {
data: t('A container with the same name already exists'),
},
};
return Promise.reject(err);
} catch (e) {
return true;
}
};
@action
async create(data) {
const { name, isPublic } = data;
await this.checkName(name);
if (!isPublic) {
return this.submitting(this.client.create(name));
}
this.isSubmitting = true;
await this.client.create(name);
return this.updatePublic(name, isPublic);
}
@action
delete = async ({ id }) => {
return this.submitting(this.client.delete(id));
};
@action
updatePublic = async (name, isPublic) => {
const headers = {
'X-Container-Read': isPublic ? '.r:*,.rlistings' : '',
};
return this.submitting(this.client.updateMetadata(name, headers));
};
}
const globalContainerStore = new ContainerStore();
export default globalContainerStore;

307
src/stores/swift/object.js Normal file
View File

@ -0,0 +1,307 @@
// Copyright 2021 99cloud
//
// Licensed 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.
import { action, observable } from 'mobx';
import client from 'client';
import { getArrayBuffer } from 'utils/file';
import Base from '../base';
export class ObjectStore extends Base {
@observable
container = null;
@observable
data = [];
@observable
hasNext = false;
@observable
copiedFiles = [];
@observable
hasCopy = false;
@observable
isCopy = true;
get client() {
return client.swift.container.object;
}
get containerClient() {
return client.swift.container;
}
get listResponseKey() {
return '';
}
get listFilterByProject() {
return false;
}
async listFetchByClient(params, originParams) {
const { folder, container } = originParams;
const { path } = params;
const result = await this.client.list(container, params);
this.container = {
name: container,
folder,
path,
hasCopy: this.copiedFiles.length > 0,
};
return result;
}
get paramsFunc() {
return (params) => {
const { current, container, folder, search = '', ...rest } = params;
return {
path: `${folder}${search}`,
delimiter: `/`,
...rest,
};
};
}
getShortName = (name, folder) => name.substring((folder || '').length);
getItemType = (it) => {
if (it.subdir) {
return 'folder';
}
const { name } = it;
if (name[name.length - 1] === '/') {
return 'folder';
}
return 'file';
};
async listDidFetch(items, _, filters) {
if (items.length === 0) {
return items;
}
const { container } = filters;
const needFetch = items.some((it) => it.subdir);
if (!needFetch) {
return this.updateData(items);
}
const result = await this.client.list(container);
const newItems = items.map((it) => {
if (it.subdir) {
const item = result.find((r) => r.name === it.subdir) || {};
return {
...it,
...item,
};
}
return { ...it };
});
return this.updateData(newItems);
}
async detailFetchByClient(resourceParams) {
const { container, name } = resourceParams;
const result = await this.containerClient.showObjectMetadata(
container,
name
);
const { headers = {} } = result;
const data = {
timestamp: headers['x-timestamp'],
contentType: headers['content-type'],
etag: headers.etag,
size: headers['content-length'],
originFileName: headers['x-object-meta-orig-filename'],
};
return data;
}
@action
updateData = (items) => {
const { name, path, folder, hasCopy } = this.container || {};
return items.map((it) => {
return {
...it,
container: name,
path,
folder,
type: this.getItemType(it),
hasCopy,
shortName: it.name && this.getShortName(it.name, folder),
};
});
};
@action
async createFolder(container, data) {
const { folder_name, dest_folder = '' } = data;
const name = `${dest_folder}${folder_name}/`;
await this.checkName(container, name);
return this.submitting(this.containerClient.createFolder(container, name));
}
@action
async createFile(container, data, config = {}) {
const { file, dest_folder = '' } = data;
const name = `${dest_folder}${file.name}`;
await this.checkName(container, name);
const headers = {
'X-Object-Meta-Orig-Filename': encodeURIComponent(file.name),
'Content-Length': file.size,
'Content-Type': file.type,
};
const content = await getArrayBuffer(file);
return this.submitting(
this.containerClient.uploadFile(container, name, content, {
headers,
...config,
})
);
}
@action
async updateFile(container, file, name, config = {}) {
const headers = {
'X-Object-Meta-Orig-Filename': encodeURIComponent(file.name),
'Content-Length': file.size,
'Content-Type': file.type,
};
const content = await getArrayBuffer(file);
return this.submitting(
this.containerClient.uploadFile(container, name, content, {
headers,
...config,
})
);
}
@action
async rename(container, name, newname) {
this.isSubmitting = true;
await this.checkName(container, newname);
await this.containerClient.copy(container, name, container, newname);
return this.delete({ container, name });
}
@action
async downloadFile({ container, name }) {
return this.client.show(container, name, null, {
responseType: 'blob',
});
}
@action
delete = async ({ container, name }) => {
return this.submitting(this.client.delete(container, name));
};
@action
checkName = async (container, name) => {
try {
await this.containerClient.showObjectMetadata(container, name);
const err = {
response: {
data: t('An object with the same name already exists'),
},
};
return Promise.reject(err);
} catch (e) {
return true;
}
};
@action
copyFiles = async (files) => {
this.copiedFiles = files;
this.hasCopy = files.length > 0;
this.isCopy = true;
return Promise.resolve();
};
@action
cutFiles = async (files) => {
this.copiedFiles = files;
this.hasCopy = files.length > 0;
this.isCopy = false;
return Promise.resolve();
};
@action
pasteFiles = async (folder) => {
if (this.copiedFiles.length === 0) {
return Promise.reject();
}
let realFolder = folder;
if (!folder) {
realFolder = {
container: this.container.name,
name: this.container.folder,
};
}
if (this.isCopy) {
return this.pasteObjects(realFolder);
}
return this.moveObjects(realFolder);
};
@action
async pasteObjects(folder) {
const { container: toContainer, name } = folder;
const { container: fromContainer } = this.copiedFiles[0];
await Promise.all(
this.copiedFiles.map((it) => {
const { shortName, name: fromName } = it;
const toName = `${name}${shortName}`;
return this.containerClient.copy(
fromContainer,
fromName,
toContainer,
toName
);
})
);
return Promise.resolve();
}
@action
async moveObjects(folder) {
await this.pasteObjects(folder);
const { container: originContainer } = this.copiedFiles[0];
await Promise.all(
this.copiedFiles.map((it) => {
const { name: fileName } = it;
return this.client.delete(originContainer, fileName);
})
);
this.copiedFiles = [];
this.hasCopy = false;
return Promise.resolve();
}
@action
clearData(listUnmount) {
this.list.reset();
if (!listUnmount) {
this.copiedFiles = [];
this.hasCopy = false;
this.container = null;
}
}
}
const globalObjectStore = new ObjectStore();
export default globalObjectStore;

View File

@ -29,3 +29,12 @@ export function getText(file) {
reader.onerror = (error) => reject(error); reader.onerror = (error) => reject(error);
}); });
} }
export function getArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
}

View File

@ -54,6 +54,8 @@ const instanceNameRegex =
const ipv6CidrOnly = const ipv6CidrOnly =
/^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9-]*[A-Za-z0-9])$|^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*\/(1[01][0-9]|12[0-8]|[0-9]{1,2})$/; /^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9-]*[A-Za-z0-9])$|^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*\/(1[01][0-9]|12[0-8]|[0-9]{1,2})$/;
const asciiRegex = /^[\x00-\x7f]*$/; // eslint-disable-line const asciiRegex = /^[\x00-\x7f]*$/; // eslint-disable-line
const swiftFileNameRegex =
/^[A-Za-z\u4e00-\u9fa5]+[A-Za-z\u4e00-\u9fa5\d-.]{2,62}$/;
export const regex = { export const regex = {
cidr, cidr,
@ -73,6 +75,7 @@ export const regex = {
instanceNameRegex, instanceNameRegex,
ipv6CidrOnly, ipv6CidrOnly,
asciiRegex, asciiRegex,
swiftFileNameRegex,
}; };
export const isPhoneNumber = (value) => isValidPhoneNumber(value); export const isPhoneNumber = (value) => isValidPhoneNumber(value);
@ -238,6 +241,13 @@ const isFilename = (value) => {
return false; return false;
}; };
const isSwiftFilename = (value) => {
if (value && isString(value)) {
return swiftFileNameRegex.test(value);
}
return false;
};
const isNameWithoutChinese = (value) => { const isNameWithoutChinese = (value) => {
if (value && isString(value)) { if (value && isString(value)) {
return nameRegexWithoutChinese.test(value) && value.length <= 128; return nameRegexWithoutChinese.test(value) && value.length <= 128;
@ -292,6 +302,10 @@ const filenameMessage = t(
'The name should start with upper letter, lower letter, and be a string of 3 to 63, characters can only contain "0-9, a-z, A-Z, -".' 'The name should start with upper letter, lower letter, and be a string of 3 to 63, characters can only contain "0-9, a-z, A-Z, -".'
); );
const swiftFilenameMessage = t(
'The name should start with upper letter, lower letter or chinese, and be a string of 3 to 63, characters can only contain "0-9, a-z, A-Z, chinese, -, .".'
);
const keypairNameMessage = t( const keypairNameMessage = t(
'The name should start with upper letter, lower letter, and be a string of 1 to 128, characters can only contain "0-9, a-z, A-Z, -, _".' 'The name should start with upper letter, lower letter, and be a string of 1 to 128, characters can only contain "0-9, a-z, A-Z, -, _".'
); );
@ -321,6 +335,7 @@ export const nameMessageInfo = {
crontabNameMessage, crontabNameMessage,
imageNameMessage, imageNameMessage,
instanceNameMessage, instanceNameMessage,
swiftFilenameMessage,
}; };
export const portMessage = t('Enter an integer value between 1 and 65535.'); export const portMessage = t('Enter an integer value between 1 and 65535.');
@ -393,6 +408,16 @@ const fileNameValidate = (rule, value) => {
return Promise.reject(new Error(`${t('Invalid: ')}${filenameMessage}`)); return Promise.reject(new Error(`${t('Invalid: ')}${filenameMessage}`));
}; };
const swiftFileNameValidate = (rule, value) => {
if (!rule.required && value === undefined) {
return Promise.resolve(true);
}
if (isSwiftFilename(value)) {
return Promise.resolve(true);
}
return Promise.reject(new Error(`${t('Invalid: ')}${swiftFilenameMessage}`));
};
const keypairNameValidate = (rule, value) => { const keypairNameValidate = (rule, value) => {
if (!rule.required && value === undefined) { if (!rule.required && value === undefined) {
return Promise.resolve(true); return Promise.resolve(true);
@ -452,6 +477,7 @@ export const nameTypeValidate = {
crontabNameValidate, crontabNameValidate,
imageNameValidate, imageNameValidate,
instanceNameValidate, instanceNameValidate,
swiftFileNameValidate,
}; };
export const cidrAllValidate = (rule, value) => { export const cidrAllValidate = (rule, value) => {