2012-10-09 18:43:53 +00:00
|
|
|
{- git-annex assistant webapp configurator for editing existing repos
|
|
|
|
-
|
2015-01-21 16:50:09 +00:00
|
|
|
- Copyright 2012 Joey Hess <id@joeyh.name>
|
2012-10-09 18:43:53 +00:00
|
|
|
-
|
|
|
|
- Licensed under the GNU AGPL version 3 or higher.
|
|
|
|
-}
|
|
|
|
|
2013-06-05 01:02:09 +00:00
|
|
|
{-# LANGUAGE CPP, QuasiQuotes, TemplateHaskell, OverloadedStrings #-}
|
2015-05-10 19:54:58 +00:00
|
|
|
{-# LANGUAGE FlexibleContexts #-}
|
2012-10-09 18:43:53 +00:00
|
|
|
|
|
|
|
module Assistant.WebApp.Configurators.Edit where
|
|
|
|
|
2012-11-25 04:26:46 +00:00
|
|
|
import Assistant.WebApp.Common
|
2013-09-18 00:02:42 +00:00
|
|
|
import Assistant.WebApp.Gpg
|
2014-04-09 20:27:24 +00:00
|
|
|
import Assistant.WebApp.Configurators
|
2012-10-14 21:18:01 +00:00
|
|
|
import Assistant.DaemonStatus
|
2013-10-28 15:33:14 +00:00
|
|
|
import Assistant.WebApp.MakeRemote (uniqueRemoteName)
|
2013-04-08 20:45:12 +00:00
|
|
|
import Assistant.ScanRemotes
|
2013-10-26 20:54:49 +00:00
|
|
|
import Assistant.Sync
|
2014-04-09 20:27:24 +00:00
|
|
|
import Assistant.Alert
|
2013-04-25 20:42:17 +00:00
|
|
|
import qualified Assistant.WebApp.Configurators.AWS as AWS
|
|
|
|
#ifdef WITH_S3
|
2015-10-13 17:24:44 +00:00
|
|
|
import qualified Assistant.WebApp.Configurators.IA as IA
|
2013-04-25 20:42:17 +00:00
|
|
|
import qualified Remote.S3 as S3
|
|
|
|
#endif
|
2012-10-09 19:11:48 +00:00
|
|
|
import qualified Remote
|
2013-01-01 17:52:47 +00:00
|
|
|
import qualified Types.Remote as Remote
|
2012-10-14 21:18:01 +00:00
|
|
|
import qualified Remote.List as Remote
|
2012-10-09 19:11:48 +00:00
|
|
|
import Logs.UUID
|
2012-10-10 20:04:28 +00:00
|
|
|
import Logs.Group
|
2012-10-10 23:13:49 +00:00
|
|
|
import Logs.PreferredContent
|
2013-04-25 20:42:17 +00:00
|
|
|
import Logs.Remote
|
2012-10-10 20:04:28 +00:00
|
|
|
import Types.StandardGroups
|
2012-10-11 23:36:28 +00:00
|
|
|
import qualified Git
|
2014-03-01 00:37:03 +00:00
|
|
|
import qualified Git.Types as Git
|
2012-10-14 21:18:01 +00:00
|
|
|
import qualified Git.Command
|
2012-10-31 18:39:02 +00:00
|
|
|
import qualified Git.Config
|
2013-03-03 18:55:25 +00:00
|
|
|
import qualified Annex
|
2013-01-14 22:42:15 +00:00
|
|
|
import Git.Remote
|
2013-09-18 00:02:42 +00:00
|
|
|
import Remote.Helper.Encryptable (extractCipher)
|
|
|
|
import Types.Crypto
|
|
|
|
import Utility.Gpg
|
2013-11-05 16:38:54 +00:00
|
|
|
import Annex.UUID
|
2013-11-07 22:02:00 +00:00
|
|
|
import Assistant.Ssh
|
|
|
|
import Config
|
2017-02-03 17:40:14 +00:00
|
|
|
import Config.GitConfig
|
2017-08-17 16:26:14 +00:00
|
|
|
import Config.DynamicConfig
|
2012-10-09 18:43:53 +00:00
|
|
|
|
|
|
|
import qualified Data.Text as T
|
2012-10-09 19:11:48 +00:00
|
|
|
import qualified Data.Map as M
|
2012-10-10 20:23:41 +00:00
|
|
|
import qualified Data.Set as S
|
|
|
|
|
|
|
|
data RepoGroup = RepoGroupCustom String | RepoGroupStandard StandardGroup
|
|
|
|
deriving (Show, Eq)
|
2012-10-09 19:11:48 +00:00
|
|
|
|
|
|
|
data RepoConfig = RepoConfig
|
2012-10-14 21:18:01 +00:00
|
|
|
{ repoName :: Text
|
|
|
|
, repoDescription :: Maybe Text
|
2012-10-10 20:23:41 +00:00
|
|
|
, repoGroup :: RepoGroup
|
2013-04-26 17:00:14 +00:00
|
|
|
, repoAssociatedDirectory :: Maybe Text
|
2012-10-11 23:22:29 +00:00
|
|
|
, repoSyncable :: Bool
|
2012-10-09 19:11:48 +00:00
|
|
|
}
|
|
|
|
deriving (Show)
|
|
|
|
|
2013-01-01 17:52:47 +00:00
|
|
|
getRepoConfig :: UUID -> Maybe Remote -> Annex RepoConfig
|
2013-04-26 17:00:14 +00:00
|
|
|
getRepoConfig uuid mremote = do
|
2014-05-30 00:12:17 +00:00
|
|
|
-- Ensure we're editing current data by discarding caches.
|
|
|
|
void groupMapLoad
|
|
|
|
void uuidMapLoad
|
|
|
|
|
2013-04-26 17:00:14 +00:00
|
|
|
groups <- lookupGroups uuid
|
|
|
|
remoteconfig <- M.lookup uuid <$> readRemoteLog
|
|
|
|
let (repogroup, associateddirectory) = case getStandardGroup groups of
|
|
|
|
Nothing -> (RepoGroupCustom $ unwords $ S.toList groups, Nothing)
|
|
|
|
Just g -> (RepoGroupStandard g, associatedDirectory remoteconfig g)
|
|
|
|
|
2013-10-02 05:06:59 +00:00
|
|
|
description <- fmap T.pack . M.lookup uuid <$> uuidMap
|
2013-04-26 17:00:14 +00:00
|
|
|
|
|
|
|
syncable <- case mremote of
|
2017-08-17 16:26:14 +00:00
|
|
|
Just r -> liftIO $ getDynamicConfig $ remoteAnnexSync $ Remote.gitconfig r
|
2017-02-03 17:40:14 +00:00
|
|
|
Nothing -> getGitConfigVal annexAutoCommit
|
2012-10-10 23:13:49 +00:00
|
|
|
|
2013-04-26 17:00:14 +00:00
|
|
|
return $ RepoConfig
|
|
|
|
(T.pack $ maybe "here" Remote.name mremote)
|
|
|
|
description
|
|
|
|
repogroup
|
|
|
|
(T.pack <$> associateddirectory)
|
|
|
|
syncable
|
|
|
|
|
2012-10-14 21:18:01 +00:00
|
|
|
setRepoConfig :: UUID -> Maybe Remote -> RepoConfig -> RepoConfig -> Handler ()
|
|
|
|
setRepoConfig uuid mremote oldc newc = do
|
2013-04-08 20:45:12 +00:00
|
|
|
when descriptionChanged $ liftAnnex $ do
|
2012-10-14 21:18:01 +00:00
|
|
|
maybe noop (describeUUID uuid . T.unpack) (repoDescription newc)
|
2012-10-30 21:22:21 +00:00
|
|
|
void uuidMapLoad
|
2013-04-08 20:45:12 +00:00
|
|
|
when nameChanged $ do
|
2013-03-04 20:36:38 +00:00
|
|
|
liftAnnex $ do
|
2013-04-08 20:45:12 +00:00
|
|
|
name <- fromRepo $ uniqueRemoteName (legalName newc) 0
|
2012-10-31 18:39:02 +00:00
|
|
|
{- git remote rename expects there to be a
|
|
|
|
- remote.<name>.fetch, and exits nonzero if
|
|
|
|
- there's not. Special remotes don't normally
|
|
|
|
- have that, and don't use it. Temporarily add
|
|
|
|
- it if it's missing. -}
|
|
|
|
let remotefetch = "remote." ++ T.unpack (repoName oldc) ++ ".fetch"
|
|
|
|
needfetch <- isNothing <$> fromRepo (Git.Config.getMaybe remotefetch)
|
|
|
|
when needfetch $
|
2013-03-03 17:39:07 +00:00
|
|
|
inRepo $ Git.Command.run
|
|
|
|
[Param "config", Param remotefetch, Param ""]
|
|
|
|
inRepo $ Git.Command.run
|
|
|
|
[ Param "remote"
|
|
|
|
, Param "rename"
|
2012-10-14 21:18:01 +00:00
|
|
|
, Param $ T.unpack $ repoName oldc
|
|
|
|
, Param name
|
|
|
|
]
|
2013-10-02 05:06:59 +00:00
|
|
|
void Remote.remoteListRefresh
|
2012-10-30 21:14:26 +00:00
|
|
|
liftAssistant updateSyncRemotes
|
2013-04-26 17:00:14 +00:00
|
|
|
when associatedDirectoryChanged $ case repoAssociatedDirectory newc of
|
|
|
|
Nothing -> noop
|
|
|
|
Just t
|
|
|
|
| T.null t -> noop
|
|
|
|
| otherwise -> liftAnnex $ do
|
|
|
|
let dir = takeBaseName $ T.unpack t
|
|
|
|
m <- readRemoteLog
|
|
|
|
case M.lookup uuid m of
|
|
|
|
Nothing -> noop
|
|
|
|
Just remoteconfig -> configSet uuid $
|
|
|
|
M.insert "preferreddir" dir remoteconfig
|
2013-04-08 20:45:12 +00:00
|
|
|
when groupChanged $ do
|
|
|
|
liftAnnex $ case repoGroup newc of
|
|
|
|
RepoGroupStandard g -> setStandardGroup uuid g
|
|
|
|
RepoGroupCustom s -> groupSet uuid $ S.fromList $ words s
|
|
|
|
{- Enabling syncing will cause a scan,
|
|
|
|
- so avoid queueing a duplicate scan. -}
|
|
|
|
when (repoSyncable newc && not syncableChanged) $ liftAssistant $
|
|
|
|
case mremote of
|
2013-10-02 05:06:59 +00:00
|
|
|
Just remote -> addScanRemotes True [remote]
|
|
|
|
Nothing -> addScanRemotes True
|
|
|
|
=<< syncDataRemotes <$> getDaemonStatus
|
2013-04-08 20:45:12 +00:00
|
|
|
when syncableChanged $
|
2013-10-26 20:54:49 +00:00
|
|
|
liftAssistant $ changeSyncable mremote (repoSyncable newc)
|
2013-04-08 20:45:12 +00:00
|
|
|
where
|
2014-10-09 18:53:13 +00:00
|
|
|
syncableChanged = repoSyncable oldc /= repoSyncable newc
|
2013-04-26 17:00:14 +00:00
|
|
|
associatedDirectoryChanged = repoAssociatedDirectory oldc /= repoAssociatedDirectory newc
|
2013-04-08 20:45:12 +00:00
|
|
|
groupChanged = repoGroup oldc /= repoGroup newc
|
|
|
|
nameChanged = isJust mremote && legalName oldc /= legalName newc
|
|
|
|
descriptionChanged = repoDescription oldc /= repoDescription newc
|
|
|
|
|
|
|
|
legalName = makeLegalName . T.unpack . repoName
|
2012-10-11 21:14:42 +00:00
|
|
|
|
2014-03-01 00:37:03 +00:00
|
|
|
editRepositoryAForm :: Maybe Remote -> RepoConfig -> MkAForm RepoConfig
|
2015-01-28 20:11:28 +00:00
|
|
|
editRepositoryAForm mremote d = RepoConfig
|
2013-06-18 21:08:37 +00:00
|
|
|
<$> areq (if ishere then readonlyTextField else textField)
|
2015-01-28 20:11:28 +00:00
|
|
|
(bfs "Name") (Just $ repoName d)
|
|
|
|
<*> aopt textField (bfs "Description") (Just $ repoDescription d)
|
|
|
|
<*> areq (selectFieldList groups `withNote` help) (bfs "Repository group") (Just $ repoGroup d)
|
2013-04-26 17:00:14 +00:00
|
|
|
<*> associateddirectory
|
2015-01-28 20:11:28 +00:00
|
|
|
<*> areq checkBoxField "Syncing enabled" (Just $ repoSyncable d)
|
2012-10-31 06:34:03 +00:00
|
|
|
where
|
2014-03-01 00:37:03 +00:00
|
|
|
ishere = isNothing mremote
|
|
|
|
isspecial = fromMaybe False $
|
|
|
|
(== Git.Unknown) . Git.location . Remote.repo <$> mremote
|
2012-12-03 02:33:30 +00:00
|
|
|
groups = customgroups ++ standardgroups
|
2012-10-31 06:34:03 +00:00
|
|
|
standardgroups :: [(Text, RepoGroup)]
|
2014-03-01 00:37:03 +00:00
|
|
|
standardgroups = map (\g -> (T.pack $ descStandardGroup g , RepoGroupStandard g)) $
|
|
|
|
filter sanegroup [minBound..maxBound]
|
|
|
|
sanegroup
|
|
|
|
| isspecial = const True
|
|
|
|
| otherwise = not . specialRemoteOnly
|
2012-10-31 06:34:03 +00:00
|
|
|
customgroups :: [(Text, RepoGroup)]
|
2015-01-28 20:11:28 +00:00
|
|
|
customgroups = case repoGroup d of
|
2012-10-31 06:34:03 +00:00
|
|
|
RepoGroupCustom s -> [(T.pack s, RepoGroupCustom s)]
|
|
|
|
_ -> []
|
2012-12-03 02:33:30 +00:00
|
|
|
help = [whamlet|<a href="@{RepoGroupR}">What's this?</a>|]
|
2012-10-09 19:11:48 +00:00
|
|
|
|
2015-01-28 20:11:28 +00:00
|
|
|
associateddirectory = case repoAssociatedDirectory d of
|
2013-04-26 19:06:18 +00:00
|
|
|
Nothing -> aopt hiddenField "" Nothing
|
2015-01-28 20:11:28 +00:00
|
|
|
Just dir -> aopt textField (bfs "Associated directory") (Just $ Just dir)
|
2013-04-26 17:00:14 +00:00
|
|
|
|
2013-11-07 22:02:00 +00:00
|
|
|
getEditRepositoryR :: RepoId -> Handler Html
|
2013-03-16 22:48:23 +00:00
|
|
|
getEditRepositoryR = postEditRepositoryR
|
|
|
|
|
2013-11-07 22:02:00 +00:00
|
|
|
postEditRepositoryR :: RepoId -> Handler Html
|
2013-03-16 22:48:23 +00:00
|
|
|
postEditRepositoryR = editForm False
|
2012-10-11 21:35:08 +00:00
|
|
|
|
2013-06-27 05:15:28 +00:00
|
|
|
getEditNewRepositoryR :: UUID -> Handler Html
|
2013-03-16 22:48:23 +00:00
|
|
|
getEditNewRepositoryR = postEditNewRepositoryR
|
|
|
|
|
2013-06-27 05:15:28 +00:00
|
|
|
postEditNewRepositoryR :: UUID -> Handler Html
|
2013-11-07 22:02:00 +00:00
|
|
|
postEditNewRepositoryR = editForm True . RepoUUID
|
2012-10-11 21:35:08 +00:00
|
|
|
|
2013-06-27 05:15:28 +00:00
|
|
|
getEditNewCloudRepositoryR :: UUID -> Handler Html
|
2013-03-16 22:48:23 +00:00
|
|
|
getEditNewCloudRepositoryR = postEditNewCloudRepositoryR
|
|
|
|
|
2013-06-27 05:15:28 +00:00
|
|
|
postEditNewCloudRepositoryR :: UUID -> Handler Html
|
2014-04-09 20:27:24 +00:00
|
|
|
postEditNewCloudRepositoryR uuid = connectionNeeded >> editForm True (RepoUUID uuid)
|
2012-10-27 16:25:29 +00:00
|
|
|
|
2013-11-07 22:02:00 +00:00
|
|
|
editForm :: Bool -> RepoId -> Handler Html
|
2014-08-12 20:35:13 +00:00
|
|
|
editForm new (RepoUUID uuid)
|
2014-12-17 17:59:23 +00:00
|
|
|
| uuid == webUUID || uuid == bitTorrentUUID = page "The web" (Just Configuration) $ do
|
2014-08-12 20:35:13 +00:00
|
|
|
$(widgetFile "configurators/edit/webrepository")
|
|
|
|
| otherwise = page "Edit repository" (Just Configuration) $ do
|
|
|
|
mremote <- liftAnnex $ Remote.remoteFromUUID uuid
|
|
|
|
when (mremote == Nothing) $
|
|
|
|
whenM ((/=) uuid <$> liftAnnex getUUID) $
|
|
|
|
error "unknown remote"
|
|
|
|
curr <- liftAnnex $ getRepoConfig uuid mremote
|
|
|
|
liftAnnex $ checkAssociatedDirectory curr mremote
|
|
|
|
((result, form), enctype) <- liftH $
|
|
|
|
runFormPostNoToken $ renderBootstrap3 bootstrapFormLayout $ editRepositoryAForm mremote curr
|
|
|
|
case result of
|
|
|
|
FormSuccess input -> liftH $ do
|
|
|
|
setRepoConfig uuid mremote curr input
|
|
|
|
liftAnnex $ checkAssociatedDirectory input mremote
|
|
|
|
redirect DashboardR
|
|
|
|
_ -> do
|
|
|
|
let istransfer = repoGroup curr == RepoGroupStandard TransferGroup
|
|
|
|
config <- liftAnnex $ M.lookup uuid <$> readRemoteLog
|
|
|
|
let repoInfo = getRepoInfo mremote config
|
|
|
|
let repoEncryption = getRepoEncryption mremote config
|
|
|
|
$(widgetFile "configurators/edit/repository")
|
2013-12-19 02:39:15 +00:00
|
|
|
editForm _new r@(RepoName _) = page "Edit repository" (Just Configuration) $ do
|
2013-11-07 22:02:00 +00:00
|
|
|
mr <- liftAnnex (repoIdRemote r)
|
|
|
|
let repoInfo = getRepoInfo mr Nothing
|
|
|
|
g <- liftAnnex gitRepo
|
|
|
|
let sshrepo = maybe False (remoteLocationIsSshUrl . flip parseRemoteLocation g . Git.repoLocation . Remote.repo) mr
|
|
|
|
$(widgetFile "configurators/edit/nonannexremote")
|
2013-04-26 17:00:14 +00:00
|
|
|
|
|
|
|
{- Makes any directory associated with the repository. -}
|
|
|
|
checkAssociatedDirectory :: RepoConfig -> Maybe Remote -> Annex ()
|
|
|
|
checkAssociatedDirectory _ Nothing = noop
|
|
|
|
checkAssociatedDirectory cfg (Just r) = do
|
|
|
|
repoconfig <- M.lookup (Remote.uuid r) <$> readRemoteLog
|
|
|
|
case repoGroup cfg of
|
|
|
|
RepoGroupStandard gr -> case associatedDirectory repoconfig gr of
|
|
|
|
Just d -> inRepo $ \g ->
|
2013-04-26 03:44:55 +00:00
|
|
|
createDirectoryIfMissing True $
|
|
|
|
Git.repoPath g </> d
|
|
|
|
Nothing -> noop
|
|
|
|
_ -> noop
|
2013-04-25 20:42:17 +00:00
|
|
|
|
|
|
|
getRepoInfo :: Maybe Remote.Remote -> Maybe Remote.RemoteConfig -> Widget
|
|
|
|
getRepoInfo (Just r) (Just c) = case M.lookup "type" c of
|
|
|
|
Just "S3"
|
|
|
|
#ifdef WITH_S3
|
2014-10-23 19:56:35 +00:00
|
|
|
| S3.configIA c -> IA.getRepoInfo c
|
2013-04-25 20:42:17 +00:00
|
|
|
#endif
|
|
|
|
| otherwise -> AWS.getRepoInfo c
|
|
|
|
Just t
|
|
|
|
| t /= "git" -> [whamlet|#{t} remote|]
|
|
|
|
_ -> getGitRepoInfo $ Remote.repo r
|
|
|
|
getRepoInfo (Just r) _ = getRepoInfo (Just r) (Just $ Remote.config r)
|
|
|
|
getRepoInfo _ _ = [whamlet|git repository|]
|
|
|
|
|
|
|
|
getGitRepoInfo :: Git.Repo -> Widget
|
|
|
|
getGitRepoInfo r = do
|
|
|
|
let loc = Git.repoLocation r
|
|
|
|
[whamlet|git repository located at <tt>#{loc}</tt>|]
|
2013-09-18 00:02:42 +00:00
|
|
|
|
|
|
|
getRepoEncryption :: Maybe Remote.Remote -> Maybe Remote.RemoteConfig -> Widget
|
|
|
|
getRepoEncryption (Just _) (Just c) = case extractCipher c of
|
2014-10-09 18:53:13 +00:00
|
|
|
Nothing ->
|
2013-09-18 00:02:42 +00:00
|
|
|
[whamlet|not encrypted|]
|
|
|
|
(Just (SharedCipher _)) ->
|
|
|
|
[whamlet|encrypted: encryption key stored in git repository|]
|
2016-05-11 16:37:13 +00:00
|
|
|
(Just (EncryptedCipher _ _ ks)) -> desckeys ks
|
|
|
|
(Just (SharedPubKeyCipher _ ks)) -> desckeys ks
|
|
|
|
where
|
|
|
|
desckeys (KeyIds { keyIds = ks }) = do
|
2015-09-09 22:06:49 +00:00
|
|
|
cmd <- liftAnnex $ gpgCmd <$> Annex.getGitConfig
|
|
|
|
knownkeys <- liftIO (secretKeys cmd)
|
2013-09-18 00:02:42 +00:00
|
|
|
[whamlet|
|
|
|
|
encrypted using gpg key:
|
|
|
|
<ul style="list-style: none">
|
|
|
|
$forall k <- ks
|
|
|
|
<li>
|
|
|
|
^{gpgKeyDisplay k (M.lookup k knownkeys)}
|
|
|
|
|]
|
2013-09-20 16:19:55 +00:00
|
|
|
getRepoEncryption _ _ = return () -- local repo
|
2013-11-07 22:02:00 +00:00
|
|
|
|
|
|
|
getUpgradeRepositoryR :: RepoId -> Handler ()
|
|
|
|
getUpgradeRepositoryR (RepoUUID _) = redirect DashboardR
|
|
|
|
getUpgradeRepositoryR r = go =<< liftAnnex (repoIdRemote r)
|
|
|
|
where
|
2014-10-09 18:53:13 +00:00
|
|
|
go Nothing = redirect DashboardR
|
2013-11-07 22:02:00 +00:00
|
|
|
go (Just rmt) = do
|
2013-12-21 00:58:36 +00:00
|
|
|
liftIO fixSshKeyPairIdentitiesOnly
|
2013-11-07 22:02:00 +00:00
|
|
|
liftAnnex $ setConfig
|
|
|
|
(remoteConfig (Remote.repo rmt) "ignore")
|
|
|
|
(Git.Config.boolConfig False)
|
|
|
|
liftAnnex $ void Remote.remoteListRefresh
|
2014-01-26 17:57:05 +00:00
|
|
|
liftAssistant updateSyncRemotes
|
|
|
|
liftAssistant $ syncRemote rmt
|
2013-11-07 22:02:00 +00:00
|
|
|
redirect DashboardR
|
2014-04-09 20:27:24 +00:00
|
|
|
|
|
|
|
{- If there is no currently connected remote, display an alert suggesting
|
|
|
|
- to set up one. -}
|
|
|
|
connectionNeeded :: Handler ()
|
|
|
|
connectionNeeded = whenM noconnection $ do
|
|
|
|
urlrender <- getUrlRender
|
|
|
|
void $ liftAssistant $ do
|
|
|
|
close <- asIO1 removeAlert
|
|
|
|
addAlert $ connectionNeededAlert $ AlertButton
|
2014-06-02 22:04:21 +00:00
|
|
|
{ buttonLabel = "Connect"
|
2014-04-09 20:27:24 +00:00
|
|
|
, buttonUrl = urlrender ConnectionNeededR
|
|
|
|
, buttonAction = Just close
|
|
|
|
, buttonPrimary = True
|
|
|
|
}
|
|
|
|
where
|
|
|
|
noconnection = S.null . currentlyConnectedRemotes <$> liftAssistant getDaemonStatus
|
|
|
|
|
|
|
|
getConnectionNeededR :: Handler Html
|
|
|
|
getConnectionNeededR = page "Connection needed" (Just Configuration) $ do
|
|
|
|
$(widgetFile "configurators/needconnection")
|