Deleted .idea/.gitignore, .idea/clion.iml, .idea/misc.xml, .idea/modules.xml, .idea/platformio.iml, .idea/serialmonitor_settings.xml, .idea/vcs.xml, .idea/watcherTasks.xml files

This commit is contained in:
2020-12-08 15:12:15 +00:00
parent c28bde9f2b
commit 24f6a8a95f
208 changed files with 25332 additions and 335 deletions

View File

@ -0,0 +1,62 @@
import React, { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
import { Tabs, Tab } from '@material-ui/core';
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
import { MenuAppBar } from '../components';
import WiFiStatusController from './WiFiStatusController';
import WiFiSettingsController from './WiFiSettingsController';
import WiFiNetworkScanner from './WiFiNetworkScanner';
import { WiFiConnectionContext } from './WiFiConnectionContext';
import { WiFiNetwork } from './types';
type WiFiConnectionProps = AuthenticatedContextProps & RouteComponentProps;
class WiFiConnection extends Component<WiFiConnectionProps, WiFiConnectionContext> {
constructor(props: WiFiConnectionProps) {
super(props);
this.state = {
selectNetwork: this.selectNetwork,
deselectNetwork: this.deselectNetwork
};
}
selectNetwork = (network: WiFiNetwork) => {
this.setState({ selectedNetwork: network });
this.props.history.push('/wifi/settings');
}
deselectNetwork = () => {
this.setState({ selectedNetwork: undefined });
}
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
this.props.history.push(path);
};
render() {
const { authenticatedContext } = this.props;
return (
<WiFiConnectionContext.Provider value={this.state}>
<MenuAppBar sectionTitle="WiFi Connection">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tab value="/wifi/status" label="WiFi Status" />
<Tab value="/wifi/scan" label="Scan Networks" disabled={!authenticatedContext.me.admin} />
<Tab value="/wifi/settings" label="WiFi Settings" disabled={!authenticatedContext.me.admin} />
</Tabs>
<Switch>
<AuthenticatedRoute exact path="/wifi/status" component={WiFiStatusController} />
<AuthenticatedRoute exact path="/wifi/scan" component={WiFiNetworkScanner} />
<AuthenticatedRoute exact path="/wifi/settings" component={WiFiSettingsController} />
<Redirect to="/wifi/status" />
</Switch>
</MenuAppBar>
</WiFiConnectionContext.Provider>
)
}
}
export default withAuthenticatedContext(WiFiConnection);

View File

@ -0,0 +1,13 @@
import React from 'react';
import { WiFiNetwork } from './types';
export interface WiFiConnectionContext {
selectedNetwork?: WiFiNetwork;
selectNetwork: (network: WiFiNetwork) => void;
deselectNetwork: () => void;
}
const WiFiConnectionContextDefaultValue = {} as WiFiConnectionContext
export const WiFiConnectionContext = React.createContext(
WiFiConnectionContextDefaultValue
);

View File

@ -0,0 +1,168 @@
import React, { Component } from 'react';
import { withSnackbar, WithSnackbarProps } from 'notistack';
import { createStyles, WithStyles, Theme, withStyles, Typography, LinearProgress } from '@material-ui/core';
import PermScanWifiIcon from '@material-ui/icons/PermScanWifi';
import { FormActions, FormButton, SectionContent } from '../components';
import { redirectingAuthorizedFetch } from '../authentication';
import { SCAN_NETWORKS_ENDPOINT, LIST_NETWORKS_ENDPOINT } from '../api';
import WiFiNetworkSelector from './WiFiNetworkSelector';
import { WiFiNetworkList, WiFiNetwork } from './types';
const NUM_POLLS = 10
const POLLING_FREQUENCY = 500
const RETRY_EXCEPTION_TYPE = "retry"
interface WiFiNetworkScannerState {
scanningForNetworks: boolean;
errorMessage?: string;
networkList?: WiFiNetworkList;
}
const styles = (theme: Theme) => createStyles({
scanningSettings: {
margin: theme.spacing(0.5),
},
scanningSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
},
scanningProgress: {
margin: theme.spacing(4),
textAlign: "center"
}
});
type WiFiNetworkScannerProps = WithSnackbarProps & WithStyles<typeof styles>;
class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkScannerState> {
pollCount: number = 0;
state: WiFiNetworkScannerState = {
scanningForNetworks: false,
};
componentDidMount() {
this.scanNetworks();
}
requestNetworkScan = () => {
const { scanningForNetworks } = this.state;
if (!scanningForNetworks) {
this.scanNetworks();
}
}
scanNetworks() {
this.pollCount = 0;
this.setState({ scanningForNetworks: true, networkList: undefined, errorMessage: undefined });
redirectingAuthorizedFetch(SCAN_NETWORKS_ENDPOINT).then(response => {
if (response.status === 202) {
this.schedulePollTimeout();
return;
}
throw Error("Scanning for networks returned unexpected response code: " + response.status);
}).catch(error => {
this.props.enqueueSnackbar("Problem scanning: " + error.message, {
variant: 'error',
});
this.setState({ scanningForNetworks: false, networkList: undefined, errorMessage: error.message });
});
}
schedulePollTimeout() {
setTimeout(this.pollNetworkList, POLLING_FREQUENCY);
}
retryError() {
return {
name: RETRY_EXCEPTION_TYPE,
message: "Network list not ready, will retry in " + POLLING_FREQUENCY + "ms."
};
}
compareNetworks(network1: WiFiNetwork, network2: WiFiNetwork) {
if (network1.rssi < network2.rssi)
return 1;
if (network1.rssi > network2.rssi)
return -1;
return 0;
}
pollNetworkList = () => {
redirectingAuthorizedFetch(LIST_NETWORKS_ENDPOINT)
.then(response => {
if (response.status === 200) {
return response.json();
}
if (response.status === 202) {
if (++this.pollCount < NUM_POLLS) {
this.schedulePollTimeout();
throw this.retryError();
} else {
throw Error("Device did not return network list in timely manner.");
}
}
throw Error("Device returned unexpected response code: " + response.status);
})
.then(json => {
json.networks.sort(this.compareNetworks)
this.setState({ scanningForNetworks: false, networkList: json, errorMessage: undefined })
})
.catch(error => {
if (error.name !== RETRY_EXCEPTION_TYPE) {
this.props.enqueueSnackbar("Problem scanning: " + error.message, {
variant: 'error',
});
this.setState({ scanningForNetworks: false, networkList: undefined, errorMessage: error.message });
}
});
}
renderNetworkScanner() {
const { classes } = this.props;
const { scanningForNetworks, networkList, errorMessage } = this.state;
if (scanningForNetworks || !networkList) {
return (
<div className={classes.scanningSettings}>
<LinearProgress className={classes.scanningSettingsDetails} />
<Typography variant="h6" className={classes.scanningProgress}>
Scanning&hellip;
</Typography>
</div>
);
}
if (errorMessage) {
return (
<div className={classes.scanningSettings}>
<Typography variant="h6" className={classes.scanningSettingsDetails}>
{errorMessage}
</Typography>
</div>
);
}
return (
<WiFiNetworkSelector networkList={networkList} />
);
}
render() {
const { scanningForNetworks } = this.state;
return (
<SectionContent title="Network Scanner">
{this.renderNetworkScanner()}
<FormActions>
<FormButton startIcon={<PermScanWifiIcon />} variant="contained" color="secondary" onClick={this.requestNetworkScan} disabled={scanningForNetworks}>
Scan again&hellip;
</FormButton>
</FormActions>
</SectionContent>
);
}
}
export default withSnackbar(withStyles(styles)(WiFiNetworkScanner));

View File

@ -0,0 +1,54 @@
import React, { Component } from 'react';
import { Avatar, Badge } from '@material-ui/core';
import { List, ListItem, ListItemIcon, ListItemText, ListItemAvatar } from '@material-ui/core';
import WifiIcon from '@material-ui/icons/Wifi';
import LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen';
import { isNetworkOpen, networkSecurityMode } from './WiFiSecurityModes';
import { WiFiConnectionContext } from './WiFiConnectionContext';
import { WiFiNetwork, WiFiNetworkList } from './types';
interface WiFiNetworkSelectorProps {
networkList: WiFiNetworkList;
}
class WiFiNetworkSelector extends Component<WiFiNetworkSelectorProps> {
static contextType = WiFiConnectionContext;
context!: React.ContextType<typeof WiFiConnectionContext>;
renderNetwork = (network: WiFiNetwork) => {
return (
<ListItem key={network.bssid} button onClick={() => this.context.selectNetwork(network)}>
<ListItemAvatar>
<Avatar>
{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={network.ssid}
secondary={"Security: " + networkSecurityMode(network) + ", Ch: " + network.channel}
/>
<ListItemIcon>
<Badge badgeContent={network.rssi + "db"}>
<WifiIcon />
</Badge>
</ListItemIcon>
</ListItem>
);
}
render() {
return (
<List>
{this.props.networkList.networks.map(this.renderNetwork)}
</List>
);
}
}
export default WiFiNetworkSelector;

View File

@ -0,0 +1,21 @@
import { WiFiNetwork, WiFiEncryptionType } from "./types";
export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) => encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN;
export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
switch (encryption_type) {
case WiFiEncryptionType.WIFI_AUTH_WEP:
case WiFiEncryptionType.WIFI_AUTH_WEP_PSK:
return "WEP";
case WiFiEncryptionType.WIFI_AUTH_WEP2_PSK:
return "WEP2";
case WiFiEncryptionType.WIFI_AUTH_WPA_WPA2_PSK:
return "WPA/WEP2";
case WiFiEncryptionType.WIFI_AUTH_WPA2_ENTERPRISE:
return "WEP2 Enterprise";
case WiFiEncryptionType.WIFI_AUTH_OPEN:
return "None";
default:
return "Unknown";
}
}

View File

@ -0,0 +1,29 @@
import React, { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import WiFiSettingsForm from './WiFiSettingsForm';
import { WIFI_SETTINGS_ENDPOINT } from '../api';
import { WiFiSettings } from './types';
type WiFiSettingsControllerProps = RestControllerProps<WiFiSettings>;
class WiFiSettingsController extends Component<WiFiSettingsControllerProps> {
componentDidMount() {
this.props.loadData();
}
render() {
return (
<SectionContent title="WiFi Settings">
<RestFormLoader
{...this.props}
render={formProps => <WiFiSettingsForm {...formProps} />}
/>
</SectionContent>
);
}
}
export default restController(WIFI_SETTINGS_ENDPOINT, WiFiSettingsController);

View File

@ -0,0 +1,200 @@
import React, { Fragment } from 'react';
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
import { Checkbox, List, ListItem, ListItemText, ListItemAvatar, ListItemSecondaryAction } from '@material-ui/core';
import Avatar from '@material-ui/core/Avatar';
import IconButton from '@material-ui/core/IconButton';
import LockIcon from '@material-ui/icons/Lock';
import LockOpenIcon from '@material-ui/icons/LockOpen';
import DeleteIcon from '@material-ui/icons/Delete';
import SaveIcon from '@material-ui/icons/Save';
import { RestFormProps, PasswordValidator, BlockFormControlLabel, FormActions, FormButton } from '../components';
import { isIP, isHostname, optional } from '../validators';
import { WiFiConnectionContext } from './WiFiConnectionContext';
import { isNetworkOpen, networkSecurityMode } from './WiFiSecurityModes';
import { WiFiSettings } from './types';
type WiFiStatusFormProps = RestFormProps<WiFiSettings>;
class WiFiSettingsForm extends React.Component<WiFiStatusFormProps> {
static contextType = WiFiConnectionContext;
context!: React.ContextType<typeof WiFiConnectionContext>;
constructor(props: WiFiStatusFormProps, context: WiFiConnectionContext) {
super(props);
const { selectedNetwork } = context;
if (selectedNetwork) {
const wifiSettings: WiFiSettings = {
ssid: selectedNetwork.ssid,
password: "",
hostname: props.data.hostname,
static_ip_config: false,
}
props.setData(wifiSettings);
}
}
componentWillMount() {
ValidatorForm.addValidationRule('isIP', isIP);
ValidatorForm.addValidationRule('isHostname', isHostname);
ValidatorForm.addValidationRule('isOptionalIP', optional(isIP));
}
deselectNetworkAndLoadData = () => {
this.context.deselectNetwork();
this.props.loadData();
}
componentWillUnmount() {
this.context.deselectNetwork();
}
render() {
const { selectedNetwork, deselectNetwork } = this.context;
const { data, handleValueChange, saveData } = this.props;
return (
<ValidatorForm onSubmit={saveData} ref="WiFiSettingsForm">
{
selectedNetwork ?
<List>
<ListItem>
<ListItemAvatar>
<Avatar>
{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={selectedNetwork.ssid}
secondary={"Security: " + networkSecurityMode(selectedNetwork) + ", Ch: " + selectedNetwork.channel}
/>
<ListItemSecondaryAction>
<IconButton aria-label="Manual Config" onClick={deselectNetwork}>
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
</List>
:
<TextValidator
validators={['matchRegexp:^.{0,32}$']}
errorMessages={['SSID must be 32 characters or less']}
name="ssid"
label="SSID"
fullWidth
variant="outlined"
value={data.ssid}
onChange={handleValueChange('ssid')}
margin="normal"
/>
}
{
(!selectedNetwork || !isNetworkOpen(selectedNetwork)) &&
<PasswordValidator
validators={['matchRegexp:^.{0,64}$']}
errorMessages={['Password must be 64 characters or less']}
name="password"
label="Password"
fullWidth
variant="outlined"
value={data.password}
onChange={handleValueChange('password')}
margin="normal"
/>
}
<TextValidator
validators={['required', 'isHostname']}
errorMessages={['Hostname is required', "Not a valid hostname"]}
name="hostname"
label="Hostname"
fullWidth
variant="outlined"
value={data.hostname}
onChange={handleValueChange('hostname')}
margin="normal"
/>
<BlockFormControlLabel
control={
<Checkbox
value="static_ip_config"
checked={data.static_ip_config}
onChange={handleValueChange("static_ip_config")}
/>
}
label="Static IP Config?"
/>
{
data.static_ip_config &&
<Fragment>
<TextValidator
validators={['required', 'isIP']}
errorMessages={['Local IP is required', 'Must be an IP address']}
name="local_ip"
label="Local IP"
fullWidth
variant="outlined"
value={data.local_ip}
onChange={handleValueChange('local_ip')}
margin="normal"
/>
<TextValidator
validators={['required', 'isIP']}
errorMessages={['Gateway IP is required', 'Must be an IP address']}
name="gateway_ip"
label="Gateway"
fullWidth
variant="outlined"
value={data.gateway_ip}
onChange={handleValueChange('gateway_ip')}
margin="normal"
/>
<TextValidator
validators={['required', 'isIP']}
errorMessages={['Subnet mask is required', 'Must be an IP address']}
name="subnet_mask"
label="Subnet"
fullWidth
variant="outlined"
value={data.subnet_mask}
onChange={handleValueChange('subnet_mask')}
margin="normal"
/>
<TextValidator
validators={['isOptionalIP']}
errorMessages={['Must be an IP address']}
name="dns_ip_1"
label="DNS IP #1"
fullWidth
variant="outlined"
value={data.dns_ip_1}
onChange={handleValueChange('dns_ip_1')}
margin="normal"
/>
<TextValidator
validators={['isOptionalIP']}
errorMessages={['Must be an IP address']}
name="dns_ip_2"
label="DNS IP #2"
fullWidth
variant="outlined"
value={data.dns_ip_2}
onChange={handleValueChange('dns_ip_2')}
margin="normal"
/>
</Fragment>
}
<FormActions>
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
Save
</FormButton>
</FormActions>
</ValidatorForm>
);
}
}
export default WiFiSettingsForm;

View File

@ -0,0 +1,41 @@
import { Theme } from '@material-ui/core';
import { WiFiStatus, WiFiConnectionStatus } from './types';
export const isConnected = ({ status }: WiFiStatus) => status === WiFiConnectionStatus.WIFI_STATUS_CONNECTED;
export const wifiStatusHighlight = ({ status }: WiFiStatus, theme: Theme) => {
switch (status) {
case WiFiConnectionStatus.WIFI_STATUS_IDLE:
case WiFiConnectionStatus.WIFI_STATUS_DISCONNECTED:
case WiFiConnectionStatus.WIFI_STATUS_NO_SHIELD:
return theme.palette.info.main;
case WiFiConnectionStatus.WIFI_STATUS_CONNECTED:
return theme.palette.success.main;
case WiFiConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
case WiFiConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
return theme.palette.error.main;
default:
return theme.palette.warning.main;
}
}
export const wifiStatus = ({ status }: WiFiStatus) => {
switch (status) {
case WiFiConnectionStatus.WIFI_STATUS_NO_SHIELD:
return "Inactive";
case WiFiConnectionStatus.WIFI_STATUS_IDLE:
return "Idle";
case WiFiConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
return "No SSID Available";
case WiFiConnectionStatus.WIFI_STATUS_CONNECTED:
return "Connected";
case WiFiConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
return "Connection Failed";
case WiFiConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
return "Connection Lost";
case WiFiConnectionStatus.WIFI_STATUS_DISCONNECTED:
return "Disconnected";
default:
return "Unknown";
}
}

View File

@ -0,0 +1,29 @@
import React, { Component } from 'react';
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import WiFiStatusForm from './WiFiStatusForm';
import { WIFI_STATUS_ENDPOINT } from '../api';
import { WiFiStatus } from './types';
type WiFiStatusControllerProps = RestControllerProps<WiFiStatus>;
class WiFiStatusController extends Component<WiFiStatusControllerProps> {
componentDidMount() {
this.props.loadData();
}
render() {
return (
<SectionContent title="WiFi Status">
<RestFormLoader
{...this.props}
render={formProps => <WiFiStatusForm {...formProps} />}
/>
</SectionContent>
);
}
}
export default restController(WIFI_STATUS_ENDPOINT, WiFiStatusController);

View File

@ -0,0 +1,117 @@
import React, { Component, Fragment } from 'react';
import { WithTheme, withTheme } from '@material-ui/core/styles';
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
import DNSIcon from '@material-ui/icons/Dns';
import WifiIcon from '@material-ui/icons/Wifi';
import SettingsInputComponentIcon from '@material-ui/icons/SettingsInputComponent';
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
import RefreshIcon from '@material-ui/icons/Refresh';
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
import { wifiStatus, wifiStatusHighlight, isConnected } from './WiFiStatus';
import { WiFiStatus } from './types';
type WiFiStatusFormProps = RestFormProps<WiFiStatus> & WithTheme;
class WiFiStatusForm extends Component<WiFiStatusFormProps> {
dnsServers(status: WiFiStatus) {
if (!status.dns_ip_1) {
return "none";
}
return status.dns_ip_1 + (status.dns_ip_2 ? ',' + status.dns_ip_2 : '');
}
createListItems() {
const { data, theme } = this.props
return (
<Fragment>
<ListItem>
<ListItemAvatar>
<HighlightAvatar color={wifiStatusHighlight(data, theme)}>
<WifiIcon />
</HighlightAvatar>
</ListItemAvatar>
<ListItemText primary="Status" secondary={wifiStatus(data)} />
</ListItem>
<Divider variant="inset" component="li" />
{
isConnected(data) &&
<Fragment>
<ListItem>
<ListItemAvatar>
<Avatar>
<SettingsInputAntennaIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="SSID" secondary={data.ssid} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>IP</Avatar>
</ListItemAvatar>
<ListItemText primary="IP Address" secondary={data.local_ip} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>
<DeviceHubIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="MAC Address" secondary={data.mac_address} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>#</Avatar>
</ListItemAvatar>
<ListItemText primary="Subnet Mask" secondary={data.subnet_mask} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>
<SettingsInputComponentIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Gateway IP" secondary={data.gateway_ip || "none"} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>
<DNSIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="DNS Server IP" secondary={this.dnsServers(data)} />
</ListItem>
<Divider variant="inset" component="li" />
</Fragment>
}
</Fragment>
);
}
render() {
return (
<Fragment>
<List>
{this.createListItems()}
</List>
<FormActions>
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
Refresh
</FormButton>
</FormActions>
</Fragment>
);
}
}
export default withTheme(WiFiStatusForm);

View File

@ -0,0 +1,56 @@
export enum WiFiConnectionStatus {
WIFI_STATUS_IDLE = 0,
WIFI_STATUS_NO_SSID_AVAIL = 1,
WIFI_STATUS_CONNECTED = 3,
WIFI_STATUS_CONNECT_FAILED = 4,
WIFI_STATUS_CONNECTION_LOST = 5,
WIFI_STATUS_DISCONNECTED = 6,
WIFI_STATUS_NO_SHIELD = 255
}
export enum WiFiEncryptionType {
WIFI_AUTH_OPEN = 0,
WIFI_AUTH_WEP = 1,
WIFI_AUTH_WEP_PSK = 2,
WIFI_AUTH_WEP2_PSK = 3,
WIFI_AUTH_WPA_WPA2_PSK = 4,
WIFI_AUTH_WPA2_ENTERPRISE = 5
}
export interface WiFiStatus {
status: WiFiConnectionStatus;
local_ip: string;
mac_address: string;
rssi: number;
ssid: string;
bssid: string;
channel: number;
subnet_mask: string;
gateway_ip: string;
dns_ip_1: string;
dns_ip_2: string;
}
export interface WiFiSettings {
ssid: string;
password: string;
hostname: string;
static_ip_config: boolean;
local_ip?: string;
gateway_ip?: string;
subnet_mask?: string;
dns_ip_1?: string;
dns_ip_2?: string;
}
export interface WiFiNetworkList {
networks: WiFiNetwork[];
}
export interface WiFiNetwork {
rssi: number;
ssid: string;
bssid: string;
channel: number;
encryption_type: WiFiEncryptionType;
}