...
 
Commits (4)
This diff is collapsed.
......@@ -3,10 +3,14 @@
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Helpers.fs" />
<Compile Include="Version.fs" />
<Compile Include="Data.fs" />
<Compile Include="Fetch.fs" />
<Compile Include="Search.fs" />
<Compile Include="Release.fs" />
<Compile Include="Channel.fs" />
<Compile Include="ChannelsTable.fs" />
<Compile Include="App.fs" />
</ItemGroup>
<ItemGroup>
......
namespace VersionsOfDotNet
open Data
open Elmish
open Fable.Helpers.React
open Fable.Helpers.React.Props
open System
module Channel =
type Info =
{ LifecyclePolicy: Url
Releases: Release.State list }
type State =
{ Index: IndexEntry
Info: Loadable<Info>
Expanded: bool
Guid: Guid }
type Msg =
| Load
| Fetched of Data.Channel
| FetchError of exn
| Expand
| Collapse
| ReleaseMsg of Guid * Release.Msg
let releaseMsg state msg = ReleaseMsg (state, msg)
let fetchChannelCmd url =
Cmd.ofPromise Fetch.channel url Fetched FetchError
let init index =
{ Index = index
Info = Unloaded
Expanded = false
Guid = Guid.NewGuid() }, Cmd.none
let update msg state =
match msg with
| Load ->
{ state with Info = Loading }, fetchChannelCmd state.Index.ReleasesJson
| Fetched channel ->
let releases =
channel.Releases
|> List.map Release.init
let info =
{ LifecyclePolicy = channel.LifecyclePolicy
Releases = releases |> List.map fst }
let cmds =
releases
|> List.map (fun (state, cmd) -> Cmd.map (releaseMsg state.Guid) cmd)
|> Cmd.batch
{ state with Info = Loaded info }, cmds
| FetchError exn ->
{ state with Info = Error exn }, Cmd.none
| Expand ->
{ state with Expanded = true },
if not (Loadable.isLoaded state.Info)
then Cmd.ofMsg Load
else Cmd.none
| Collapse ->
{ state with Expanded = false }, Cmd.none
| ReleaseMsg (guid, msg) ->
let updateReleaseState info =
let updatedReleases, cmds =
info.Releases
|> List.map (fun rel -> if rel.Guid = guid
then Release.update msg rel
else rel, Cmd.none)
|> List.unzip
{ info with Releases = updatedReleases }, cmds |> Cmd.batch
let info, cmd = state.Info |> Loadable.map updateReleaseState |> Loadable.unzip
{ state with
Info = info },
match cmd with Loaded cmd -> Cmd.map (releaseMsg guid) cmd | _ -> Cmd.none
let headRow =
tr [ ]
[ th [ Class "hide-border" ] [ ]
th [ ] [ ]
th [ ] [ str "Channel" ]
th [ ] [ str "Latest release" ]
th [ ] [ str "Latest release date" ]
th [ ] [ str "Support" ]
th [ ] [ str "End of Life date" ] ]
let view last state dispatch =
let index = state.Index
let supportIndicator supportPhase =
match supportPhase with
| "preview" ->
[ span [ Class "status-indicator preview"
Title "Preview" ] [ ]
span [ ] [ str "Preview" ] ]
| "lts" ->
[ span [ Class "status-indicator lts"
Title "Long Term Support" ] [ ]
span [ ] [ str "Long Term Support" ] ]
| "eol" ->
[ span [ Class "status-indicator eol"
Title "End of Life" ] [ ]
span [ ] [ str "End of Life" ] ]
| "maintenance" ->
[ span [ Class "status-indicator maintenance"
Title "Maintenance" ] [ ]
span [ ] [ str "Maintenance" ] ]
| t ->
[ span [ Class "status-indicator unknown"
Title t ] [ str "?" ]
span [ ] [ str t ] ]
let channelRow =
let toggleExpand _ =
(if state.Expanded then Collapse else Expand) |> dispatch
tr [ OnClick toggleExpand ]
[ td [ Class "expand-button"
ColSpan 2 ]
[ (if state.Expanded then View.chevronDown else View.chevronRight) [ ] ]
td [ ] [ str <| string index.ChannelVersion ]
td [ ] [ str <| string index.LatestRelease ]
td [ ] [ View.dateToHtmlTime index.LatestReleaseDate ]
td [ ] [ div [ Class "status-box" ] ( supportIndicator index.SupportPhase ) ]
td [ ] [ ( match index.EolDate with
| Some d -> View.dateToHtmlTime d
| None -> str "-" ) ] ]
let expandedChannel () =
match state.Info with
| Unloaded | Loading ->
[ tr [ ]
[ td [ ColSpan 7 ]
[ div [ Class "expanded-loading" ]
[ div [ Class "loading" ] [ ] ] ] ] ]
| Error ex ->
[ tr [ ]
[ td [ ColSpan 7 ]
[ div [ Class "channel-error column" ]
( View.errorView ex (fun _ -> dispatch Load) ) ] ] ]
| Loaded info ->
[ tr [ ]
[ th [ ] [ ]
th [ ] [ ]
th [ ] [ str "Version" ]
th [ ] [ str "Release date" ]
th [ ] [ str "Runtime" ]
th [ ] [ str "Sdk" ]
th [ ] [ ] ] ] @
[ for r in info.Releases do
yield! Release.view r (releaseMsg r.Guid >> dispatch) ] @
if last then [ ] else [ headRow ]
[ yield channelRow
if state.Expanded then yield! expandedChannel () ]
\ No newline at end of file
namespace VersionsOfDotNet
open Data
open Elmish
open Search
open Fable.Helpers.React
open Fable.Helpers.React.Props
open System
module ChannelsTable =
type State =
{ Channels: Loadable<Channel.State list>
Filter: Search.Filter }
type Msg =
| Load
| Filter of Search.Filter
| Fetched of IndexEntry list
| FetchError of exn
| ChannelMsg of Guid * Channel.Msg
let channelMsg state msg = ChannelMsg (state, msg)
let fetchIndexCmd =
Cmd.ofPromise Fetch.index () Fetched FetchError
let init () =
{ Channels = Unloaded
Filter = ShowAll },
Cmd.ofMsg Load
let update msg state =
match msg with
| Load ->
{ state with Channels = Loading }, fetchIndexCmd
| Filter filter ->
printfn "Channels filter: %A" filter
{ state with Filter = filter }, Cmd.none
| Fetched index ->
let channels =
index
|> List.map Channel.init
let states = channels |> List.map fst
let latestChannel =
states
|> List.filter (fun c -> c.Index.SupportPhase <> "preview")
|> List.maxBy (fun c -> c.Index.LatestRelease)
let cmds =
channels
|> List.map (fun (state, cmd) -> Cmd.map (channelMsg state.Guid) cmd)
|> fun list -> Cmd.ofMsg (channelMsg latestChannel.Guid Channel.Load) :: list
|> Cmd.batch
{ state with Channels = Loaded states }, cmds
| FetchError ex ->
{ state with Channels = Error ex }, Cmd.none
| ChannelMsg (guid, msg) ->
let updateChannel (channels: Channel.State list) =
channels
|> List.map (fun channel ->
if channel.Guid = guid
then Channel.update msg channel
else channel, Cmd.none)
|> List.unzip
let loadableChannels, cmds = state.Channels |> Loadable.map updateChannel |> Loadable.unzip
{ state with Channels = loadableChannels },
Cmd.map (channelMsg guid) (match cmds with Loaded cmds -> Cmd.batch cmds | _ -> Cmd.none)
let view state dispatch =
match state.Channels with
| Unloaded | Loading ->
div [ Class "main-loading" ]
[ div [ Class "loading" ] [ ] ]
| Error ex ->
div [ Class "main-error" ]
[ div [ Class "container column" ]
( View.errorView ex (fun _ -> dispatch Load) ) ]
| Loaded channels ->
let releaseFilter filter (release: Release.State) =
let release = release.Release
match filter with
| ShowAll -> true
| WithPrefix (pf, version) ->
match pf with
| Release -> release.ReleaseVersion |> Version.matches version
| Runtime -> release.Runtime |> Option.exists (fun r -> r.Version |> Version.matches version)
| Sdk -> release.Sdk.Version |> Version.matches version
| AspRuntime -> release.AspnetcoreRuntime
|> Option.exists (fun a -> a.Version |> Version.matches version)
| AspModule ->
match release.AspnetcoreRuntime with
| Some { VersionAspnetcoremodule = Some vs } ->
vs |> List.exists (Version.matches version)
| _ -> false
| Generic version ->
release.ReleaseVersion |> Version.matches version ||
release.Sdk.Version |> Version.matches version ||
release.Runtime |> Option.exists (fun r -> r.Version |> Version.matches version)
let filterChannels filter channels =
match filter with
| ShowAll -> channels
| filter ->
let filterReleases (channelModel: Channel.State) =
let newInfo =
channelModel.Info
|> Loadable.map (fun info ->
{ info with Releases = List.filter (releaseFilter filter) info.Releases })
{ channelModel with Info = newInfo }
let filterChannelModel (channelModel: Channel.State) =
match channelModel.Info, filter with
| _, ShowAll -> true
| Loaded info, _ -> not info.Releases.IsEmpty
| _, Generic v -> channelModel.Index.ChannelVersion |> Version.mightMatchInChannel v
| _, WithPrefix (pf, v) ->
match pf with
| Release | Runtime | Sdk | AspRuntime ->
channelModel.Index.ChannelVersion |> Version.mightMatchInChannel v
| AspModule -> true
let filtered =
channels
|> List.map filterReleases
|> List.filter filterChannelModel
do filtered
|> List.filter (fun c -> c.Info = Unloaded)
|> List.map (fun c -> ChannelMsg (c.Guid, Channel.Load))
|> List.iter dispatch
filtered
let filtered = filterChannels state.Filter channels
if filtered |> List.isEmpty then
div [ Class "channel-error column" ]
[ span [ Class "error-symbol" ]
[ str "×" ]
span [ Class "error-title" ]
[ str "No releases found." ] ]
else
table [ ]
[ thead [ ]
[ Channel.headRow ]
tbody [ ]
[ for c in filtered do
yield! Channel.view (c = List.last filtered) c (channelMsg c.Guid >> dispatch) ] ]
namespace VersionsOfDotNet
open Thoth.Json
open System
open VersionsOfDotNet // To get our Version type, instead of the one in System
open Thoth.Json
module Data =
type Url = string
......@@ -15,8 +15,8 @@ module Data =
| Ok s ->
match Version.parse s with
| Some v -> Ok v
| None -> (path, Decode.BadPrimitive("a version", value)) |> Error
| Error v -> Error v)
| None -> (path, Decode.BadPrimitive("a version", value)) |> Result.Error
| Result.Error v -> Result.Error v)
let private getOptionalDate (get: Decode.IGetters) jsonName =
get.Required.Field jsonName Decode.string
......
......@@ -19,7 +19,7 @@ module Fetch =
let decoder = Decode.object (fun get -> get.Required.Field "releases-index" (Decode.list IndexEntry.Decoder))
match Decode.fromString decoder json with
| Ok res -> return res
| Error e -> return failwith e
| Result.Error e -> return failwith e
}
let channel (githubUrl: Url) =
......@@ -29,6 +29,6 @@ module Fetch =
let! response = fetch url []
let! json = response.text()
match Decode.fromString Channel.Decoder json with
| Ok res -> return githubUrl, res
| Error e -> return failwith e
| Ok res -> return res
| Result.Error e -> return failwith e
}
namespace VersionsOfDotNet
open Fable.Helpers.React
open Fable.Helpers.React.Props
open System
type Loadable<'t> =
| Unloaded
| Loading
| Error of exn
| Loaded of 't
[<RequireQualifiedAccess>]
module Loadable =
let isLoaded x =
match x with
| Loaded _ -> true
| _ -> false
let map f x =
match x with
| Unloaded -> Unloaded | Loading -> Loading | Error e -> Error e
| Loaded t -> Loaded (f t)
let unzip x =
map fst x, map snd x
[<RequireQualifiedAccess>]
module Int =
let parse i =
match Int32.TryParse i with
| (true, r) -> Some r
| _ -> None
[<RequireQualifiedAccess>]
module String =
let split (sep: char) (s: string) = s.Split(sep) |> Array.toList
let trim (s: string) = s.Trim()
let toLowerInvariant (s: string) = s.ToLowerInvariant()
let isEmpty (s: string) = String.IsNullOrEmpty(s)
let isWhitespace (s: string) = String.IsNullOrWhiteSpace(s)
[<RequireQualifiedAccess>]
module Option =
let mapList optionList =
if optionList |> List.forall Option.isSome
then optionList |> List.map Option.get |> Some
else None
module View =
let chevronRight props = button props [ img [ Src "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjwhRE9DVFlQRSBzdmcgIFBVQkxJQyAnLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4nICAnaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkJz48c3ZnIGhlaWdodD0iNTEycHgiIGlkPSJMYXllcl8xIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyOyIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgd2lkdGg9IjUxMnB4IiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48cGF0aCBkPSJNMjk4LjMsMjU2TDI5OC4zLDI1NkwyOTguMywyNTZMMTMxLjEsODEuOWMtNC4yLTQuMy00LjEtMTEuNCwwLjItMTUuOGwyOS45LTMwLjZjNC4zLTQuNCwxMS4zLTQuNSwxNS41LTAuMmwyMDQuMiwyMTIuNyAgYzIuMiwyLjIsMy4yLDUuMiwzLDguMWMwLjEsMy0wLjksNS45LTMsOC4xTDE3Ni43LDQ3Ni44Yy00LjIsNC4zLTExLjIsNC4yLTE1LjUtMC4yTDEzMS4zLDQ0NmMtNC4zLTQuNC00LjQtMTEuNS0wLjItMTUuOCAgTDI5OC4zLDI1NnoiLz48L3N2Zz4=" ] ]
let chevronDown props = button props [ img [ Src "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjwhRE9DVFlQRSBzdmcgIFBVQkxJQyAnLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4nICAnaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkJz48c3ZnIGhlaWdodD0iNTEycHgiIGlkPSJMYXllcl8xIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyOyIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgd2lkdGg9IjUxMnB4IiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48cGF0aCBkPSJNMjU2LDI5OC4zTDI1NiwyOTguM0wyNTYsMjk4LjNsMTc0LjItMTY3LjJjNC4zLTQuMiwxMS40LTQuMSwxNS44LDAuMmwzMC42LDI5LjljNC40LDQuMyw0LjUsMTEuMywwLjIsMTUuNUwyNjQuMSwzODAuOSAgYy0yLjIsMi4yLTUuMiwzLjItOC4xLDNjLTMsMC4xLTUuOS0wLjktOC4xLTNMMzUuMiwxNzYuN2MtNC4zLTQuMi00LjItMTEuMiwwLjItMTUuNUw2NiwxMzEuM2M0LjQtNC4zLDExLjUtNC40LDE1LjgtMC4yTDI1NiwyOTguMyAgeiIvPjwvc3ZnPg==" ] ]
let dateToHtmlTime (date: DateTime) =
let s = date.ToString("yyyy-MM-dd")
time [ Props.DateTime s ] [ str s ]
let errorView (ex: exn) retryFun =
[ span [ Class "error-symbol" ]
[ str "×" ]
span [ Class "error-title" ]
[ str "An error occurred." ]
button [ Class "error-retry-button"
OnClick retryFun ]
[ str "Try again" ]
span [ Class "error-details" ]
[ str (sprintf "Details: %s" ex.Message) ]
a [ Class "error-issue-link"
Href (sprintf "https://github.com/arthurrump/versionsof.net/issues/new?body=Error details: %s" ex.Message) ]
[ str "Open an issue" ] ]
\ No newline at end of file
namespace VersionsOfDotNet
open Data
open Fable.Helpers.React
open Fable.Helpers.React.Props
open Elmish
open Fable.Core
open System
module Release =
type State =
{ Release: Release
Expanded: bool
Guid: Guid }
type Msg =
| Expand
| Collapse
let init rel =
{ Release = rel
Expanded = false
Guid = Guid.NewGuid() }, Cmd.none
let update msg state =
match msg with
| Expand -> { state with Expanded = true }, Cmd.none
| Collapse -> { state with Expanded = false }, Cmd.none
let view state dispatch =
let r = state.Release
let securityIndicator =
if r.Security then
[ div [ Class "status-box" ]
[ span [ Class "status-indicator security"
Title "Security" ] [ str "!" ]
span [ ] [ str "Security" ] ] ]
else [ ]
let expandedRelease () =
let fullRuntimeVersion (runtime: Runtime option) =
runtime
|> Option.exists (fun r -> r.VersionDisplay
|> Option.exists (fun vd -> r.Version |> Version.displayedAs vd))
let fullSdkVersion (sdk: Sdk) =
sdk.VersionDisplay |> Option.exists (fun vd -> sdk.Version |> Version.displayedAs vd)
let lif fmt = Printf.kprintf (fun s -> li [ ] [ str s ]) fmt
let lia href text = li [ ] [ a [ Href href ] [ str text ] ]
let (|SomeText|_|) input =
match input with
| Some i when not (String.isWhitespace i) -> Some i
| _ -> None
tr [ ]
[ td [ Class "hide-border" ] [ ]
td [ Class "hide-border" ] [ ]
td [ Class "hide-border"
ColSpan 5 ]
[ ul [ Class "expanded-release" ]
[ if fullRuntimeVersion r.Runtime then
yield lif "Runtime version %O" r.Runtime.Value.Version
if fullSdkVersion r.Sdk then
yield lif "Sdk version %O" r.Sdk.Version
match r.Sdk.VsVersion with SomeText v -> yield lif "Included in Visual Studio %s" v | _ -> ()
match r.Sdk.CsharpLanguage with SomeText v -> yield lif "Supports C# %s" v | _ -> ()
match r.Sdk.FsharpLanguage with SomeText v -> yield lif "Supports F# %s" v | _ -> ()
match r.Sdk.VbLanguage with SomeText v -> yield lif "Supports Visual Basic %s" v | _ -> ()
match r.AspnetcoreRuntime with
| Some a ->
yield lif "ASP.NET Core Runtime %O" a.Version
match a.VersionAspnetcoremodule with
| Some a when not a.IsEmpty ->
yield lif "ASP.NET Core IIS Module %O" a.Head
| _ -> ()
| None -> ()
match r.ReleaseNotes with Some url -> yield lia url "Release notes" | None -> () ] ] ]
[ yield
tr [ OnClick (fun _ -> (if state.Expanded then Collapse else Expand)
|> dispatch) ]
[ td [ Class "hide-border" ] [ ]
td [ Class "expand-button" ]
[ (if state.Expanded then View.chevronDown else View.chevronRight) [ ] ]
td [ ] [ str (string r.ReleaseVersion) ]
td [ ] [ View.dateToHtmlTime r.ReleaseDate ]
td [ ] [ str (match r.Runtime with
| Some r -> Option.defaultValue (string r.Version) r.VersionDisplay
| None -> "-") ]
td [ ] [ str (Option.defaultValue (string r.Sdk.Version) r.Sdk.VersionDisplay) ]
td [ ] ( securityIndicator ) ]
if state.Expanded then yield expandedRelease () ]
......@@ -8,6 +8,7 @@ open Version
open Fable.Import.React
open Fable.Import
open Elmish.React
open Elmish
module Search =
type QueryPrefix =
......@@ -34,7 +35,7 @@ module Search =
Label: string
Valid: SuggestionValidity }
type Model =
type State =
{ InFocus: bool
Query: string
Suggestions: SearchSuggestion list
......@@ -110,22 +111,23 @@ module Search =
Query = ""
Suggestions = suggestionsForQuery ShowAll ""
SelectedSuggestion = (suggestionsForQuery ShowAll "").Head
Filter = ShowAll }
Filter = ShowAll }, Cmd.none
let update msg model =
match msg with
| FocusChanged focus ->
{ model with InFocus = focus
SelectedSuggestion = if focus then model.SelectedSuggestion else model.Suggestions.Head }
SelectedSuggestion = if focus then model.SelectedSuggestion else model.Suggestions.Head },
Cmd.none
| QueryChanged query ->
let sug = suggestionsForQuery model.Filter query
{ model with Query = query; Suggestions = sug; SelectedSuggestion = sug.Head }
{ model with Query = query; Suggestions = sug; SelectedSuggestion = sug.Head }, Cmd.none
| SelectionChanged selected ->
{ model with SelectedSuggestion = selected }
{ model with SelectedSuggestion = selected }, Cmd.none
| FilterSet (filter, queryText) ->
let sugs = suggestionsForQuery filter model.Query
{ model with Filter = filter; Query = queryText
Suggestions = sugs; SelectedSuggestion = sugs.Head }
Suggestions = sugs; SelectedSuggestion = sugs.Head }, Cmd.none
module private Refs =
let mutable input : Browser.HTMLElement = null
......
......@@ -2,6 +2,9 @@ namespace VersionsOfDotNet
open System
#nowarn "342" // Don't warn about implementing Equals
[<CustomComparison; StructuralEquality>]
type Version =
{ Numbers: int list
Preview: string option }
......@@ -12,24 +15,23 @@ type Version =
| Some preview -> sprintf "%s-%s" s preview
| None -> s
module Version =
module private Int =
let parse i =
match Int32.TryParse i with
| (true, r) -> Some r
| _ -> None
module private String =
let split (sep: char) (s: string) = s.Split(sep) |> Array.toList
let trim (s: string) = s.Trim()
let toLowerInvariant (s: string) = s.ToLowerInvariant()
module private Option =
let mapList optionList =
if optionList |> List.forall Option.isSome
then optionList |> List.map Option.get |> Some
else None
member this.CompareTo(other) =
match compare this.Numbers other.Numbers with
| 0 ->
match this.Preview, other.Preview with
| None, None -> 0
| Some p1, Some p2 -> compare p1 p2
| Some _, None -> -1
| None, Some _ -> 1
| c -> c
interface IComparable with
member this.CompareTo(other: obj) =
match other with
| :? Version as v -> this.CompareTo(v)
| _ -> invalidArg "other" "cannot compare values of different types"
module Version =
let parse (s: string) =
match s |> String.trim |> String.toLowerInvariant |> String.split '-' with
| [ ] | [ "" ] -> None
......
......@@ -107,7 +107,7 @@ button:hover {
}
}
.error-title, .error-details {
.error-title, .error-details, .error-issue-link {
text-align: center;
}
......@@ -115,7 +115,7 @@ button:hover {
margin-top: 1rem;
}
.error-details {
.error-details, .error-issue-link {
font-size: 0.8rem;
}
......@@ -418,4 +418,8 @@ a {
footer a {
text-decoration: none;
}
.main-error .error-issue-link {
color: #fff;
}
\ No newline at end of file