diff --git a/static/css/order-hardware.css b/static/css/order-hardware.css index cc75723..98d93ca 100644 --- a/static/css/order-hardware.css +++ b/static/css/order-hardware.css @@ -73,6 +73,10 @@ text-decoration: none; } +.options-invalid { + box-shadow: 0 0 0 .25rem rgba(229, 62, 62, 0.25)!important; + --bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important; +} /* ##Device = Tablets, Ipads (portrait) ##Screen = B/w 768px to 1024px diff --git a/static/js/shop/options/components/Line.jsx b/static/js/shop/options/components/Line.jsx index 48d77d9..6114b01 100644 --- a/static/js/shop/options/components/Line.jsx +++ b/static/js/shop/options/components/Line.jsx @@ -1,22 +1,25 @@ import React, {Component} from "react"; import {Tip} from "./Tip"; +import {Validation} from "../validation"; class Line extends Component { constructor(props) { super(props); // Initialize the state object with the initial values from the props this.state = { - text: props.outvar in props.data ? props.data[props.outvar] : (props.fallback ? props.fallback : "") + text: props.outvar in props.data ? props.data[props.outvar] : (props.fallback ? props.fallback : ""), + valid: true }; // Bind the event handler to this - this.handleClick = this.handleClick.bind(this); + this.handleChange = this.handleChange.bind(this); this.props.target.construct(this.props.outvar, this.state.text); } - handleClick(element) { + handleChange(element) { let text = element.target.value; this.setState({ - text: text + text: text, + valid: this.props.validator ? this.props.validator(text) : true }); this.props.target.update(this.props.outvar, text); } @@ -39,14 +42,14 @@ class Line extends Component { {this.props.title}: {this.props.tip && } - ); } } -export function LineWrapper(target, id, data, {title, fallback, outvar, icon, tip, classes}) { +export function LineWrapper(target, id, data, {title, fallback, outvar, icon, tip, classes, validator}) { return ; + id={id} data={data} classes={classes} validator={validator && Validation[validator.name](validator.params)}/>; } \ No newline at end of file diff --git a/static/js/shop/options/components/SwitchLine.jsx b/static/js/shop/options/components/SwitchLine.jsx index 7765da6..39dcd32 100644 --- a/static/js/shop/options/components/SwitchLine.jsx +++ b/static/js/shop/options/components/SwitchLine.jsx @@ -1,5 +1,6 @@ import React, {Component} from "react"; import {Tip} from "./Tip"; +import {Validation} from "../validation"; class SwitchLine extends Component { constructor(props) { @@ -7,7 +8,8 @@ class SwitchLine extends Component { // Initialize the state object with the initial values from the props this.state = { text: props.outvar in props.data ? props.data[props.outvar].text : (props.fallback ? props.fallback.text : ""), - checked: props.outvar in props.data ? props.data[props.outvar].checked : (props.fallback ? props.fallback.checked : false) + checked: props.outvar in props.data ? props.data[props.outvar].checked : (props.fallback ? props.fallback.checked : false), + valid: true }; // Bind the event handler to this this.handleText = this.handleText.bind(this); @@ -18,7 +20,8 @@ class SwitchLine extends Component { handleText(element) { let new_state = { ...this.state, - text: element.target.value + text: element.target.value, + valid: this.props.validator ? this.props.validator(element.target.value) : true } this.setState(new_state); this.props.target.update(this.props.outvar, new_state); @@ -39,6 +42,7 @@ class SwitchLine extends Component { return { checked: props.data[props.outvar].checked, text: props.data[props.outvar].text, + valid: this.props.validator ? this.props.validator(props.data[props.outvar].text) : true } } return null @@ -64,14 +68,14 @@ class SwitchLine extends Component { {this.props.tip && } - ); } } -export function SwitchLineWrapper(target, id, data, {title, fallback, outvar, icon, tip, classes}) { +export function SwitchLineWrapper(target, id, data, {title, fallback, outvar, icon, tip, classes, validator}) { return ; + id={id} data={data} classes={classes} validator={validator && Validation[validator.name](validator.params)}/>; } diff --git a/static/js/shop/options/validation.js b/static/js/shop/options/validation.js new file mode 100644 index 0000000..d0ee431 --- /dev/null +++ b/static/js/shop/options/validation.js @@ -0,0 +1,51 @@ + + +const ipv4 = (params) => { + const ipv4WithMaskPattern = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(0|1[0-9]|2[0-9]|3[0-2]|[0-9])$/; + return (text) => { + return ipv4WithMaskPattern.test(text); + } +} + +const ipv6 = (params) => { + const ipv6WithMaskPattern = /(^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\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}))|:)))(\/(\d{1,2}|1[0-1]\d|12[0-8]))(%.+)?\s*$)/; + + + return (text) => { + return ipv6WithMaskPattern.test(text); + } +} + +const ipv4or6 = (params) => { + const ipv4Local = ipv4(params); + const ipv6Local = ipv6(params); + return (text) => { + return ipv4Local(text) || ipv6Local(text); + } +} + +const frequency = (params) => { + const factors = { + "mhz": 1e6, + "khz": 1e3, + "hz": 1e1, + "ghz": 1e9, + }; + + return (text) => { + const splited = text.split(/(\s+)/); + const numerator = parseFloat(splited[0]); + if (splited.length !== 3 || isNaN(numerator)) return false; + const factor = factors[splited[2].toLowerCase()]; + if (!factor) return false; + const realFreq = factor * numerator; + return realFreq >= (params.min || 10*factors.mhz) && realFreq <= (params.max || 1*factors.ghz); + } +} + +export const Validation = { + ipv4: ipv4, + ipv6: ipv6, + ipv4or6: ipv4or6, + frequency: frequency +}; \ No newline at end of file diff --git a/static/js/shop_data.js b/static/js/shop_data.js index 039881e..f665fd2 100644 --- a/static/js/shop_data.js +++ b/static/js/shop_data.js @@ -194,9 +194,14 @@ const shop_data = { ] }, [ - {type: "Line", args: {title: "IPv4", outvar: "ipv4", fallback: "192.168.1.75/24", tip: "Set up IPv4 address used by core device"}}, - {type: "SwitchLine", args: {title: "IPv6", outvar: "ipv6"}}, - {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}, tip: "Use external clock reference: 10, 80 (beta), 100 or 125 MHz. Other variants may be provided if needed."}} + {type: "Line", args: {title: "IPv4", outvar: "ipv4", fallback: "192.168.1.75/24", + tip: "Set up IPv4 address and mask used by core device", validator: {name: "ipv4"}}}, + {type: "SwitchLine", args: {title: "IPv6", outvar: "ipv6", + tip: "Set up IPv6 address and prefix used by core device", + validator: {name: "ipv6"}}}, + {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}, + validator: {name: "frequency", params: {min: 10e6, max: 1e9}}, + tip: "Use external clock reference: 10, 80 (beta), 100 or 125 MHz. Other variants may be provided if needed."}} ], [ {type: "Switch", args: {title: "Optical fiber", outvar: "optics", tip: "Use optical fiber instead of direct attach copper cable"}}, @@ -262,9 +267,17 @@ const shop_data = { ] }, [ - {type: "Line", args: {title: "IPv4", outvar: "ipv4", fallback: "192.168.1.75/24", tip: "Set up IPv4 address used by core device"}}, - {type: "SwitchLine", args: {title: "IPv6", outvar: "ipv6"}}, - {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}, tip: "Use external clock reference: 10, 80 (beta), 100 or 125 MHz. Other variants may be provided if needed."}} + {type: "Line", args: {title: "IPv4", outvar: "ipv4", + validator: {name: "ipv4"}, + fallback: "192.168.1.75/24", + tip: "Set up IPv4 address used by core device"}}, + {type: "SwitchLine", args: {title: "IPv6", outvar: "ipv6", + tip: "Set up IPv6 address and prefix used by core device", + validator: {name: "ipv6"}}}, + {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", + validator: {name: "frequency", params: {min: 10e6, max: 1e9}}, + fallback: {text: "125 MHz", checked: false}, + tip: "Use external clock reference: 10, 80 (beta), 100 or 125 MHz. Other variants may be provided if needed."}} ], [ {type: "Switch", args: {title: "Optical fiber", outvar: "optics", tip: "Use optical fiber instead of direct attach copper cable"}}, @@ -658,7 +671,9 @@ const shop_data = { "if": [ {"var": "mono_eem"}, [ - {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}}}, + {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", + validator: {name: "frequency", params: {min: 10e6, max: 1e9}}, + fallback: {text: "125 MHz", checked: false}}}, ], [ {type: "Switch", args: {title: "Synchronization", outvar: "sync", tip: "Synchronize phases across Urukuls"}}, @@ -667,7 +682,9 @@ const shop_data = { {"var": "sync"}, null, [ - {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}}}, + {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", + validator: {name: "frequency", params: {min: 10e6, max: 1e9}}, + fallback: {text: "125 MHz", checked: false}}}, { "if": [ {"var": "ext_data.has_sampler"}, @@ -716,7 +733,9 @@ const shop_data = { datasheet_name: '4410/4412 Urukul datasheet', options: [ {type: "Switch", args: {title: "Use 1 EEM", outvar: "mono_eem", tip: "Use one EEM port setup. RF switch and synchronization will be unavailable."}}, - {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}}} + {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", + validator: {name: "frequency", params: {min: 10e6, max: 1e9}}, + fallback: {text: "125 MHz", checked: false}}} ], size: 'small', type: 'urukul', @@ -747,7 +766,9 @@ const shop_data = { 'The upconverter is optional, if you would like the baseband version please leave us a note.' ], options: [ - {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}}}, + {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", + validator: {name: "frequency", params: {min: 10e6, max: 1e9}}, + fallback: {text: "125 MHz", checked: false}}}, {type: "Radio", args: {title: "Variant", outvar: "variant", variants: ["Baseband", "Upconverter"], fallback: 1}}, ], size: 'small', @@ -1025,7 +1046,10 @@ const shop_data = { 'Can be controlled by Kasli or work stand-alone with PoE supply.' ], options: [ - {type: "SwitchLine", args: {title: "IP", outvar: "ip", fallback: {text: "DHCP", checked: false}, tip: "Set up IP address used by the device"}}, + {type: "SwitchLine", args: {title: "IP", outvar: "ip", + validator: {name: "ipv4or6"}, + fallback: {text: "DHCP", checked: false}, + tip: "Set up IP address used by the device"}}, {type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}}, {type: "Switch", args: {title: "Term #0", outvar: "term_0", tip: "Enable termination on ADC channel #0"}}, {type: "Switch", args: {title: "Term #1", outvar: "term_1", tip: "Enable termination on ADC channel #1"}} @@ -1057,7 +1081,9 @@ const shop_data = { 'Large frequency changes take several milliseconds.', ], options: [ - {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}}} + {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", + validator: {name: "frequency", params: {min: 10e6, max: 600e6}}, + fallback: {text: "125 MHz", checked: false}}} ], size: 'small', type: null, @@ -1084,7 +1110,9 @@ const shop_data = { 'Each Almazny channel outputs twice the frequency of its corresponding Mirny channel.', ], options: [ - {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}}} + {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", + validator: {name: "frequency", params: {min: 10e6, max: 600e6}}, + fallback: {text: "125 MHz", checked: false}}} ], size: 'big', type: null, @@ -1166,9 +1194,13 @@ const shop_data = { 'AD9959 DDS (500MSPS, 10-bit).' ], options: [ - {type: "SwitchLine", args: {title: "IP", outvar: "ip", fallback: {text: "DHCP", checked: false}, tip: "Set up IP address used by the device"}}, + {type: "SwitchLine", args: {title: "IP", outvar: "ip", + validator: {name: "ipv4or6"}, + fallback: {text: "DHCP", checked: false}, + tip: "Set up IP address used by the device"}}, {type: "Switch", args: {title: "Ext power", outvar: "ext_pwr", "tip": "Use external power supply in order to reduce number of used EEM connectors"}}, - {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", fallback: {text: "125 MHz", checked: false}}}, + {type: "SwitchLine", args: {title: "Ext CLK", outvar: "ext_clk", + fallback: {text: "125 MHz", checked: false}, validator: {name: "frequency", params: {min: 10e6, max: 1e9}}}}, {type: "Switch", args: {title: "Termination #0", outvar: "term_0", tip: "Enable termination on ADC channel #0"}}, {type: "Switch", args: {title: "Termination #1", outvar: "term_1", tip: "Enable termination on ADC channel #1"}} ],