Skip to content

Commit

Permalink
feat: Add vm config parser, unify vm and helm type app deployment (#503)
Browse files Browse the repository at this point in the history
* refactor: Deploy page combine vm and helm type app
* refactor: VM config translator, compat with edge case
* Deploy page updated when changed runtime, version
* Fix test code
  • Loading branch information
sunnywx authored Oct 31, 2018
1 parent 87e2e36 commit aa84b64
Show file tree
Hide file tree
Showing 25 changed files with 1,156 additions and 787 deletions.
1 change: 1 addition & 0 deletions lib/config-parser/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mock/
163 changes: 163 additions & 0 deletions lib/config-parser/factory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
const toString = Object.prototype.toString;
const isObject = val => toString.call(val) === '[object Object]';
const isNumber = val => toString.call(val) === '[object Number]';

const validRenderTypes = [
'radio',
// 'checkbox',
'input',
'select',
'text',
'slider',
'number',
'node-role',
'yaml'
];

const TEXT_KEYS = ['description']; // keys will be transformed to text area

const NODE_MUST_HAVE_KEYS = ['cpu', 'memory'];

// keys to check if object can be normalized to Section
const featureKeys = ['key'];

const sectionProto = {
default: '',
description: '',
key: '',
label: '',
required: false,
type: 'string',
getRenderType() {
if (validRenderTypes.includes(this.renderType)) {
return this.renderType;
}

if (TEXT_KEYS.includes(this.key)) {
return 'text';
}

if (
this.type === 'string' &&
!this.items &&
!TEXT_KEYS.includes(this.key) &&
!(this.range || this.step)
) {
return 'input';
}

if (this.type === 'integer' && Array.isArray(this.range)) {
return 'radio';
}

if (
this.type === 'integer' &&
isNumber(this.step) &&
isNumber(this.min) &&
isNumber(this.max)
) {
return 'slider';
}

if (this.type === 'integer' && !this.step && (isNumber(this.min) || isNumber(this.max))) {
return 'number';
}

if (this.type === 'string' && Array.isArray(this.items)) {
return 'select';
}

if (this.type === 'array' && isNodeItem(this)) {
return 'node-role';
}

if (typeof this.type === 'string' && 'changeable' in this) {
return Array.isArray(this.range) ? 'radio' : 'input';
}

throw Error(`unknown render type for section: ${this}`);
},
toString() {
return JSON.stringify(...this);
},
toJSON() {
// transform to object that react can accept
const ret = Object.assign(
{ ...this },
{
default: this.default,
description: this.description,
label: this.label || this.key,
required: typeof this.required === 'boolean' ? this.required : this.required !== 'false',
type: this.type,
renderType: this.getRenderType()
}
);

ret.keyName = ret.originKey = ret.key;
ret.defaultValue = ret.default;

delete ret.key;
delete ret.default;

// generate unique keyName based on each level
if (ret.keyPrefix) {
ret.keyName = [ret.keyPrefix, ret.keyName].join('.');
delete ret.keyPrefix;
}

return ret;
}
};

export const isNodeItem = (item = {}) => {
if (!Array.isArray(item.properties)) {
return false;
}

const itemKeys = item.properties.map(o => o.key);

return NODE_MUST_HAVE_KEYS.reduce((check, key) => {
return check && itemKeys.includes(key);
}, true);
};

export const genPrefix = (prefix, base) => {
if (typeof prefix === 'string' && prefix) {
return [prefix, base].join('.');
}
return base;
};

const factory = (ownProps = {}, extendProps = {}) => {
const wrapObj = obj => {
const ownKeys = (obj && Object.getOwnPropertyNames(obj)) || [];
const hasFeatureKeys = featureKeys.reduce((check, key) => {
return check && ownKeys.includes(key);
}, true);

if (!hasFeatureKeys) {
return obj;
}

const inst = Object.create(sectionProto);

if (Array.isArray(obj.properties)) {
obj.properties = factory(obj.properties, {
keyPrefix: genPrefix(extendProps.keyPrefix, obj.key)
});
}

return Object.assign(inst, obj, extendProps);
};

if (isObject(ownProps)) {
return wrapObj(ownProps);
}

if (Array.isArray(ownProps)) {
return ownProps.filter(Boolean).map(prop => wrapObj(prop));
}
};

export default factory;
166 changes: 166 additions & 0 deletions lib/config-parser/vm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import _ from 'lodash';

import factory, { isNodeItem } from './factory';

const defaultOptions = {};

const CLUSTER_KEY = 'cluster';
const NAME_KEY = 'name';
const DESC_KEY = 'description';
const SUBNET_KEY = 'subnet';
const ENV_KEY = 'env';

// setting placeholder
const defaultNameSetting = {
key: 'name',
label: 'Name',
type: 'string',
default: '',
required: false
};

const defaultRuntimeSetting = {
key: 'runtime',
label: 'Runtime',
renderType: 'radio',
range: []
};

const defaultVersionSetting = {
key: 'version',
label: 'Version',
items: [
// {name: 'x1', value: 'v1'},
]
};

export default class VMConfigParser {
constructor(config, options = {}) {
this.config = config;
this.options = _.extend(defaultOptions, options);

this.runtimeConf = _.clone(defaultRuntimeSetting);
this.versionConf = _.clone(defaultVersionSetting);
this.vxnetConf = { items: [] };
}

setConfig(config = {}) {
if (typeof config === 'string') {
config = JSON.parse(config);
}

if (!this.validConfig(config)) {
throw Error('invalid config input');
}

// config.json
this.config = config;
}

validConfig(config) {
return (
_.isObject(config) && typeof config.type === 'string' && Array.isArray(config.properties)
);
}

isReady() {
return !_.isEmpty(this.config);
}

getConfigByKey(config, key) {
if (typeof config === 'string' && !key) {
key = config;
config = null;
}

if (typeof key !== 'string') {
throw Error(`key should be string, but ${typeof key} given`);
}

config = config || this.config;
if (_.isEmpty(config)) {
return;
}

const properties = config.properties || config;

if (_.isArray(properties)) {
return _.find(properties, { key });
}
if (_.isObject(properties)) {
return _.extend({}, properties[key], { key });
}
}

getClusterSetting() {
return this.getConfigByKey(CLUSTER_KEY);
}

/**
* mock basic setting for helm app
*/
getDefaultBasicSetting(override = {}) {
const setting = [];
const nameConf = _.clone(defaultNameSetting);
setting.push(nameConf, this.runtimeConf, this.versionConf);

if (!_.isEmpty(override)) {
_.forEach(setting, setting => {
if (setting.key === override.key) {
_.extend(setting, override);
}
});
}

return factory(setting, { keyPrefix: CLUSTER_KEY });
}

getBasicSetting() {
const clusterSetting = this.getClusterSetting();
const setting = [];
const nameConf = this.getConfigByKey(clusterSetting, NAME_KEY);
const descConf = this.getConfigByKey(clusterSetting, DESC_KEY);

setting.push(nameConf, descConf, this.runtimeConf, this.versionConf);

return factory(setting, { keyPrefix: CLUSTER_KEY });
}

getNodeSetting() {
const clusterSetting = this.getClusterSetting();
return factory(_.filter(clusterSetting.properties, isNodeItem), { keyPrefix: 'node' });
}

getVxnetSetting() {
return factory([this.vxnetConf], { keyPrefix: CLUSTER_KEY });
}

getEnvSetting() {
const envSetting = this.getConfigByKey(ENV_KEY);
return factory(_.get(envSetting, 'properties', []), { keyPrefix: 'env', labelPrefix: 'env' });
}

setRuntimes(runtimes = [], mergeProps) {
this.runtimeConf.range = runtimes;
!_.isEmpty(mergeProps) && _.extend(this.runtimeConf, mergeProps);
}

setVersions(versions = [], mergeProps) {
this.versionConf.items = versions;
!_.isEmpty(mergeProps) && _.extend(this.versionConf, mergeProps);
}

setSubnets(nets = [], mergeProps) {
if (!this.vxnetConf.key) {
const clusterSetting = this.getClusterSetting();

if (!_.isEmpty(clusterSetting)) {
_.extend(this.vxnetConf, this.getConfigByKey(clusterSetting, SUBNET_KEY));
}
}

this.vxnetConf.items = nets;

!_.isEmpty(mergeProps) && _.extend(this.vxnetConf, mergeProps);
}
}
12 changes: 6 additions & 6 deletions src/components/Base/Button/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,27 @@ import styles from './index.scss';

export default class Button extends PureComponent {
static propTypes = {
type: PropTypes.string,
type: PropTypes.oneOf(['default', 'primary', 'delete']),
htmlType: PropTypes.oneOf(['submit', 'button', 'reset']),
className: PropTypes.string,
style: PropTypes.object,
loading: PropTypes.bool,
disabled: PropTypes.bool,
onClick: PropTypes.func,
onClick: PropTypes.func
};

static defaultProps = {
type: 'default',
htmlType: 'button',
htmlType: 'button'
};

handleClick = (e) => {
handleClick = e => {
const isDisabled = this.props.disabled;

if (!isDisabled && isFunction(this.props.onClick)) {
this.props.onClick(e);
}
}
};

render() {
const { children, type, htmlType, className, loading, ...others } = this.props;
Expand All @@ -37,7 +37,7 @@ export default class Button extends PureComponent {
<button
className={classNames(styles.button, styles[type], {
[styles.loading]: loading,
[className]: className,
[className]: className
})}
type={htmlType}
onClick={this.handleClick}
Expand Down
1 change: 1 addition & 0 deletions src/components/Base/CodeMirror/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default class CodeMirrorX extends React.Component {
onChange={onChange}
options={{ mode, lineNumbers: true }}
ref="editor"
{...rest}
/>
);
}
Expand Down
Loading

0 comments on commit aa84b64

Please sign in to comment.