mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 12:51:55 +00:00
compute: add notebook block component (#32290)
This commit is contained in:
parent
154ca95e7c
commit
f0ca58171f
1
.gitignore
vendored
1
.gitignore
vendored
@ -117,6 +117,7 @@ sourcegraph-webapp-*.tgz
|
||||
graphql-operations.ts
|
||||
*.module.scss.d.ts
|
||||
dll-bundle
|
||||
elm-stuff
|
||||
|
||||
# Extensions
|
||||
/client/extension-api/dist
|
||||
|
||||
@ -181,6 +181,18 @@ const config = {
|
||||
type: 'asset/source',
|
||||
})
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.elm$/,
|
||||
exclude: /elm-stuff/,
|
||||
use: {
|
||||
loader: 'elm-webpack-loader',
|
||||
options: {
|
||||
cwd: path.resolve(ROOT_PATH, 'client/web/src/notebooks/blocks/compute/component'),
|
||||
report: 'json',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Disable `CaseSensitivePathsPlugin` by default to speed up development build.
|
||||
// Similar discussion: https://github.com/vercel/next.js/issues/6927#issuecomment-480579191
|
||||
remove(config.plugins, plugin => plugin instanceof CaseSensitivePathsPlugin)
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
import { storiesOf } from '@storybook/react'
|
||||
import { noop } from 'lodash'
|
||||
import React from 'react'
|
||||
|
||||
import { WebStory } from '../../../components/WebStory'
|
||||
|
||||
import { NotebookComputeBlock } from './NotebookComputeBlock'
|
||||
|
||||
const { add } = storiesOf('web/search/notebooks/blocks/compute/NotebookComputeBlock', module).addDecorator(story => (
|
||||
<div className="p-3 container">{story()}</div>
|
||||
))
|
||||
|
||||
const noopBlockCallbacks = {
|
||||
onRunBlock: noop,
|
||||
onBlockInputChange: noop,
|
||||
onSelectBlock: noop,
|
||||
onMoveBlockSelection: noop,
|
||||
onDeleteBlock: noop,
|
||||
onMoveBlock: noop,
|
||||
onDuplicateBlock: noop,
|
||||
}
|
||||
|
||||
add('default', () => (
|
||||
<WebStory>
|
||||
{props => (
|
||||
<NotebookComputeBlock
|
||||
type="compute"
|
||||
{...props}
|
||||
{...noopBlockCallbacks}
|
||||
input=""
|
||||
output=""
|
||||
id="compute-block-1"
|
||||
isSelected={true}
|
||||
isReadOnly={false}
|
||||
isOtherBlockSelected={false}
|
||||
isMacPlatform={true}
|
||||
/>
|
||||
)}
|
||||
</WebStory>
|
||||
))
|
||||
@ -1,5 +1,6 @@
|
||||
import classNames from 'classnames'
|
||||
import React, { useRef } from 'react'
|
||||
import ElmComponent from 'react-elm-components'
|
||||
|
||||
import { ThemeProps } from '@sourcegraph/shared/src/theme'
|
||||
|
||||
@ -10,12 +11,68 @@ import blockStyles from '../NotebookBlock.module.scss'
|
||||
import { useBlockSelection } from '../useBlockSelection'
|
||||
import { useBlockShortcuts } from '../useBlockShortcuts'
|
||||
|
||||
import { Elm } from './component/src/Main.elm'
|
||||
import styles from './NotebookComputeBlock.module.scss'
|
||||
|
||||
interface ComputeBlockProps extends BlockProps, ComputeBlock, ThemeProps {
|
||||
isMacPlatform: boolean
|
||||
}
|
||||
|
||||
interface ElmEvent {
|
||||
data: string
|
||||
eventType?: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
function setupPorts(ports: {
|
||||
receiveEvent: { send: (event: ElmEvent) => void }
|
||||
openStream: { subscribe: (callback: (args: string[]) => void) => void }
|
||||
}): void {
|
||||
const sources: { [key: string]: EventSource } = {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function sendEventToElm(event: any): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
const elmEvent = { data: event.data, eventType: event.type || null, id: event.id || null }
|
||||
ports.receiveEvent.send(elmEvent)
|
||||
}
|
||||
|
||||
function newEventSource(address: string): EventSource {
|
||||
sources[address] = new EventSource(address)
|
||||
return sources[address]
|
||||
}
|
||||
|
||||
function deleteAllEventSources(): void {
|
||||
for (const [key] of Object.entries(sources)) {
|
||||
deleteEventSource(key)
|
||||
}
|
||||
}
|
||||
|
||||
function deleteEventSource(address: string): void {
|
||||
sources[address].close()
|
||||
delete sources[address]
|
||||
}
|
||||
|
||||
ports.openStream.subscribe((args: string[]) => {
|
||||
deleteAllEventSources() // Close any open streams if we receive a request to open a new stream before seeing 'done'.
|
||||
console.log(`stream: ${args[0]}`)
|
||||
const address = args[0]
|
||||
|
||||
const eventSource = newEventSource(address)
|
||||
eventSource.addEventListener('error', () => {
|
||||
console.log('EventSource failed')
|
||||
})
|
||||
eventSource.addEventListener('results', sendEventToElm)
|
||||
eventSource.addEventListener('alert', sendEventToElm)
|
||||
eventSource.addEventListener('error', sendEventToElm)
|
||||
eventSource.addEventListener('done', () => {
|
||||
deleteEventSource(address)
|
||||
// Note: 'done:true' is sent in progress too. But we want a 'done' for the entire stream in case we don't see it.
|
||||
sendEventToElm({ type: 'done', data: '' })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const NotebookComputeBlock: React.FunctionComponent<ComputeBlockProps> = ({
|
||||
id,
|
||||
input,
|
||||
@ -78,7 +135,10 @@ export const NotebookComputeBlock: React.FunctionComponent<ComputeBlockProps> =
|
||||
aria-label="Notebook compute block"
|
||||
ref={blockElement}
|
||||
>
|
||||
<div className="elm" />
|
||||
<div className="elm">
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */}
|
||||
<ElmComponent src={Elm.Main} ports={setupPorts} flags={null} />
|
||||
</div>
|
||||
</div>
|
||||
{blockMenu}
|
||||
</div>
|
||||
|
||||
34
client/web/src/notebooks/blocks/compute/component/elm.json
Normal file
34
client/web/src/notebooks/blocks/compute/component/elm.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"type": "application",
|
||||
"source-directories": ["src"],
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
|
||||
"elm/browser": "1.0.2",
|
||||
"elm/core": "1.0.5",
|
||||
"elm/html": "1.0.0",
|
||||
"elm/json": "1.1.3",
|
||||
"elm/svg": "1.0.1",
|
||||
"elm/time": "1.0.0",
|
||||
"elm/url": "1.0.0",
|
||||
"mdgriffith/elm-ui": "1.1.8",
|
||||
"terezka/elm-charts": "3.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"danhandrea/elm-time-extra": "1.1.0",
|
||||
"debois/elm-dom": "1.3.0",
|
||||
"elm/parser": "1.1.0",
|
||||
"elm/virtual-dom": "1.0.2",
|
||||
"justinmimbs/date": "3.2.1",
|
||||
"justinmimbs/time-extra": "1.1.0",
|
||||
"myrho/elm-round": "1.0.4",
|
||||
"ryannhg/date-format": "2.3.0",
|
||||
"terezka/intervals": "2.0.1"
|
||||
}
|
||||
},
|
||||
"test-dependencies": {
|
||||
"direct": {},
|
||||
"indirect": {}
|
||||
}
|
||||
}
|
||||
639
client/web/src/notebooks/blocks/compute/component/src/Main.elm
Normal file
639
client/web/src/notebooks/blocks/compute/component/src/Main.elm
Normal file
@ -0,0 +1,639 @@
|
||||
port module Main exposing (..)
|
||||
|
||||
import Browser
|
||||
import Chart as C
|
||||
import Chart.Attributes as CA
|
||||
import Dict exposing (Dict)
|
||||
import Element as E
|
||||
import Element.Background as Background
|
||||
import Element.Border as Border
|
||||
import Element.Events
|
||||
import Element.Font as F
|
||||
import Element.Input as I
|
||||
import Html exposing (Html, input, text)
|
||||
import Html.Attributes exposing (..)
|
||||
import Json.Decode as Decode exposing (Decoder, fail, field, maybe)
|
||||
import Json.Decode.Pipeline
|
||||
import Process
|
||||
import Task
|
||||
import Url.Builder
|
||||
import Url.Parser exposing (..)
|
||||
|
||||
|
||||
|
||||
-- CONSTANTS
|
||||
|
||||
|
||||
width : Int
|
||||
width =
|
||||
800
|
||||
|
||||
|
||||
debounceQueryInputMillis : Float
|
||||
debounceQueryInputMillis =
|
||||
400
|
||||
|
||||
|
||||
endpoint : String
|
||||
endpoint =
|
||||
"https://sourcegraph.test:3443/.api"
|
||||
|
||||
|
||||
|
||||
-- MAIN
|
||||
|
||||
|
||||
main : Program () Model Msg
|
||||
main =
|
||||
Browser.element
|
||||
{ init = init
|
||||
, update = update
|
||||
, view = view
|
||||
, subscriptions = subscriptions
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- MODEL
|
||||
|
||||
|
||||
type alias DataValue =
|
||||
{ name : String
|
||||
, value : Float
|
||||
}
|
||||
|
||||
|
||||
type alias Filter a =
|
||||
{ a
|
||||
| dataPoints : Int
|
||||
, sortByCount : Bool
|
||||
, reverse : Bool
|
||||
, excludeStopWords : Bool
|
||||
}
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ query : String
|
||||
, debounce : Int
|
||||
, dataPoints : Int
|
||||
, sortByCount : Bool
|
||||
, reverse : Bool
|
||||
, excludeStopWords : Bool
|
||||
, selectedTab : Tab
|
||||
, resultsMap : Dict String DataValue
|
||||
|
||||
-- Debug client only
|
||||
, serverless : Bool
|
||||
}
|
||||
|
||||
|
||||
init : () -> ( Model, Cmd Msg )
|
||||
init _ =
|
||||
( { query = "repo:.* content:output((.|\\n)* -> $date) type:commit count:all"
|
||||
, dataPoints = 30
|
||||
, sortByCount = True
|
||||
, reverse = False
|
||||
, excludeStopWords = False
|
||||
, selectedTab = Chart
|
||||
, debounce = 0
|
||||
, resultsMap = Dict.empty
|
||||
, serverless = False
|
||||
}
|
||||
, Task.perform identity (Task.succeed RunCompute)
|
||||
)
|
||||
|
||||
|
||||
|
||||
-- PORTS
|
||||
|
||||
|
||||
type alias RawEvent =
|
||||
{ data : String
|
||||
, eventType : Maybe String
|
||||
, id : Maybe String
|
||||
}
|
||||
|
||||
|
||||
port receiveEvent : (RawEvent -> msg) -> Sub msg
|
||||
|
||||
|
||||
port openStream : ( String, Maybe String ) -> Cmd msg
|
||||
|
||||
|
||||
|
||||
-- SUBSCRIPTIONS
|
||||
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions _ =
|
||||
Sub.batch [ receiveEvent eventDecoder ]
|
||||
|
||||
|
||||
eventDecoder : RawEvent -> Msg
|
||||
eventDecoder event =
|
||||
case event.eventType of
|
||||
Just "results" ->
|
||||
OnResults (resultEventDecoder event.data)
|
||||
|
||||
Just "done" ->
|
||||
ResultStreamDone
|
||||
|
||||
_ ->
|
||||
NoOp
|
||||
|
||||
|
||||
resultEventDecoder : String -> List Result
|
||||
resultEventDecoder input =
|
||||
case Decode.decodeString (Decode.list resultDecoder) input of
|
||||
Ok results ->
|
||||
results
|
||||
|
||||
Err _ ->
|
||||
[]
|
||||
|
||||
|
||||
|
||||
-- UPDATE
|
||||
|
||||
|
||||
type Msg
|
||||
= -- User inputs
|
||||
OnQueryChanged String
|
||||
| OnDebounce
|
||||
| OnDataPoints String
|
||||
| OnSortByCheckbox Bool
|
||||
| OnReverseCheckbox Bool
|
||||
| OnExcludeStopWordsCheckbox Bool
|
||||
| OnTabSelected Tab
|
||||
-- Data processing
|
||||
| RunCompute
|
||||
| OnResults (List Result)
|
||||
| ResultStreamDone
|
||||
| NoOp
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
OnQueryChanged newQuery ->
|
||||
( { model | query = newQuery, debounce = model.debounce + 1 }
|
||||
, Task.perform (\_ -> OnDebounce) (Process.sleep debounceQueryInputMillis)
|
||||
)
|
||||
|
||||
OnDebounce ->
|
||||
if model.debounce - 1 == 0 then
|
||||
update RunCompute { model | debounce = model.debounce - 1 }
|
||||
|
||||
else
|
||||
( { model | debounce = model.debounce - 1 }, Cmd.none )
|
||||
|
||||
OnSortByCheckbox sortByCount ->
|
||||
( { model | sortByCount = sortByCount }, Cmd.none )
|
||||
|
||||
OnReverseCheckbox reverse ->
|
||||
( { model | reverse = reverse }, Cmd.none )
|
||||
|
||||
OnExcludeStopWordsCheckbox excludeStopWords ->
|
||||
( { model | excludeStopWords = excludeStopWords }, Cmd.none )
|
||||
|
||||
OnDataPoints i ->
|
||||
let
|
||||
newDataPoints =
|
||||
case String.toInt i of
|
||||
Just n ->
|
||||
n
|
||||
|
||||
Nothing ->
|
||||
0
|
||||
in
|
||||
( { model | dataPoints = newDataPoints }, Cmd.none )
|
||||
|
||||
OnTabSelected selectedTab ->
|
||||
( { model | selectedTab = selectedTab }, Cmd.none )
|
||||
|
||||
RunCompute ->
|
||||
if model.serverless then
|
||||
( { model | resultsMap = exampleResultsMap }, Cmd.none )
|
||||
|
||||
else
|
||||
( { model | resultsMap = Dict.empty }
|
||||
, openStream ( endpoint ++ Url.Builder.absolute [ "compute", "stream" ] [ Url.Builder.string "q" model.query ], Nothing )
|
||||
)
|
||||
|
||||
OnResults r ->
|
||||
( { model | resultsMap = List.foldl updateResultsMap model.resultsMap (parseResults r) }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
ResultStreamDone ->
|
||||
( model, Cmd.none )
|
||||
|
||||
NoOp ->
|
||||
( model, Cmd.none )
|
||||
|
||||
|
||||
|
||||
-- VIEW
|
||||
|
||||
|
||||
table : List DataValue -> E.Element Msg
|
||||
table data =
|
||||
let
|
||||
headerAttrs =
|
||||
[ F.bold
|
||||
, F.size 12
|
||||
, F.color darkModeFontColor
|
||||
, E.padding 5
|
||||
, Border.widthEach { bottom = 1, top = 0, left = 0, right = 0 }
|
||||
]
|
||||
in
|
||||
E.el [ E.padding 100, E.centerX ]
|
||||
(E.table [ E.width E.fill ]
|
||||
{ data = data
|
||||
, columns =
|
||||
[ { header = E.el headerAttrs (E.text " ")
|
||||
, width = E.fillPortion 2
|
||||
, view = \v -> E.el [ E.padding 5 ] (E.el [ E.width E.fill, E.padding 10, Border.rounded 5, Border.width 1 ] (E.text v.name))
|
||||
}
|
||||
, { header = E.el (headerAttrs ++ [ F.alignRight ]) (E.text "Count")
|
||||
, width = E.fillPortion 1
|
||||
, view =
|
||||
\v ->
|
||||
E.el
|
||||
[ E.centerY
|
||||
, F.size 12
|
||||
, F.color darkModeFontColor
|
||||
, F.alignRight
|
||||
, E.padding 5
|
||||
]
|
||||
(E.text (String.fromFloat v.value))
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
histogram : List DataValue -> E.Element Msg
|
||||
histogram data =
|
||||
E.el
|
||||
[ E.width E.fill
|
||||
, E.height (E.fill |> E.minimum 400)
|
||||
, E.centerX
|
||||
, E.alignTop
|
||||
, E.padding 30
|
||||
]
|
||||
(E.html
|
||||
(C.chart
|
||||
[ CA.height 300, CA.width (toFloat width) ]
|
||||
[ C.bars
|
||||
[ CA.spacing 0.0 ]
|
||||
[ C.bar .value [ CA.color "#A112FF", CA.roundTop 0.2 ] ]
|
||||
data
|
||||
, C.binLabels .name [ CA.moveDown 25, CA.rotate 45, CA.alignRight ]
|
||||
, C.barLabels [ CA.moveDown 12, CA.color "white", CA.fontSize 12 ]
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
dataView : List DataValue -> E.Element Msg
|
||||
dataView data =
|
||||
E.row []
|
||||
[ E.el [ E.padding 10, E.alignLeft, E.width E.fill ]
|
||||
(E.column [] (List.map (\d -> E.text d.name) data))
|
||||
]
|
||||
|
||||
|
||||
inputRow : Model -> E.Element Msg
|
||||
inputRow model =
|
||||
E.el [ E.centerX, E.width E.fill ]
|
||||
(E.column [ E.width E.fill ]
|
||||
[ I.text [ Background.color darkModeTextInputColor ]
|
||||
{ onChange = OnQueryChanged
|
||||
, placeholder = Nothing
|
||||
, text = model.query
|
||||
, label = I.labelHidden ""
|
||||
}
|
||||
, E.row [ E.paddingXY 0 10 ]
|
||||
[ I.text [ E.width (E.fill |> E.maximum 65), F.center, Background.color darkModeTextInputColor ]
|
||||
{ onChange = OnDataPoints
|
||||
, placeholder = Nothing
|
||||
, text =
|
||||
case model.dataPoints of
|
||||
0 ->
|
||||
""
|
||||
|
||||
n ->
|
||||
String.fromInt n
|
||||
, label = I.labelHidden ""
|
||||
}
|
||||
, I.checkbox [ E.paddingXY 10 0 ]
|
||||
{ onChange = OnSortByCheckbox
|
||||
, icon = I.defaultCheckbox
|
||||
, checked = model.sortByCount
|
||||
, label = I.labelRight [] (E.text "sort by count")
|
||||
}
|
||||
, I.checkbox [ E.paddingXY 10 0 ]
|
||||
{ onChange = OnReverseCheckbox
|
||||
, icon = I.defaultCheckbox
|
||||
, checked = model.reverse
|
||||
, label = I.labelRight [] (E.text "reverse")
|
||||
}
|
||||
, I.checkbox [ E.paddingXY 10 0 ]
|
||||
{ onChange = OnExcludeStopWordsCheckbox
|
||||
, icon = I.defaultCheckbox
|
||||
, checked = model.excludeStopWords
|
||||
, label = I.labelRight [] (E.text "exclude stop words")
|
||||
}
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
type Tab
|
||||
= Chart
|
||||
| Table
|
||||
| Data
|
||||
|
||||
|
||||
color =
|
||||
{ skyBlue = E.rgb255 0x00 0xCB 0xEC
|
||||
, vividViolet = E.rgb255 0xA1 0x12 0xFF
|
||||
, vermillion = E.rgb255 0xFF 0x55 0x43
|
||||
}
|
||||
|
||||
|
||||
tab : Tab -> Tab -> E.Element Msg
|
||||
tab thisTab selectedTab =
|
||||
let
|
||||
isSelected =
|
||||
thisTab == selectedTab
|
||||
|
||||
padOffset =
|
||||
if isSelected then
|
||||
0
|
||||
|
||||
else
|
||||
2
|
||||
|
||||
borderWidths =
|
||||
if isSelected then
|
||||
{ left = 1, top = 1, right = 1, bottom = 0 }
|
||||
|
||||
else
|
||||
{ bottom = 1, top = 0, left = 0, right = 0 }
|
||||
|
||||
corners =
|
||||
if isSelected then
|
||||
{ topLeft = 6, topRight = 6, bottomLeft = 0, bottomRight = 0 }
|
||||
|
||||
else
|
||||
{ topLeft = 0, topRight = 0, bottomLeft = 0, bottomRight = 0 }
|
||||
|
||||
tabColor =
|
||||
case selectedTab of
|
||||
Chart ->
|
||||
color.vividViolet
|
||||
|
||||
Table ->
|
||||
color.vermillion
|
||||
|
||||
Data ->
|
||||
color.skyBlue
|
||||
|
||||
text =
|
||||
case thisTab of
|
||||
Chart ->
|
||||
"Chart"
|
||||
|
||||
Table ->
|
||||
"Table"
|
||||
|
||||
Data ->
|
||||
"Data"
|
||||
in
|
||||
E.el
|
||||
[ Border.widthEach borderWidths
|
||||
, Border.roundEach corners
|
||||
, Border.color tabColor
|
||||
, Element.Events.onClick (OnTabSelected thisTab)
|
||||
, E.htmlAttribute (Html.Attributes.style "cursor" "pointer")
|
||||
, E.width E.fill
|
||||
]
|
||||
(E.el
|
||||
[ E.centerX
|
||||
, E.width E.fill
|
||||
, E.centerY
|
||||
, E.paddingEach { left = 30, right = 30, top = 10 + padOffset, bottom = 10 - padOffset }
|
||||
]
|
||||
(E.text text)
|
||||
)
|
||||
|
||||
|
||||
outputRow : Tab -> E.Element Msg
|
||||
outputRow selectedTab =
|
||||
E.row [ E.centerX, E.width E.fill ]
|
||||
[ tab Chart selectedTab
|
||||
, tab Table selectedTab
|
||||
, tab Data selectedTab
|
||||
]
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
E.layout
|
||||
[ E.width E.fill
|
||||
, F.family [ F.typeface "Fira Code" ]
|
||||
, F.size 12
|
||||
, F.color darkModeFontColor
|
||||
, Background.color darkModeBackgroundColor
|
||||
]
|
||||
(E.row [ E.centerX, E.width (E.fill |> E.maximum width) ]
|
||||
[ E.column [ E.centerX, E.width (E.fill |> E.maximum width), E.paddingXY 20 20 ]
|
||||
[ inputRow model
|
||||
, outputRow model.selectedTab
|
||||
, let
|
||||
data =
|
||||
Dict.toList model.resultsMap
|
||||
|> List.map Tuple.second
|
||||
|> filterData model
|
||||
in
|
||||
case model.selectedTab of
|
||||
Chart ->
|
||||
histogram data
|
||||
|
||||
Table ->
|
||||
table data
|
||||
|
||||
Data ->
|
||||
dataView data
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
|
||||
-- DATA LOGIC
|
||||
|
||||
|
||||
parseResults : List Result -> List String
|
||||
parseResults l =
|
||||
List.filterMap
|
||||
(\r ->
|
||||
case r of
|
||||
Output v ->
|
||||
String.split "\n" v.value
|
||||
|> List.filter (not << String.isEmpty)
|
||||
|> Just
|
||||
|
||||
ReplaceInPlace v ->
|
||||
Just [ v.value ]
|
||||
)
|
||||
l
|
||||
|> List.concat
|
||||
|
||||
|
||||
updateResultsMap : String -> Dict String DataValue -> Dict String DataValue
|
||||
updateResultsMap textResult =
|
||||
Dict.update
|
||||
textResult
|
||||
(\v ->
|
||||
case v of
|
||||
Nothing ->
|
||||
Just { name = textResult, value = 1 }
|
||||
|
||||
Just existing ->
|
||||
Just { existing | value = existing.value + 1 }
|
||||
)
|
||||
|
||||
|
||||
filterData : Filter a -> List DataValue -> List DataValue
|
||||
filterData { dataPoints, sortByCount, reverse, excludeStopWords } data =
|
||||
let
|
||||
pipeSort =
|
||||
if sortByCount then
|
||||
List.sortWith
|
||||
(\l r ->
|
||||
if l.value < r.value then
|
||||
GT
|
||||
|
||||
else if l.value > r.value then
|
||||
LT
|
||||
|
||||
else
|
||||
EQ
|
||||
)
|
||||
|
||||
else
|
||||
identity
|
||||
in
|
||||
let
|
||||
pipeReverse =
|
||||
if reverse then
|
||||
List.reverse
|
||||
|
||||
else
|
||||
identity
|
||||
in
|
||||
let
|
||||
pipeStopWords =
|
||||
if excludeStopWords then
|
||||
List.filter (\{ name } -> not (Dict.member (String.toLower name) Dict.empty))
|
||||
|
||||
else
|
||||
identity
|
||||
in
|
||||
data
|
||||
|> pipeStopWords
|
||||
|> pipeSort
|
||||
|> pipeReverse
|
||||
|> List.take dataPoints
|
||||
|> pipeReverse
|
||||
|
||||
|
||||
|
||||
-- STREAMING RESULT TYPES
|
||||
|
||||
|
||||
type Result
|
||||
= Output TextResult
|
||||
| ReplaceInPlace TextResult
|
||||
|
||||
|
||||
type alias TextResult =
|
||||
{ value : String
|
||||
, repository : Maybe String
|
||||
, commit : Maybe String
|
||||
, path : Maybe String
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- DECODERS
|
||||
|
||||
|
||||
resultDecoder : Decoder Result
|
||||
resultDecoder =
|
||||
field "kind" Decode.string
|
||||
|> Decode.andThen
|
||||
(\t ->
|
||||
case t of
|
||||
"replace-in-place" ->
|
||||
textResultDecoder
|
||||
|> Decode.map ReplaceInPlace
|
||||
|
||||
"output" ->
|
||||
textResultDecoder
|
||||
|> Decode.map Output
|
||||
|
||||
_ ->
|
||||
fail ("Unrecognized type " ++ t)
|
||||
)
|
||||
|
||||
|
||||
textResultDecoder : Decoder TextResult
|
||||
textResultDecoder =
|
||||
Decode.succeed TextResult
|
||||
|> Json.Decode.Pipeline.required "value" Decode.string
|
||||
|> Json.Decode.Pipeline.optional "repository" (maybe Decode.string) Nothing
|
||||
|> Json.Decode.Pipeline.optional "commit" (maybe Decode.string) Nothing
|
||||
|> Json.Decode.Pipeline.optional "path" (maybe Decode.string) Nothing
|
||||
|
||||
|
||||
|
||||
-- STYLING
|
||||
|
||||
|
||||
darkModeBackgroundColor : E.Color
|
||||
darkModeBackgroundColor =
|
||||
E.rgb255 0x18 0x1B 0x26
|
||||
|
||||
|
||||
darkModeFontColor : E.Color
|
||||
darkModeFontColor =
|
||||
E.rgb255 0xFF 0xFF 0xFF
|
||||
|
||||
|
||||
darkModeTextInputColor : E.Color
|
||||
darkModeTextInputColor =
|
||||
E.rgb255 0x1D 0x22 0x2F
|
||||
|
||||
|
||||
|
||||
-- DEBUG DATA
|
||||
|
||||
|
||||
exampleResultsMap : Dict String DataValue
|
||||
exampleResultsMap =
|
||||
[ { name = "Errorf"
|
||||
, value = 10.0
|
||||
}
|
||||
, { name = "Func\nmulti\nline"
|
||||
, value = 5.0
|
||||
}
|
||||
, { name = "Qux"
|
||||
, value = 2.0
|
||||
}
|
||||
]
|
||||
|> List.map (\d -> ( d.name, d ))
|
||||
|> Dict.fromList
|
||||
5
client/web/src/notebooks/blocks/compute/elm.d.ts
vendored
Normal file
5
client/web/src/notebooks/blocks/compute/elm.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
declare module '*.elm' {
|
||||
export const Elm: any
|
||||
}
|
||||
|
||||
declare module 'react-elm-components'
|
||||
@ -231,6 +231,17 @@ const config = {
|
||||
},
|
||||
{ test: /\.ya?ml$/, type: 'asset/source' },
|
||||
{ test: /\.(png|woff2)$/, type: 'asset/resource' },
|
||||
{
|
||||
test: /\.elm$/,
|
||||
exclude: /elm-stuff/,
|
||||
use: {
|
||||
loader: 'elm-webpack-loader',
|
||||
options: {
|
||||
cwd: path.resolve(ROOT_PATH, 'client/web/src/notebooks/blocks/compute/component'),
|
||||
report: 'json',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@ -242,6 +242,8 @@
|
||||
"cross-env": "^7.0.2",
|
||||
"css-loader": "^5.2.6",
|
||||
"css-minimizer-webpack-plugin": "^3.0.2",
|
||||
"elm": "^0.19.1-3",
|
||||
"elm-webpack-loader": "^8.0.0",
|
||||
"esbuild": "^0.14.2",
|
||||
"eslint": "^7.17.0",
|
||||
"eslint-formatter-lsif": "^1.0.3",
|
||||
@ -404,6 +406,7 @@
|
||||
"react-circular-progressbar": "^2.0.3",
|
||||
"react-dom": "^16.14.0",
|
||||
"react-dom-confetti": "^0.1.4",
|
||||
"react-elm-components": "^1.1.0",
|
||||
"react-focus-lock": "^2.4.1",
|
||||
"react-grid-layout": "1.3.0",
|
||||
"react-router": "^5.2.0",
|
||||
|
||||
87
yarn.lock
87
yarn.lock
@ -9088,6 +9088,14 @@ create-error-class@^3.0.0:
|
||||
dependencies:
|
||||
capture-stack-trace "^1.0.0"
|
||||
|
||||
create-react-class@^15.5.3:
|
||||
version "15.7.0"
|
||||
resolved "https://registry.npmjs.org/create-react-class/-/create-react-class-15.7.0.tgz#7499d7ca2e69bb51d13faf59bd04f0c65a1d6c1e"
|
||||
integrity sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==
|
||||
dependencies:
|
||||
loose-envify "^1.3.1"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
create-react-context@0.3.0, create-react-context@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.npmjs.org/create-react-context/-/create-react-context-0.3.0.tgz#546dede9dc422def0d3fc2fe03afe0bc0f4f7d8c"
|
||||
@ -9122,6 +9130,17 @@ cross-fetch@^3.0.4, cross-fetch@^3.0.6, cross-fetch@^3.1.4:
|
||||
dependencies:
|
||||
node-fetch "2.6.1"
|
||||
|
||||
cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5:
|
||||
version "6.0.5"
|
||||
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
|
||||
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
|
||||
dependencies:
|
||||
nice-try "^1.0.4"
|
||||
path-key "^2.0.1"
|
||||
semver "^5.5.0"
|
||||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||
@ -9140,17 +9159,6 @@ cross-spawn@^5.0.1:
|
||||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
cross-spawn@^6.0.0, cross-spawn@^6.0.5:
|
||||
version "6.0.5"
|
||||
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
|
||||
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
|
||||
dependencies:
|
||||
nice-try "^1.0.4"
|
||||
path-key "^2.0.1"
|
||||
semver "^5.5.0"
|
||||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
crypt@0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
|
||||
@ -10826,6 +10834,21 @@ element-resize-detector@^1.2.2:
|
||||
dependencies:
|
||||
batch-processor "1.0.0"
|
||||
|
||||
elm-webpack-loader@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.npmjs.org/elm-webpack-loader/-/elm-webpack-loader-8.0.0.tgz#73a6be9315fceff27fdc62d46c89520bdffad20a"
|
||||
integrity sha512-R49j9GOGbZ+PjrktAzYzjUgf6w+RHOIUNWLfOVdc7OoGG52SreEk63l/bLJV31HwLE8PU6KANEnzeZ3238fAUg==
|
||||
dependencies:
|
||||
loader-utils "^2.0.0"
|
||||
node-elm-compiler "^5.0.0"
|
||||
|
||||
elm@^0.19.1-3:
|
||||
version "0.19.1-5"
|
||||
resolved "https://registry.npmjs.org/elm/-/elm-0.19.1-5.tgz#61f18437222972e20f316f9b2d2c76a781a9991b"
|
||||
integrity sha512-dyBoPvFiNLvxOStQJdyq28gZEjS/enZXdZ5yyCtNtDEMbFJJVQq4pYNRKvhrKKdlxNot6d96iQe1uczoqO5yvA==
|
||||
dependencies:
|
||||
request "^2.88.0"
|
||||
|
||||
emittery@^0.8.1:
|
||||
version "0.8.1"
|
||||
resolved "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860"
|
||||
@ -12217,6 +12240,14 @@ find-cache-dir@^3.2.0, find-cache-dir@^3.3.1:
|
||||
make-dir "^3.0.2"
|
||||
pkg-dir "^4.1.0"
|
||||
|
||||
find-elm-dependencies@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.npmjs.org/find-elm-dependencies/-/find-elm-dependencies-2.0.4.tgz#0a327fc8c0c0297b54115efbf0a9d6de474cfc89"
|
||||
integrity sha512-x/4w4fVmlD2X4PD9oQ+yh9EyaQef6OtEULdMGBTuWx0Nkppvo2Z/bAiQioW2n+GdRYKypME2b9OmYTw5tw5qDg==
|
||||
dependencies:
|
||||
firstline "^1.2.0"
|
||||
lodash "^4.17.19"
|
||||
|
||||
find-root@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
|
||||
@ -12313,6 +12344,11 @@ first-chunk-stream@3.0.0, first-chunk-stream@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-3.0.0.tgz#06972a66263505ed82b2c4db93c1b5e078a6576a"
|
||||
integrity sha512-LNRvR4hr/S8cXXkIY5pTgVP7L3tq6LlYWcg9nWBuW7o1NMxKZo6oOVa/6GIekMGI0Iw7uC+HWimMe9u/VAeKqw==
|
||||
|
||||
firstline@^1.2.0:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.npmjs.org/firstline/-/firstline-1.3.1.tgz#59e84af0fd858fbc6dac0a0ff97fd22a47e58084"
|
||||
integrity sha512-ycwgqtoxujz1dm0kjkBFOPQMESxB9uKc/PlD951dQDIG+tBXRpYZC2UmJb0gDxopQ1ZX6oyRQN3goRczYu7Deg==
|
||||
|
||||
flagged-respawn@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.0.tgz#4e79ae9b2eb38bf86b3bb56bf3e0a56aa5fcabd7"
|
||||
@ -17567,6 +17603,16 @@ node-dir@^0.1.10:
|
||||
dependencies:
|
||||
minimatch "^3.0.2"
|
||||
|
||||
node-elm-compiler@^5.0.0:
|
||||
version "5.0.6"
|
||||
resolved "https://registry.npmjs.org/node-elm-compiler/-/node-elm-compiler-5.0.6.tgz#d4a6e6c9d9a26dba4211ccd2aeae7d5e34057f0c"
|
||||
integrity sha512-DWTRQR8b54rvschcZRREdsz7K84lnS8A6YJu8du3QLQ8f204SJbyTaA6NzYYbfUG97OTRKRv/0KZl82cTfpLhA==
|
||||
dependencies:
|
||||
cross-spawn "6.0.5"
|
||||
find-elm-dependencies "^2.0.4"
|
||||
lodash "^4.17.19"
|
||||
temp "^0.9.0"
|
||||
|
||||
node-fetch@2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||
@ -19824,6 +19870,13 @@ react-draggable@^4.0.0, react-draggable@^4.0.3, react-draggable@^4.4.3:
|
||||
classnames "^2.2.5"
|
||||
prop-types "^15.6.0"
|
||||
|
||||
react-elm-components@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/react-elm-components/-/react-elm-components-1.1.0.tgz#e36dbd4d80feee9b32c009482542d43b35be0b36"
|
||||
integrity sha512-L1JUZTfobgHGhNtrWI5MtSHEs2eUux4pupZj47rF6r+BIQ7ec8Gu2mGenl1SgOGvUChyF3kIQn/ti1+d/4X24w==
|
||||
dependencies:
|
||||
create-react-class "^15.5.3"
|
||||
|
||||
react-error-overlay@^6.0.9:
|
||||
version "6.0.9"
|
||||
resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
|
||||
@ -20676,7 +20729,7 @@ replaceall@^0.1.6:
|
||||
resolved "https://registry.npmjs.org/replaceall/-/replaceall-0.1.6.tgz#81d81ac7aeb72d7f5c4942adf2697a3220688d8e"
|
||||
integrity sha1-gdgax663LX9cSUKt8ml6MiBojY4=
|
||||
|
||||
request@2.88.2, "request@>=2.76.0 <3.0.0", request@^2.83.0, request@^2.88.2, request@~2.88.0:
|
||||
request@2.88.2, "request@>=2.76.0 <3.0.0", request@^2.83.0, request@^2.88.0, request@^2.88.2, request@~2.88.0:
|
||||
version "2.88.2"
|
||||
resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
|
||||
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
|
||||
@ -20891,7 +20944,7 @@ right-pad@^1.0.1:
|
||||
resolved "https://registry.npmjs.org/right-pad/-/right-pad-1.0.1.tgz#8ca08c2cbb5b55e74dafa96bf7fd1a27d568c8d0"
|
||||
integrity sha1-jKCMLLtbVedNr6lr9/0aJ9VoyNA=
|
||||
|
||||
rimraf@2.6.3, rimraf@^2.2.8, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3:
|
||||
rimraf@2.6.3, rimraf@^2.2.8, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@~2.6.2:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
|
||||
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
|
||||
@ -22549,6 +22602,14 @@ telejson@^5.3.2:
|
||||
lodash "^4.17.21"
|
||||
memoizerific "^1.11.3"
|
||||
|
||||
temp@^0.9.0:
|
||||
version "0.9.4"
|
||||
resolved "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz#cd20a8580cb63635d0e4e9d4bd989d44286e7620"
|
||||
integrity sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==
|
||||
dependencies:
|
||||
mkdirp "^0.5.1"
|
||||
rimraf "~2.6.2"
|
||||
|
||||
term-size@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user