Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New crystal with contexts. #690

Merged
merged 2 commits into from
Apr 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions common/src/main/scala/explore/AppMain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import cats.effect.IOApp
import cats.syntax.all._
import clue.WebSocketReconnectionStrategy
import clue.js.WebSocketJSBackend
import crystal.AppRootContext
import crystal.react.AppRoot
import explore.components.ui.ExploreStyles
import explore.model.AppConfig
import explore.model.AppContext
Expand Down Expand Up @@ -42,12 +40,14 @@ import scala.concurrent.duration._
import scala.scalajs.js

import js.annotation._

object AppCtx extends AppRootContext[AppContextIO]
import japgolly.scalajs.react.Reusability
import crystal.react._

trait AppMain extends IOApp {
LogLevelLogger.setLevel(LogLevelLogger.Level.INFO)

implicit val reuseContext: Reusability[AppContextIO] = Reusability.never

implicit val logger: Logger[IO] = LogLevelLogger.createForRoot[IO]

implicit val gqlStreamingBackend: WebSocketJSBackend[IO] = WebSocketJSBackend[IO]
Expand Down Expand Up @@ -136,19 +136,20 @@ trait AppMain extends IOApp {
}

(for {
_ <- utils.setupScheme[IO](Theme.Dark)
appConfig <- fetchConfig
_ <- logger.info(s"Git Commit: [${BuildInfo.gitHeadCommit.getOrElse("NONE")}]")
_ <- logger.info(s"Config: ${appConfig.show}")
ctx <- AppContext.from[IO](appConfig, reconnectionStrategy, pageUrl, IO.fromFuture)
r <- (ctx.sso.whoami,
setupDOM(),
showEnvironment(appConfig.environment),
AppCtx.initIn[IO](ctx)
).parTupled
(vault, container, _, _) = r
_ <- utils.setupScheme[IO](Theme.Dark)
appConfig <- fetchConfig
_ <- logger.info(s"Git Commit: [${BuildInfo.gitHeadCommit.getOrElse("NONE")}]")
_ <- logger.info(s"Config: ${appConfig.show}")
ctx <- AppContext.from[IO](appConfig, reconnectionStrategy, pageUrl, IO.fromFuture)
r <- (ctx.sso.whoami, setupDOM(), showEnvironment(appConfig.environment)).parTupled
(vault, container, _) = r
} yield {
val RootComponent = AppRoot[IO](initialModel(vault))(rootView => rootComponent(rootView))
val RootComponent =
ContextProvider[IO](AppCtx, ctx)(
ContextProvider[IO](HelpCtx, HelpContext(none))(
StateProvider[IO](initialModel(vault))(rootView => rootComponent(rootView))
)
)

RootComponent().renderIntoDOM(container)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import explore.model.ObsSummary
import explore.model.reusability._
import explore.schemas.ObservationDB
import explore.schemas.ObservationDB.Types._
import japgolly.scalajs.react.Reusability
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.html_<^._
import lucuma.core.model.ConstraintSet
import lucuma.core.model.Observation
Expand Down Expand Up @@ -668,14 +668,9 @@ object ConstraintSetObsQueries {
implicit val constraintSetWithObsReusability: Reusability[ConstraintSetsWithObs] =
Reusability.derive

type LiveQueryRenderer =
(
View[ConstraintSetsWithObs] => VdomNode
) => LiveQueryRenderMod[ObservationDB, ConstraintSetsObsQuery.Data, ConstraintSetsWithObs]

val ConstraintSetObsLiveQuery: LiveQueryRenderer =
render =>
AppCtx.runWithCtx { implicit appCtx =>
val ConstraintSetObsLiveQuery =
ScalaFnComponent[View[ConstraintSetsWithObs] => VdomNode](render =>
AppCtx.using { implicit appCtx =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it so that we execute this on IO when calling using? Or is it different than runWitCtx in that sense

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inner workings are different than before. Ctx.using (or Ctx.usingView) return a VdomElement, so they can only be used within render. In other words, they act as a component; and since it's declarative, a name with run in it didn't seem fitting anymore. Also, the function passed to them must be a render function, returning a VdomNode.

This is a functionality provided by React, and is basically a way of "teleporting" props further down the component tree, without the need of explicitly passing them in all intermediate components. If the context changes, it is automatically propagated to all .using(View) blocks in the currently rendered component tree.

LiveQueryRenderMod[ObservationDB, ConstraintSetsObsQuery.Data, ConstraintSetsWithObs](
ConstraintSetsObsQuery.query(),
ConstraintSetsObsQuery.Data.asConstraintSetsWithObs.get,
Expand All @@ -685,5 +680,6 @@ object ConstraintSetObsQueries {
)
)(render)
}
)

}
13 changes: 5 additions & 8 deletions common/src/main/scala/explore/common/ObsQueries.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import explore.implicits._
import explore.model.ObsSummary
import explore.model.reusability._
import explore.schemas.ObservationDB
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.html_<^._
import lucuma.core.model.Observation
import lucuma.ui.reusability._
Expand Down Expand Up @@ -202,14 +203,9 @@ object ObsQueries {
// format: on
/* END: Generated by clue. Will be replaced when regenerating. */

type LiveQueryRenderer =
(
View[ObservationList] => VdomNode
) => LiveQueryRenderMod[ObservationDB, ProgramObservationsQuery.Data, ObservationList]

val ObsLiveQuery: LiveQueryRenderer =
render =>
AppCtx.runWithCtx { implicit appCtx =>
val ObsLiveQuery =
ScalaFnComponent[View[ObservationList] => VdomNode](render =>
AppCtx.using { implicit appCtx =>
LiveQueryRenderMod[ObservationDB, ProgramObservationsQuery.Data, ObservationList](
ProgramObservationsQuery.query(),
ProgramObservationsQuery.Data.asObservationList.get,
Expand All @@ -219,6 +215,7 @@ object ObsQueries {
)
)(render)
}
)

@GraphQL
trait ProgramCreateObservationGQL extends GraphQLOperation[ObservationDB] {
Expand Down
14 changes: 5 additions & 9 deletions common/src/main/scala/explore/common/TargetObsQueries.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import io.circe.Decoder
import io.circe.HCursor
import io.circe.generic.semiauto._
import io.circe.refined._
import japgolly.scalajs.react.Reusability
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.html_<^._
import lucuma.core.model.Asterism
import lucuma.core.model.Observation
Expand Down Expand Up @@ -1186,14 +1186,9 @@ object TargetObsQueries {
implicit val targetsWithObsReusability: Reusability[TargetsAndAsterismsWithObs] =
Reusability.derive

type LiveQueryRenderer =
(
View[TargetsAndAsterismsWithObs] => VdomNode
) => LiveQueryRenderMod[ObservationDB, TargetsObsQuery.Data, TargetsAndAsterismsWithObs]

val TargetObsLiveQuery: LiveQueryRenderer =
render =>
AppCtx.runWithCtx { implicit appCtx =>
val TargetObsLiveQuery =
ScalaFnComponent[View[TargetsAndAsterismsWithObs] => VdomNode](render =>
AppCtx.using { implicit appCtx =>
LiveQueryRenderMod[ObservationDB, TargetsObsQuery.Data, TargetsAndAsterismsWithObs](
TargetsObsQuery.query(),
TargetsObsQuery.Data.asTargetsWithObs.get,
Expand All @@ -1205,4 +1200,5 @@ object TargetObsQueries {
)
)(render)
}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ object ConnectionsStatus {
val component = ScalaComponent
.builder[Props]
.render(_ =>
AppCtx.runWithCtx { ctx =>
AppCtx.using { ctx =>
ctx.clients.ODBConnectionStatus(renderStatus("ODB"))
}
)
Expand Down
2 changes: 1 addition & 1 deletion common/src/main/scala/explore/components/InputModal.scala
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ object InputModal {
.builder[Props]
.initialStateFromProps(p => State(p.initialValue.fold("")(_.value)))
.renderPS { ($, props, state) =>
AppCtx.runWithCtx { implicit appCtx =>
AppCtx.using { implicit appCtx =>
val valueView = ViewF.fromState[IO]($).zoom(State.inputValue)

val cleanInput = $.setStateL(State.inputValue)("")
Expand Down
14 changes: 5 additions & 9 deletions common/src/main/scala/explore/components/UserSelectionForm.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,10 @@ final case class UserSelectionForm(
vault: View[Option[UserVault]],
message: View[Option[NonEmptyString]]
) extends ReactProps[UserSelectionForm](UserSelectionForm.component) {
def guest: Callback =
AppCtx.runWithCtx { implicit ctx =>
ctx.sso.guest.flatMap(v => vault.set(v.some)).runAsyncCB
}
def login: Callback =
AppCtx.runWithCtx { implicit ctx =>
ctx.sso.redirectToLogin.runAsyncCB
}
def guest(implicit ctx: AppContextIO): Callback =
ctx.sso.guest.flatMap(v => vault.set(v.some)).runAsyncCB
def login(implicit ctx: AppContextIO): Callback =
ctx.sso.redirectToLogin.runAsyncCB

def supportedOrcidBrowser: CallbackTo[(Boolean, Boolean)] = CallbackTo[(Boolean, Boolean)] {
val browser = new UAParser(dom.window.navigator.userAgent).getBrowser()
Expand Down Expand Up @@ -68,7 +64,7 @@ object UserSelectionForm {
p.supportedOrcidBrowser.map(Function.tupled(State.apply _))
}
.render_PS { (p, s) =>
AppCtx.runWithCtx { implicit ctx =>
AppCtx.using { implicit ctx =>
Modal(
size = ModalSize.Large,
clazz = ExploreStyles.LoginBox,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import cats.syntax.all._
import crystal.react.implicits._
import eu.timepit.refined.cats._
import eu.timepit.refined.types.string.NonEmptyString
import explore.AppCtx
import explore.implicits._
import explore.model.Clients
import io.circe.Json
Expand All @@ -22,7 +21,8 @@ import react.semanticui.sizes._

final case class ConnectionManager(ssoToken: NonEmptyString, onConnect: IO[Unit])(
val render: () => VdomNode
) extends ReactProps[ConnectionManager](ConnectionManager.component)
)(implicit val ctx: AppContextIO)
extends ReactProps[ConnectionManager](ConnectionManager.component)

object ConnectionManager {
type Props = ConnectionManager
Expand Down Expand Up @@ -55,24 +55,21 @@ object ConnectionManager {
.builder[Props]
.initialState(State())
.renderBackend[Backend]
.componentDidMount($ =>
AppCtx.runWithCtx { implicit ctx =>
$.backend.onMount(ctx.clients).runAsyncCB
}
)
.componentDidUpdate($ =>
AppCtx.runWithCtx { implicit ctx =>
(Logger[IO].debug(s"[ConnectionManager] Token changed. Refreshing connections.") >>
$.backend.refresh(ctx.clients))
.whenA($.prevProps.ssoToken =!= $.currentProps.ssoToken)
.runAsyncCB
}
)
.componentWillUnmountConst(
AppCtx.runWithCtx { implicit ctx =>
(Logger[IO].debug(s"[ConnectionManager] Terminating connections.") >>
ctx.clients.close()).runAsyncCB
}
)
.componentDidMount { $ =>
implicit val ctx = $.props.ctx
$.backend.onMount(ctx.clients).runAsyncCB
}
.componentDidUpdate { $ =>
implicit val ctx = $.currentProps.ctx
(Logger[IO].debug(s"[ConnectionManager] Token changed. Refreshing connections.") >>
$.backend.refresh(ctx.clients))
.whenA($.prevProps.ssoToken =!= $.currentProps.ssoToken)
.runAsyncCB
}
.componentWillUnmount { $ =>
implicit val ctx = $.props.ctx
(Logger[IO].debug(s"[ConnectionManager] Terminating connections.") >>
ctx.clients.close()).runAsyncCB
}
.build
}
30 changes: 15 additions & 15 deletions common/src/main/scala/explore/components/state/IfLogged.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,30 @@ object IfLogged {
type Props = IfLogged

// Creates a "profile" for user preferences.
private def createUserPrefs(vault: UserVault): IO[Unit] =
AppCtx.flatMap(implicit ctx =>
UserInsertMutation.execute(Input(vault.user.id.toString)).start.void
)
private def createUserPrefs(vault: UserVault)(implicit ctx: AppContextIO): IO[Unit] =
UserInsertMutation.execute(Input(vault.user.id.toString)).start.void

private val component =
ScalaComponent
.builder[IfLogged]
.stateless
.render_P { p =>
val vaultView = p.view.zoom(RootModel.vault)
val messageView = p.view.zoom(RootModel.userSelectionMessage)
AppCtx.using { implicit ctx =>
val vaultView = p.view.zoom(RootModel.vault)
val messageView = p.view.zoom(RootModel.userSelectionMessage)

vaultView.get.fold[VdomElement](
UserSelectionForm(vaultView, messageView)
) { vault =>
React.Fragment(
SSOManager(vault.expiration, vaultView.set, messageView.set.compose(_.some)),
ConnectionManager(vault.token, onConnect = createUserPrefs(vault))(() =>
LogoutTracker(vaultView.set, messageView.set.compose(_.some))(onLogout =>
p.render(vault, onLogout)
vaultView.get.fold[VdomElement](
UserSelectionForm(vaultView, messageView)
) { vault =>
React.Fragment(
SSOManager(vault.expiration, vaultView.set, messageView.set.compose(_.some)),
ConnectionManager(vault.token, onConnect = createUserPrefs(vault))(() =>
LogoutTracker(vaultView.set, messageView.set.compose(_.some))(onLogout =>
p.render(vault, onLogout)
)
)
)
)
}
}
}
.build
Expand Down
28 changes: 13 additions & 15 deletions common/src/main/scala/explore/components/state/LogoutTracker.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import cats.syntax.all._
import crystal.react.implicits._
import eu.timepit.refined.auto._
import eu.timepit.refined.types.string.NonEmptyString
import explore.AppCtx
import explore.implicits._
import explore.model.UserVault
import explore.utils.ExploreEvent
Expand All @@ -21,7 +20,7 @@ import react.common.ReactProps
final case class LogoutTracker(
setVault: Option[UserVault] => IO[Unit],
setMessage: NonEmptyString => IO[Unit]
)(val render: IO[Unit] => VdomNode)
)(val render: IO[Unit] => VdomNode)(implicit val ctx: AppContextIO)
extends ReactProps[LogoutTracker](LogoutTracker.component)

object LogoutTracker {
Expand All @@ -42,19 +41,18 @@ object LogoutTracker {
)
}
.componentDidMount { $ =>
AppCtx.runWithCtx { implicit ctx =>
IO {
val bc = new BroadcastChannel[ExploreEvent]("explore")
bc.onmessage = (x: ExploreEvent) =>
// This is coming from the js world, we can't match the type
(x.event match {
case ExploreEvent.Logout.event =>
$.props.setVault(none) >> $.props.setMessage("You logged out in another instance")
case _ => IO.unit
})
bc
}.flatMap(bc => $.modStateIn[IO](State.bc.set(bc.some))).runAsyncCB
}
implicit val ctx = $.props.ctx
IO {
val bc = new BroadcastChannel[ExploreEvent]("explore")
bc.onmessage = (x: ExploreEvent) =>
// This is coming from the js world, we can't match the type
(x.event match {
case ExploreEvent.Logout.event =>
$.props.setVault(none) >> $.props.setMessage("You logged out in another instance")
case _ => IO.unit
})
bc
}.flatMap(bc => $.modStateIn[IO](State.bc.set(bc.some))).runAsyncCB
}
.componentWillUnmount($ =>
$.state.bc.map(bc => IO(bc.close()).attempt.void).orEmpty.runAsyncAndForgetCB
Expand Down
Loading