diff options
| author | 2022-09-29 12:02:41 +0200 | |
|---|---|---|
| committer | 2022-09-29 12:02:41 +0200 | |
| commit | 938328cd077d40b75e0834d56ff8d43ad035fd2b (patch) | |
| tree | 76ed59a9adf8a40e83c99a3ea34ce7cb5a5f8877 /web/source/settings-panel/admin | |
| parent | [chore] simplify generating log entry caller information (#863) (diff) | |
| download | gotosocial-938328cd077d40b75e0834d56ff8d43ad035fd2b.tar.xz | |
[frontend] Unified panels (#812)
* settings panel restructuring
* clean up old Gin handlers
* colorscheme redesign, some other small css tweaks
* basic router layout, error boundary
* colorscheme redesign, some other small css tweaks
* kebab-case consistency
* superfluous padding on applist
* remove unused consts
* redux, whitespace changes..
* use .jsx extensions for components
* login flow up till app registration
* full redux oauth implementation, with basic error handling
* split oauth api functions
* oauth api revocation handling
* basic profile change submission
* move old dir
* profile overview
* fix keeping track of the wrong instance url (for different instance/api domains)
* use redux state for profile form
* delete old/index.js, old/basic.js, fully implemented
* implement old/user/profile.js
* implement password change
* remove debug logging
* support future api for removing files
* customize profile css
* remove unneeded wrapper components
* restructure form fields
* start on admin pages
* admin panel settings
* admin settings panel
* remove old/admin files
* add top-level redirect
* refactor/cleanup forms
* only do API checks on logged-in state
* admin-status based routing
* federation block routing
* federation blocks
* upgrade dependencies
* react 18 changes
* media cleanup
* fix useEffect hooks
* remove unused require
* custom emoji base
* emoji uploader
* delete last old panel files
* sidebar styling, remove unused page
* refactor submit functions
* fix sidebar boxshadow-border
* fix old css variables
* fix fake-toot avatar
* fix non-square emoji
* fix user settings redux keys
* properly get admin account contact from instance response
* Account.source default values
* source.status_format key
* mobile responsiveness
* mobile element tweaks
* proper redirect after removing block
* add redirects for old setting panel urls
* deletes
* fix mobile overflow
* clean up debug logging calls
Diffstat (limited to 'web/source/settings-panel/admin')
| -rw-r--r-- | web/source/settings-panel/admin/actions.js | 61 | ||||
| -rw-r--r-- | web/source/settings-panel/admin/emoji.js | 212 | ||||
| -rw-r--r-- | web/source/settings-panel/admin/federation.js | 382 | ||||
| -rw-r--r-- | web/source/settings-panel/admin/settings.js | 110 | 
4 files changed, 765 insertions, 0 deletions
diff --git a/web/source/settings-panel/admin/actions.js b/web/source/settings-panel/admin/actions.js new file mode 100644 index 000000000..d4980d021 --- /dev/null +++ b/web/source/settings-panel/admin/actions.js @@ -0,0 +1,61 @@ +/* +	GoToSocial +	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + +	This program is free software: you can redistribute it and/or modify +	it under the terms of the GNU Affero General Public License as published by +	the Free Software Foundation, either version 3 of the License, or +	(at your option) any later version. + +	This program is distributed in the hope that it will be useful, +	but WITHOUT ANY WARRANTY; without even the implied warranty of +	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +	GNU Affero General Public License for more details. + +	You should have received a copy of the GNU Affero General Public License +	along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +"use strict"; + +const Promise = require("bluebird"); +const React = require("react"); +const Redux = require("react-redux"); + +const Submit = require("../components/submit"); + +const api = require("../lib/api"); +const submit = require("../lib/submit"); + +module.exports = function AdminActionPanel() { +	const dispatch = Redux.useDispatch(); + +	const [days, setDays] = React.useState(30); + +	const [errorMsg, setError] = React.useState(""); +	const [statusMsg, setStatus] = React.useState(""); + +	const removeMedia = submit( +		() => dispatch(api.admin.mediaCleanup(days)), +		{setStatus, setError} +	); + +	return ( +		<> +			<h1>Admin Actions</h1> +			<div> +				<h2>Media cleanup</h2> +				<p> +					Clean up remote media older than the specified number of days. +					If the remote instance is still online they will be refetched when needed. +					Also cleans up unused headers and avatars from the media cache. +				</p> +				<div> +					<label htmlFor="days">Days: </label> +					<input id="days" type="number" value={days} onChange={(e) => setDays(e.target.value)}/> +				</div> +				<Submit onClick={removeMedia} label="Remove media" errorMsg={errorMsg} statusMsg={statusMsg} /> +			</div> +		</> +	); +};
\ No newline at end of file diff --git a/web/source/settings-panel/admin/emoji.js b/web/source/settings-panel/admin/emoji.js new file mode 100644 index 000000000..1ef4a54a3 --- /dev/null +++ b/web/source/settings-panel/admin/emoji.js @@ -0,0 +1,212 @@ +/* +	GoToSocial +	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + +	This program is free software: you can redistribute it and/or modify +	it under the terms of the GNU Affero General Public License as published by +	the Free Software Foundation, either version 3 of the License, or +	(at your option) any later version. + +	This program is distributed in the hope that it will be useful, +	but WITHOUT ANY WARRANTY; without even the implied warranty of +	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +	GNU Affero General Public License for more details. + +	You should have received a copy of the GNU Affero General Public License +	along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +"use strict"; + +const Promise = require("bluebird"); +const React = require("react"); +const Redux = require("react-redux"); +const {Switch, Route, Link, Redirect, useRoute, useLocation} = require("wouter"); + +const Submit = require("../components/submit"); +const FakeToot = require("../components/fake-toot"); +const { formFields } = require("../components/form-fields"); + +const api = require("../lib/api"); +const adminActions = require("../redux/reducers/admin").actions; +const submit = require("../lib/submit"); + +const base = "/settings/admin/custom-emoji"; + +module.exports = function CustomEmoji() { +	return ( +		<Switch> +			<Route path={`${base}/:emojiId`}> +				<EmojiDetailWrapped /> +			</Route> +			<EmojiOverview /> +		</Switch> +	); +}; + +function EmojiOverview() { +	const dispatch = Redux.useDispatch(); +	const [loaded, setLoaded] = React.useState(false); + +	const [errorMsg, setError] = React.useState(""); + +	React.useEffect(() => { +		if (!loaded) { +			Promise.try(() => { +				return dispatch(api.admin.fetchCustomEmoji()); +			}).then(() => { +				setLoaded(true); +			}).catch((e) => { +				setLoaded(true); +				setError(e.message); +			}); +		} +	}, []); + +	if (!loaded) { +		return ( +			<> +				<h1>Custom Emoji</h1> +				Loading... +			</> +		); +	} + +	return ( +		<> +			<h1>Custom Emoji</h1> +			<EmojiList/> +			<NewEmoji/> +			{errorMsg.length > 0 &&  +				<div className="error accent">{errorMsg}</div> +			} +		</> +	); +} + +const NewEmojiForm = formFields(adminActions.updateNewEmojiVal, (state) => state.admin.newEmoji); +function NewEmoji() { +	const dispatch = Redux.useDispatch(); +	const newEmojiForm = Redux.useSelector((state) => state.admin.newEmoji); + +	const [errorMsg, setError] = React.useState(""); +	const [statusMsg, setStatus] = React.useState(""); + +	const uploadEmoji = submit( +		() => dispatch(api.admin.newEmoji()), +		{ +			setStatus, setError, +			onSuccess: function() { +				URL.revokeObjectURL(newEmojiForm.image); +				return Promise.all([ +					dispatch(adminActions.updateNewEmojiVal(["image", undefined])), +					dispatch(adminActions.updateNewEmojiVal(["imageFile", undefined])), +					dispatch(adminActions.updateNewEmojiVal(["shortcode", ""])), +				]); +			} +		} +	); + +	React.useEffect(() => { +		if (newEmojiForm.shortcode.length == 0) { +			if (newEmojiForm.imageFile != undefined) { +				let [name, ext] = newEmojiForm.imageFile.name.split("."); +				dispatch(adminActions.updateNewEmojiVal(["shortcode", name])); +			} +		} +	}); + +	let emojiOrShortcode = `:${newEmojiForm.shortcode}:`; + +	if (newEmojiForm.image != undefined) { +		emojiOrShortcode = <img +			className="emoji" +			src={newEmojiForm.image} +			title={`:${newEmojiForm.shortcode}:`} +			alt={newEmojiForm.shortcode} +		/>; +	} + +	return ( +		<div> +			<h2>Add new custom emoji</h2> + +			<FakeToot> +				Look at this new custom emoji {emojiOrShortcode} isn't it cool? +			</FakeToot> + +			<NewEmojiForm.File +				id="image" +				name="Image" +				fileType="image/png,image/gif" +				showSize={true} +				maxSize={50 * 1000} +			/> + +			<NewEmojiForm.TextInput +				id="shortcode" +				name="Shortcode (without : :), must be unique on the instance" +				placeHolder="blobcat" +			/> + +			<Submit onClick={uploadEmoji} label="Upload" errorMsg={errorMsg} statusMsg={statusMsg} /> +		</div> +	); +} + +function EmojiList() { +	const emoji = Redux.useSelector((state) => state.admin.emoji); + +	return ( +		<div> +			<h2>Overview</h2> +			<div className="list emoji-list"> +				{Object.entries(emoji).map(([category, entries]) => { +					return <EmojiCategory key={category} category={category} entries={entries}/>; +				})} +			</div> +		</div> +	); +} + +function EmojiCategory({category, entries}) { +	return ( +		<div className="entry"> +			<b>{category}</b> +			<div className="emoji-group"> +				{entries.map((e) => { +					return ( +						// <Link key={e.static_url} to={`${base}/${e.shortcode}`}> +						<Link key={e.static_url} to={`${base}`}> +							<a> +								<img src={e.static_url} alt={e.shortcode} title={`:${e.shortcode}:`}/> +							</a> +						</Link> +					); +				})} +			</div> +		</div> +	); +} + +function EmojiDetailWrapped() { +	/* We wrap the component to generate formFields with a setter depending on the domain +		 if formFields() is used inside the same component that is re-rendered with their state, +		 inputs get re-created on every change, causing them to lose focus, and bad performance +	*/ +	let [_match, {emojiId}] = useRoute(`${base}/:emojiId`); + +	function alterEmoji([key, val]) { +		return adminActions.updateDomainBlockVal([emojiId, key, val]); +	} + +	const fields = formFields(alterEmoji, (state) => state.admin.blockedInstances[emojiId]); + +	return <EmojiDetail id={emojiId} Form={fields} />; +} + +function EmojiDetail({id, Form}) { +	return ( +		"Not implemented yet" +	); +}
\ No newline at end of file diff --git a/web/source/settings-panel/admin/federation.js b/web/source/settings-panel/admin/federation.js new file mode 100644 index 000000000..7afc3c699 --- /dev/null +++ b/web/source/settings-panel/admin/federation.js @@ -0,0 +1,382 @@ +/* +	GoToSocial +	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + +	This program is free software: you can redistribute it and/or modify +	it under the terms of the GNU Affero General Public License as published by +	the Free Software Foundation, either version 3 of the License, or +	(at your option) any later version. + +	This program is distributed in the hope that it will be useful, +	but WITHOUT ANY WARRANTY; without even the implied warranty of +	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +	GNU Affero General Public License for more details. + +	You should have received a copy of the GNU Affero General Public License +	along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +"use strict"; + +const Promise = require("bluebird"); +const React = require("react"); +const Redux = require("react-redux"); +const {Switch, Route, Link, Redirect, useRoute, useLocation} = require("wouter"); +const fileDownload = require("js-file-download"); + +const { formFields } = require("../components/form-fields"); + +const api = require("../lib/api"); +const adminActions = require("../redux/reducers/admin").actions; +const submit = require("../lib/submit"); + +const base = "/settings/admin/federation"; + +// const { +// 	TextInput, +// 	TextArea, +// 	File +// } = require("../components/form-fields").formFields(adminActions.setAdminSettingsVal, (state) => state.instances.adminSettings); + +module.exports = function AdminSettings() { +	const dispatch = Redux.useDispatch(); +	// const instance = Redux.useSelector(state => state.instances.adminSettings); +	const loadedBlockedInstances = Redux.useSelector(state => state.admin.loadedBlockedInstances); + +	React.useEffect(() => { +		if (!loadedBlockedInstances ) { +			Promise.try(() => { +				return dispatch(api.admin.fetchDomainBlocks()); +			}); +		} +	}, []); + +	if (!loadedBlockedInstances) { +		return ( +			<div> +				<h1>Federation</h1> +				Loading... +			</div> +		); +	} + +	return ( +		<Switch> +			<Route path={`${base}/:domain`}> +				<InstancePageWrapped /> +			</Route> +			<InstanceOverview /> +		</Switch> +	); +}; + +function InstanceOverview() { +	const [filter, setFilter] = React.useState(""); +	const blockedInstances = Redux.useSelector(state => state.admin.blockedInstances); +	const [_location, setLocation] = useLocation(); + +	function filterFormSubmit(e) { +		e.preventDefault(); +		setLocation(`${base}/${filter}`); +	} + +	return ( +		<> +			<h1>Federation</h1> +			Here you can see an overview of blocked instances. + +			<div className="instance-list"> +				<h2>Blocked instances</h2> +				<form action={`${base}/view`} className="filter" role="search" onSubmit={filterFormSubmit}> +					<input name="domain" value={filter} onChange={(e) => setFilter(e.target.value)}/> +					<Link to={`${base}/${filter}`}><a className="button">Add block</a></Link> +				</form> +				<div className="list"> +					{Object.values(blockedInstances).filter((a) => a.domain.startsWith(filter)).map((entry) => { +						return ( +							<Link key={entry.domain} to={`${base}/${entry.domain}`}> +								<a className="entry nounderline"> +									<span id="domain"> +										{entry.domain} +									</span> +									<span id="date"> +										{new Date(entry.created_at).toLocaleString()} +									</span> +								</a> +							</Link> +						); +					})} +				</div> +			</div> + +			<BulkBlocking/> +		</> +	); +} + +const Bulk = formFields(adminActions.updateBulkBlockVal, (state) => state.admin.bulkBlock); +function BulkBlocking() { +	const dispatch = Redux.useDispatch(); +	const {bulkBlock, blockedInstances} = Redux.useSelector(state => state.admin); + +	const [errorMsg, setError] = React.useState(""); +	const [statusMsg, setStatus] = React.useState(""); + +	function importBlocks() { +		setStatus("Processing"); +		setError(""); +		return Promise.try(() => { +			return dispatch(api.admin.bulkDomainBlock()); +		}).then(({success, invalidDomains}) => { +			return Promise.try(() => { +				return resetBulk(); +			}).then(() => { +				dispatch(adminActions.updateBulkBlockVal(["list", invalidDomains.join("\n")])); + +				let stat = ""; +				if (success == 0) { +					return setError("No valid domains in import"); +				} else if (success == 1) { +					stat = "Imported 1 domain"; +				} else { +					stat = `Imported ${success} domains`; +				} + +				if (invalidDomains.length > 0) { +					if (invalidDomains.length == 1) { +						stat += ", input contained 1 invalid domain."; +					} else { +						stat += `, input contained ${invalidDomains.length} invalid domains.`; +					} +				} else { +					stat += "!"; +				} + +				setStatus(stat); +			}); +		}).catch((e) => { +			console.error(e); +			setError(e.message); +			setStatus(""); +		}); +	} + +	function exportBlocks() { +		return Promise.try(() => { +			setStatus("Exporting"); +			setError(""); +			let asJSON = bulkBlock.exportType.startsWith("json"); +			let _asCSV = bulkBlock.exportType.startsWith("csv"); + +			let exportList = Object.values(blockedInstances).map((entry) => { +				if (asJSON) { +					return { +						domain: entry.domain, +						public_comment: entry.public_comment +					}; +				} else { +					return entry.domain; +				} +			}); +			 +			if (bulkBlock.exportType == "json") { +				return dispatch(adminActions.updateBulkBlockVal(["list", JSON.stringify(exportList)])); +			} else if (bulkBlock.exportType == "json-download") { +				return fileDownload(JSON.stringify(exportList), "block-export.json"); +			} else if (bulkBlock.exportType == "plain") { +				return dispatch(adminActions.updateBulkBlockVal(["list", exportList.join("\n")])); +			} +		}).then(() => { +			setStatus("Exported!"); +		}).catch((e) => { +			setError(e.message); +			setStatus(""); +		}); +	} + +	function resetBulk(e) { +		if (e != undefined) { +			e.preventDefault(); +		} +		return dispatch(adminActions.resetBulkBlockVal()); +	} + +	function disableInfoFields(props={}) { +		if (bulkBlock.list[0] == "[") { +			return { +				...props, +				disabled: true, +				placeHolder: "Domain list is a JSON import, input disabled" +			}; +		} else { +			return props; +		} +	} + +	return ( +		<div className="bulk"> +			<h2>Import / Export <a onClick={resetBulk}>reset</a></h2> +			<Bulk.TextArea +				id="list" +				name="Domains, one per line" +				placeHolder={`google.com\nfacebook.com`} +			/> + +			<Bulk.TextArea +				id="public_comment" +				name="Public comment" +				inputProps={disableInfoFields({rows: 3})} +			/> + +			<Bulk.TextArea +				id="private_comment" +				name="Private comment" +				inputProps={disableInfoFields({rows: 3})} +			/> + +			<Bulk.Checkbox +				id="obfuscate" +				name="Obfuscate domains? " +				inputProps={disableInfoFields()} +			/> + +			<div className="hidden"> +				<Bulk.File +					id="json" +					fileType="application/json" +					withPreview={false} +				/> +			</div> + +			<div className="messagebutton"> +				<div> +					<button type="submit" onClick={importBlocks}>Import</button> +				</div> + +				<div> +					<button type="submit" onClick={exportBlocks}>Export</button> + +					<Bulk.Select id="exportType" name="Export type" options={ +						<> +							<option value="plain">One per line in text field</option> +							<option value="json">JSON in text field</option> +							<option value="json-download">JSON file download</option> +							<option disabled value="csv">CSV in text field (glitch-soc)</option> +							<option disabled value="csv-download">CSV file download (glitch-soc)</option> +						</> +					}/> +				</div> +				<br/> +				<div> +					{errorMsg.length > 0 &&  +						<div className="error accent">{errorMsg}</div> +					} +					{statusMsg.length > 0 && +						<div className="accent">{statusMsg}</div> +					} +				</div> +			</div> +		</div> +	); +} + +function BackButton() { +	return ( +		<Link to={base}> +			<a className="button">< back</a> +		</Link> +	); +} + +function InstancePageWrapped() { +	/* We wrap the component to generate formFields with a setter depending on the domain +		 if formFields() is used inside the same component that is re-rendered with their state, +		 inputs get re-created on every change, causing them to lose focus, and bad performance +	*/ +	let [_match, {domain}] = useRoute(`${base}/:domain`); + +	if (domain == "view") { // from form field submission +		let realDomain = (new URL(document.location)).searchParams.get("domain"); +		if (realDomain == undefined) { +			return <Redirect to={base}/>; +		} else { +			domain = realDomain; +		} +	} + +	function alterDomain([key, val]) { +		return adminActions.updateDomainBlockVal([domain, key, val]); +	} + +	const fields = formFields(alterDomain, (state) => state.admin.newInstanceBlocks[domain]); + +	return <InstancePage domain={domain} Form={fields} />; +} + +function InstancePage({domain, Form}) { +	const dispatch = Redux.useDispatch(); +	const entry = Redux.useSelector(state => state.admin.newInstanceBlocks[domain]); +	const [_location, setLocation] = useLocation(); + +	React.useEffect(() => { +		if (entry == undefined) { +			dispatch(api.admin.getEditableDomainBlock(domain)); +		} +	}, []); + +	const [errorMsg, setError] = React.useState(""); +	const [statusMsg, setStatus] = React.useState(""); + +	if (entry == undefined) { +		return "Loading..."; +	} + +	const updateBlock = submit( +		() => dispatch(api.admin.updateDomainBlock(domain)), +		{setStatus, setError} +	); + +	const removeBlock = submit( +		() => dispatch(api.admin.removeDomainBlock(domain)), +		{setStatus, setError, startStatus: "Removing", successStatus: "Removed!", onSuccess: () => { +			setLocation(base); +		}} +	); + +	return ( +		<div> +			<h1><BackButton/> Federation settings for: {domain}</h1> +			{entry.new && "No stored block yet, you can add one below:"} + +			<Form.TextArea +				id="public_comment" +				name="Public comment" +			/> + +			<Form.TextArea +				id="private_comment" +				name="Private comment" +			/> + +			<Form.Checkbox +				id="obfuscate" +				name="Obfuscate domain? " +			/> + +			<div className="messagebutton"> +				<button type="submit" onClick={updateBlock}>{entry.new ? "Add block" : "Save block"}</button> + +				{!entry.new && +					<button className="danger" onClick={removeBlock}>Remove block</button> +				} + +				{errorMsg.length > 0 &&  +					<div className="error accent">{errorMsg}</div> +				} +				{statusMsg.length > 0 && +					<div className="accent">{statusMsg}</div> +				} +			</div> +		</div> +	); +}
\ No newline at end of file diff --git a/web/source/settings-panel/admin/settings.js b/web/source/settings-panel/admin/settings.js new file mode 100644 index 000000000..845a1f924 --- /dev/null +++ b/web/source/settings-panel/admin/settings.js @@ -0,0 +1,110 @@ +/* +	GoToSocial +	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + +	This program is free software: you can redistribute it and/or modify +	it under the terms of the GNU Affero General Public License as published by +	the Free Software Foundation, either version 3 of the License, or +	(at your option) any later version. + +	This program is distributed in the hope that it will be useful, +	but WITHOUT ANY WARRANTY; without even the implied warranty of +	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +	GNU Affero General Public License for more details. + +	You should have received a copy of the GNU Affero General Public License +	along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +"use strict"; + +const Promise = require("bluebird"); +const React = require("react"); +const Redux = require("react-redux"); + +const Submit = require("../components/submit"); + +const api = require("../lib/api"); +const submit = require("../lib/submit"); + +const adminActions = require("../redux/reducers/instances").actions; + +const { +	TextInput, +	TextArea, +	File +} = require("../components/form-fields").formFields(adminActions.setAdminSettingsVal, (state) => state.instances.adminSettings); + +module.exports = function AdminSettings() { +	const dispatch = Redux.useDispatch(); + +	const [errorMsg, setError] = React.useState(""); +	const [statusMsg, setStatus] = React.useState(""); + +	const updateSettings = submit( +		() => dispatch(api.admin.updateInstance()), +		{setStatus, setError} +	); + +	return ( +		<div> +			<h1>Instance Settings</h1> +			<TextInput +				id="title" +				name="Title" +				placeHolder="My GoToSocial instance" +			/> + +			<TextArea +				id="short_description" +				name="Short description" +				placeHolder="A small testing instance for the GoToSocial alpha." +			/> +			<TextArea +				id="description" +				name="Full description" +				placeHolder="A small testing instance for the GoToSocial alpha." +			/> + +			<TextInput +				id="contact_account.username" +				name="Contact user (local account username)" +				placeHolder="admin" +			/> +			<TextInput +				id="email" +				name="Contact email" +				placeHolder="admin@example.com" +			/> + +			<TextArea +				id="terms" +				name="Terms & Conditions" +				placeHolder="" +			/> + +			{/* <div className="file-upload"> +				<h3>Instance avatar</h3> +				<div> +					<img className="preview avatar" src={instance.avatar} alt={instance.avatar ? `Avatar image for the instance` : "No instance avatar image set"} /> +					<File  +						id="avatar" +						fileType="image/*" +					/> +				</div> +			</div> + +			<div className="file-upload"> +				<h3>Instance header</h3> +				<div> +					<img className="preview header" src={instance.header} alt={instance.header ? `Header image for the instance` : "No instance header image set"} /> +					<File  +						id="header" +						fileType="image/*" +					/> +				</div> +			</div> */} +			<Submit onClick={updateSettings} label="Save" errorMsg={errorMsg} statusMsg={statusMsg} /> +		</div> +	); +};
\ No newline at end of file  | 
