Compare commits

...

26 Commits

Author SHA1 Message Date
Thomas Camlong
aab1492934 🔖 v0.7.2 2022-06-25 17:53:20 +02:00
Thomas Camlong
1ae074db8f 🔥 Remove .docusaurus/ 2022-06-25 17:51:44 +02:00
Thomas Camlong
f21004e944 🔖 v0.7.2
Tag version v0.7.2
2022-06-25 17:47:39 +02:00
Thomas Camlong
7c421cc52f 🔀 Merge pull request #260 from walkxcode/dev
💄 Changes AppShelf category styling
2022-06-25 16:11:38 +02:00
Thomas Camlong
d8e407ab22 🔀 Merge pull request #257 from ajnart/fix-multiple-torrent-client
🐛 Fix itteration on the different types of services
2022-06-25 15:36:59 +02:00
Thomas Camlong
37565284e6 🔀 Merge pull request #271 from ajnart/searchBar
 Adds query placeholder and autoFocus (#267 #268)
2022-06-25 15:36:06 +02:00
Bjorn Lammers
b758df9f44 🔥 Fix indentation because I'm a perfectionist 2022-06-25 15:14:39 +02:00
Bjorn Lammers
a735ae47c5 🙈 Updates .dockerignore 2022-06-25 15:13:00 +02:00
WalkxCode
97d585dc17 Adds query placeholder and autoFocus (#267 #268) 2022-06-25 14:02:53 +02:00
Thomas Camlong
7f3db9add1 🐛 Fix adding a service doesn't fetch 2022-06-24 13:44:43 +02:00
Thomas Camlong
6d6964f086 🔀 Merge pull request #258 from ajnart/ajnart/issue256
🐛 Allow anything in the input for the form.
2022-06-24 13:39:18 +02:00
Thomas Camlong
2a4012f73a 🔀 Merge pull request #263 from ajnart/#261-discord
💬 Adds Discord Button (#261)
2022-06-24 12:44:23 +02:00
Bjorn Lammers
9385315f03 🔀 Merge pull request #265 from jelliuk/patch-1 2022-06-24 12:06:27 +02:00
James
ee824f0b27 Update README.md
Correct the link to Wiki/Integrations
2022-06-24 10:36:43 +01:00
Bjorn Lammers
792af504c7 💬 Adds Discord Button
#261
2022-06-22 13:19:44 +02:00
Bjorn Lammers
cd3c062a24 💄 Changes AppShelf category styling 2022-06-21 19:14:18 +00:00
ajnart
a5f477c19b 🚑 Hotfix to spread torrent pushing 2022-06-21 21:04:21 +02:00
ajnart
85164d79fc 🚑 Hotfix password and usernames 2022-06-21 20:35:40 +02:00
ajnart
7aedc4111f 🚑 Hotfix how the result from the services are awaited 2022-06-21 19:59:25 +02:00
ajnart
d1f89847f5 💄 Small UI fix for mobile 2022-06-21 19:38:32 +02:00
ajnart
57170847a1 🐛 Allow anything in the input for the form.
If it works, it works.
Fixes #256
2022-06-21 19:22:14 +02:00
ajnart
45de715390 🐛 Fix itteration on the different types of services 2022-06-21 19:16:29 +02:00
Thomas Camlong
c29d6f58dd 🔀 Merge pull request #252 from LarveyOfficial/fix-multiple-download-clients
🐛Allow multiple of the same torrent client +1
2022-06-21 16:22:18 +02:00
ajnart
f0bae49830 🚨 Lint and prettier fix 2022-06-21 16:21:40 +02:00
Larvey
c3ceae4dc6 Also fixed Torrent form fields 2022-06-20 17:26:13 -04:00
Larvey
d654fb39e5 🐛Allow multiple of the same torrent client
Allows multiple of the same type of torrent client
2022-06-20 17:10:54 -04:00
17 changed files with 191 additions and 141 deletions

View File

@@ -2,5 +2,8 @@ Dockerfile
.dockerignore .dockerignore
node_modules node_modules
npm-debug.log npm-debug.log
README.md *.md
.git .git
.github
LICENSE
docs/

View File

@@ -2,7 +2,7 @@ FROM node:16-alpine
WORKDIR /app WORKDIR /app
ENV NODE_ENV production ENV NODE_ENV production
COPY /next.config.js ./ COPY /next.config.js ./
COPY /public ./public COPY /public ./public
COPY /package.json ./package.json COPY /package.json ./package.json
# Automatically leverage output traces to reduce image size. https://nextjs.org/docs/advanced-features/output-file-tracing # Automatically leverage output traces to reduce image size. https://nextjs.org/docs/advanced-features/output-file-tracing
COPY /.next/standalone ./ COPY /.next/standalone ./

View File

@@ -33,7 +33,7 @@ Homarr is a simple and lightweight homepage for your server, that helps you easi
It integrates with the services you use to display information on the homepage (E.g. Show upcoming Sonarr/Radarr releases). It integrates with the services you use to display information on the homepage (E.g. Show upcoming Sonarr/Radarr releases).
For a full list of integrations look at: [wiki/integrations](#). For a full list of integrations look at: [wiki/integrations](https://github.com/ajnart/homarr/wiki/Integrations)
If you have any questions about Homarr or want to share information with us, please go to one of the following places: If you have any questions about Homarr or want to share information with us, please go to one of the following places:

View File

@@ -1,2 +1,2 @@
export const REPO_URL = 'ajnart/homarr'; export const REPO_URL = 'ajnart/homarr';
export const CURRENT_VERSION = 'v0.7.1'; export const CURRENT_VERSION = 'v0.7.2';

View File

@@ -1,6 +1,6 @@
{ {
"name": "homarr", "name": "homarr",
"version": "0.7.1", "version": "0.7.2",
"description": "Homarr - A homepage for your server.", "description": "Homarr - A homepage for your server.",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -123,13 +123,9 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
validate: { validate: {
apiKey: () => null, apiKey: () => null,
// Validate icon with a regex // Validate icon with a regex
icon: (value: string) => { icon: (value: string) =>
// Regex to match everything that ends with and icon extension // Disable matching to allow any values
if (!value.match(/\.(png|jpg|jpeg|gif|svg)$/)) { null,
return 'Please enter a valid icon URL';
}
return null;
},
// Validate url with a regex http/https // Validate url with a regex http/https
url: (value: string) => { url: (value: string) => {
try { try {
@@ -321,9 +317,20 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
/> />
</> </>
)} )}
{(form.values.type === 'Deluge' || {form.values.type === 'Deluge' && (
form.values.type === 'Transmission' || <>
form.values.type === 'qBittorrent') && ( <TextInput
label="Password"
placeholder="password"
value={form.values.password}
onChange={(event) => {
form.setFieldValue('password', event.currentTarget.value);
}}
error={form.errors.password && 'Invalid password'}
/>
</>
)}
{form.values.type === 'Transmission' && (
<> <>
<TextInput <TextInput
label="Username" label="Username"
@@ -336,7 +343,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
/> />
<TextInput <TextInput
label="Password" label="Password"
placeholder="password" placeholder="adminadmin"
value={form.values.password} value={form.values.password}
onChange={(event) => { onChange={(event) => {
form.setFieldValue('password', event.currentTarget.value); form.setFieldValue('password', event.currentTarget.value);

View File

@@ -20,15 +20,30 @@ import DownloadComponent from '../modules/downloads/DownloadsModule';
const useStyles = createStyles((theme, _params) => ({ const useStyles = createStyles((theme, _params) => ({
item: { item: {
borderBottom: 0,
overflow: 'hidden', overflow: 'hidden',
border: '1px solid transparent', borderLeft: '3px solid transparent',
borderRadius: theme.radius.lg, borderRight: '3px solid transparent',
borderBottom: '3px solid transparent',
borderRadius: '20px',
borderColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
marginTop: theme.spacing.md, marginTop: theme.spacing.md,
}, },
itemOpened: { control: {
borderColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[3], backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
borderRadius: theme.spacing.md,
'&:hover': {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
},
},
content: {
margin: theme.spacing.md,
},
label: {
overflow: 'visible',
}, },
})); }));
@@ -147,11 +162,6 @@ const AppShelf = (props: any) => {
order={2} order={2}
iconPosition="right" iconPosition="right"
multiple multiple
styles={{
item: {
borderRadius: '20px',
},
}}
initialState={toggledCategories} initialState={toggledCategories}
onChange={(idx) => settoggledCategories(idx)} onChange={(idx) => settoggledCategories(idx)}
> >

View File

@@ -20,11 +20,7 @@ export default function AppShelfMenu(props: any) {
onClose={() => setOpened(false)} onClose={() => setOpened(false)}
title="Modify a service" title="Modify a service"
> >
<AddAppShelfItemForm <AddAppShelfItemForm setOpened={setOpened} {...service} message="Save service" />
setOpened={setOpened}
{...service}
message="Save service"
/>
</Modal> </Modal>
<Menu <Menu
position="right" position="right"

View File

@@ -1,6 +1,6 @@
import { ActionIcon, Group, Text, SegmentedControl, TextInput, Anchor } from '@mantine/core'; import { ActionIcon, Group, Text, SegmentedControl, TextInput, Anchor } from '@mantine/core';
import { useState } from 'react'; import { useState } from 'react';
import { IconBrandGithub as BrandGithub } from '@tabler/icons'; import { IconBrandGithub as BrandGithub, IconBrandDiscord as BrandDiscord } from '@tabler/icons';
import { CURRENT_VERSION } from '../../../data/constants'; import { CURRENT_VERSION } from '../../../data/constants';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch'; import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch';
@@ -28,6 +28,15 @@ export default function CommonSettings(args: any) {
<Group direction="column" grow> <Group direction="column" grow>
<Group grow direction="column" spacing={0}> <Group grow direction="column" spacing={0}>
<Text>Search engine</Text> <Text>Search engine</Text>
<Text
style={{
fontSize: '0.75rem',
color: 'gray',
marginBottom: '0.5rem',
}}
>
Tip: %s can be used as a placeholder for the query.
</Text>
<SegmentedControl <SegmentedControl
fullWidth fullWidth
title="Search engine" title="Search engine"
@@ -53,7 +62,7 @@ export default function CommonSettings(args: any) {
{searchUrl === 'Custom' && ( {searchUrl === 'Custom' && (
<TextInput <TextInput
label="Query URL" label="Query URL"
placeholder="Custom query url" placeholder="Custom query URL"
value={customSearchUrl} value={customSearchUrl}
onChange={(event) => { onChange={(event) => {
setCustomSearchUrl(event.currentTarget.value); setCustomSearchUrl(event.currentTarget.value);
@@ -98,21 +107,26 @@ export default function CommonSettings(args: any) {
{CURRENT_VERSION} {CURRENT_VERSION}
</Text> </Text>
</Group> </Group>
<Text <Group spacing={1}>
style={{ <Text
fontSize: '0.90rem', style={{
textAlign: 'center', fontSize: '0.90rem',
color: 'gray', textAlign: 'center',
}} color: 'gray',
> }}
Made with by @
<Anchor
href="https://github.com/ajnart"
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
> >
ajnart Made with by @
</Anchor> <Anchor
</Text> href="https://github.com/ajnart"
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
>
ajnart
</Anchor>
</Text>
<ActionIcon<'a'> component="a" href="https://discord.gg/aCsmEV5RgA" size="lg">
<BrandDiscord size={18} />
</ActionIcon>
</Group>
</Group> </Group>
</Group> </Group>
); );

View File

@@ -78,7 +78,7 @@ export function Header(props: any) {
> >
{(styles) => ( {(styles) => (
<div style={styles}> <div style={styles}>
<ScrollArea style={{ height: '90vh' }}> <ScrollArea offsetScrollbars style={{ height: '90vh' }}>
<Group my="sm" grow direction="column" style={{ width: 300 }}> <Group my="sm" grow direction="column" style={{ width: 300 }}>
<ModuleWrapper module={CalendarModule} /> <ModuleWrapper module={CalendarModule} />
<ModuleWrapper module={TotalDownloadsModule} /> <ModuleWrapper module={TotalDownloadsModule} />

View File

@@ -71,11 +71,13 @@ export default function CalendarComponent(props: any) {
const currentSonarrMedias: any[] = []; const currentSonarrMedias: any[] = [];
Promise.all( Promise.all(
sonarrServices.map((service) => sonarrServices.map((service) =>
getMedias(service, 'sonarr').then((res) => { getMedias(service, 'sonarr')
currentSonarrMedias.push(...res.data); .then((res) => {
}).catch(() => { currentSonarrMedias.push(...res.data);
currentSonarrMedias.push([]); })
}) .catch(() => {
currentSonarrMedias.push([]);
})
) )
).then(() => { ).then(() => {
setSonarrMedias(currentSonarrMedias); setSonarrMedias(currentSonarrMedias);
@@ -83,11 +85,13 @@ export default function CalendarComponent(props: any) {
const currentRadarrMedias: any[] = []; const currentRadarrMedias: any[] = [];
Promise.all( Promise.all(
radarrServices.map((service) => radarrServices.map((service) =>
getMedias(service, 'radarr').then((res) => { getMedias(service, 'radarr')
currentRadarrMedias.push(...res.data); .then((res) => {
}).catch(() => { currentRadarrMedias.push(...res.data);
currentRadarrMedias.push([]); })
}) .catch(() => {
currentRadarrMedias.push([]);
})
) )
).then(() => { ).then(() => {
setRadarrMedias(currentRadarrMedias); setRadarrMedias(currentRadarrMedias);
@@ -95,11 +99,13 @@ export default function CalendarComponent(props: any) {
const currentLidarrMedias: any[] = []; const currentLidarrMedias: any[] = [];
Promise.all( Promise.all(
lidarrServices.map((service) => lidarrServices.map((service) =>
getMedias(service, 'lidarr').then((res) => { getMedias(service, 'lidarr')
currentLidarrMedias.push(...res.data); .then((res) => {
}).catch(() => { currentLidarrMedias.push(...res.data);
currentLidarrMedias.push([]); })
}) .catch(() => {
currentLidarrMedias.push([]);
})
) )
).then(() => { ).then(() => {
setLidarrMedias(currentLidarrMedias); setLidarrMedias(currentLidarrMedias);
@@ -107,11 +113,13 @@ export default function CalendarComponent(props: any) {
const currentReadarrMedias: any[] = []; const currentReadarrMedias: any[] = [];
Promise.all( Promise.all(
readarrServices.map((service) => readarrServices.map((service) =>
getMedias(service, 'readarr').then((res) => { getMedias(service, 'readarr')
currentReadarrMedias.push(...res.data); .then((res) => {
}).catch(() => { currentReadarrMedias.push(...res.data);
currentReadarrMedias.push([]); })
}) .catch(() => {
currentReadarrMedias.push([]);
})
) )
).then(() => { ).then(() => {
setReadarrMedias(currentReadarrMedias); setReadarrMedias(currentReadarrMedias);

View File

@@ -59,7 +59,7 @@ export default function DownloadComponent() {
setIsLoading(false); setIsLoading(false);
}); });
}, 5000); }, 5000);
}, [config.services]); }, []);
if (downloadServices.length === 0) { if (downloadServices.length === 0) {
return ( return (

View File

@@ -23,19 +23,19 @@ export default function PingComponent(props: any) {
const exists = config.modules?.[PingModule.title]?.enabled ?? false; const exists = config.modules?.[PingModule.title]?.enabled ?? false;
function statusCheck(response: AxiosResponse) { function statusCheck(response: AxiosResponse) {
const { status }: {status: string[]} = props; const { status }: { status: string[] } = props;
//Default Status //Default Status
let acceptableStatus = ['200']; let acceptableStatus = ['200'];
if (status !== undefined && status.length) { if (status !== undefined && status.length) {
acceptableStatus = status; acceptableStatus = status;
} }
// Checks if reported status is in acceptable status array // Checks if reported status is in acceptable status array
if (acceptableStatus.indexOf((response.status).toString()) >= 0) { if (acceptableStatus.indexOf(response.status.toString()) >= 0) {
setOnline('online'); setOnline('online');
setResponse(response.status); setResponse(response.status);
} else { } else {
setOnline('down'); setOnline('down');
setResponse(response.status) setResponse(response.status);
} }
} }
@@ -59,7 +59,13 @@ export default function PingComponent(props: any) {
<Tooltip <Tooltip
radius="lg" radius="lg"
style={{ position: 'absolute', bottom: 20, right: 20 }} style={{ position: 'absolute', bottom: 20, right: 20 }}
label={isOnline === 'loading' ? 'Loading...' : isOnline === 'online' ? `Online - ${response}` : `Offline - ${response}`} label={
isOnline === 'loading'
? 'Loading...'
: isOnline === 'online'
? `Online - ${response}`
: `Offline - ${response}`
}
> >
<motion.div <motion.div
animate={{ animate={{

View File

@@ -96,7 +96,13 @@ export default function SearchBar(props: any) {
} else if (isTorrent) { } else if (isTorrent) {
window.open(`https://bitsearch.to/search?q=${query.substring(3)}`); window.open(`https://bitsearch.to/search?q=${query.substring(3)}`);
} else { } else {
window.open(`${queryUrl}${values.query}`); window.open(
`${
queryUrl.includes('%s')
? queryUrl.replace('%s', values.query)
: queryUrl + values.query
}`
);
} }
}, 20); }, 20);
})} })}
@@ -114,6 +120,7 @@ export default function SearchBar(props: any) {
onBlurCapture={() => setOpened(false)} onBlurCapture={() => setOpened(false)}
target={ target={
<Autocomplete <Autocomplete
autoFocus
variant="filled" variant="filled"
data={autocompleteData} data={autocompleteData}
icon={icon} icon={icon}

View File

@@ -44,9 +44,9 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
}); });
} }
// Get the origin URL // Get the origin URL
var { href: origin } = new URL(service.url); let { href: origin } = new URL(service.url);
if (origin.endsWith("/")) { if (origin.endsWith('/')) {
origin = origin.slice(0, -1) origin = origin.slice(0, -1);
} }
const pined = `${origin}${url?.url}?apiKey=${service.apiKey}&end=${nextMonth}&start=${lastMonth}`; const pined = `${origin}${url?.url}?apiKey=${service.apiKey}&end=${nextMonth}&start=${lastMonth}`;
const data = await axios.get( const data = await axios.get(

View File

@@ -7,53 +7,52 @@ import { Config } from '../../../tools/types';
async function Post(req: NextApiRequest, res: NextApiResponse) { async function Post(req: NextApiRequest, res: NextApiResponse) {
// Get the type of service from the request url // Get the type of service from the request url
const torrents: NormalizedTorrent[] = [];
const { config }: { config: Config } = req.body; const { config }: { config: Config } = req.body;
const qBittorrentService = config.services const qBittorrentServices = config.services.filter((service) => service.type === 'qBittorrent');
.filter((service) => service.type === 'qBittorrent') const delugeServices = config.services.filter((service) => service.type === 'Deluge');
.at(0); const transmissionServices = config.services.filter((service) => service.type === 'Transmission');
const delugeService = config.services.filter((service) => service.type === 'Deluge').at(0);
const transmissionService = config.services const torrents: NormalizedTorrent[] = [];
.filter((service) => service.type === 'Transmission')
.at(0); if (!qBittorrentServices && !delugeServices && !transmissionServices) {
if (!qBittorrentService && !delugeService && !transmissionService) {
return res.status(500).json({ return res.status(500).json({
statusCode: 500, statusCode: 500,
message: 'Missing service', message: 'Missing services',
}); });
} }
if (qBittorrentService) { await Promise.all(
torrents.push( qBittorrentServices.map((service) =>
...( new QBittorrent({
await new QBittorrent({ baseUrl: service.url,
baseUrl: qBittorrentService.url, username: service.username,
username: qBittorrentService.username, password: service.password,
password: qBittorrentService.password, })
}).getAllData() .getAllData()
).torrents .then((e) => torrents.push(...e.torrents))
); )
} );
if (delugeService) { await Promise.all(
torrents.push( delugeServices.map((service) =>
...( new Deluge({
await new Deluge({ baseUrl: service.url,
baseUrl: delugeService.url, password: 'password' in service ? service.password : '',
password: 'password' in delugeService ? delugeService.password : '', })
}).getAllData() .getAllData()
).torrents .then((e) => torrents.push(...e.torrents))
); )
} );
if (transmissionService) { // Map transmissionServices
torrents.push( await Promise.all(
...( transmissionServices.map((service) =>
await new Transmission({ new Transmission({
baseUrl: transmissionService.url, baseUrl: service.url,
username: transmissionService.username, username: 'username' in service ? service.username : '',
password: 'password' in transmissionService ? transmissionService.password : '', password: 'password' in service ? service.password : '',
}).getAllData() })
).torrents .getAllData()
); .then((e) => torrents.push(...e.torrents))
} )
);
res.status(200).json(torrents); res.status(200).json(torrents);
} }

View File

@@ -33,30 +33,30 @@ interface ConfigModule {
} }
export const StatusCodes = [ export const StatusCodes = [
{value: '200', label: '200 - OK', group:'Sucessful responses'}, { value: '200', label: '200 - OK', group: 'Sucessful responses' },
{value: '204', label: '204 - No Content', group:'Sucessful responses'}, { value: '204', label: '204 - No Content', group: 'Sucessful responses' },
{value: '301', label: '301 - Moved Permanently', group:'Redirection responses'}, { value: '301', label: '301 - Moved Permanently', group: 'Redirection responses' },
{value: '302', label: '302 - Found / Moved Temporarily', group:'Redirection responses'}, { value: '302', label: '302 - Found / Moved Temporarily', group: 'Redirection responses' },
{value: '304', label: '304 - Not Modified', group:'Redirection responses'}, { value: '304', label: '304 - Not Modified', group: 'Redirection responses' },
{value: '307', label: '307 - Temporary Redirect', group:'Redirection responses'}, { value: '307', label: '307 - Temporary Redirect', group: 'Redirection responses' },
{value: '308', label: '308 - Permanent Redirect', group:'Redirection responses'}, { value: '308', label: '308 - Permanent Redirect', group: 'Redirection responses' },
{value: '400', label: '400 - Bad Request', group:'Client error responses'}, { value: '400', label: '400 - Bad Request', group: 'Client error responses' },
{value: '401', label: '401 - Unauthorized', group:'Client error responses'}, { value: '401', label: '401 - Unauthorized', group: 'Client error responses' },
{value: '403', label: '403 - Forbidden', group:'Client error responses'}, { value: '403', label: '403 - Forbidden', group: 'Client error responses' },
{value: '404', label: '404 - Not Found', group:'Client error responses'}, { value: '404', label: '404 - Not Found', group: 'Client error responses' },
{value: '408', label: '408 - Request Timeout', group:'Client error responses'}, { value: '408', label: '408 - Request Timeout', group: 'Client error responses' },
{value: '410', label: '410 - Gone', group:'Client error responses'}, { value: '410', label: '410 - Gone', group: 'Client error responses' },
{value: '429', label: '429 - Too Many Requests', group:'Client error responses'}, { value: '429', label: '429 - Too Many Requests', group: 'Client error responses' },
{value: '500', label: '500 - Internal Server Error', group:'Server error responses'}, { value: '500', label: '500 - Internal Server Error', group: 'Server error responses' },
{value: '502', label: '502 - Bad Gateway', group:'Server error responses'}, { value: '502', label: '502 - Bad Gateway', group: 'Server error responses' },
{value: '503', label: '503 - Service Unavailable', group:'Server error responses'}, { value: '503', label: '503 - Service Unavailable', group: 'Server error responses' },
{value: '054', label: '504 - Gateway Timeout Error', group:'Server error responses'}, { value: '054', label: '504 - Gateway Timeout Error', group: 'Server error responses' },
]; ];
export const Targets = [ export const Targets = [
{value: '_blank', label: 'New Tab'}, { value: '_blank', label: 'New Tab' },
{value: '_top', label: 'Same Window'} { value: '_top', label: 'Same Window' },
] ];
export const ServiceTypeList = [ export const ServiceTypeList = [
'Other', 'Other',