From 376332fce7bda334d64813e4c1cf2aa8954d42b0 Mon Sep 17 00:00:00 2001 From: Jilles van Gurp Date: Wed, 13 Sep 2023 13:46:43 +0200 Subject: [PATCH] move info modal away from using friz2 modal and just use overlay directly so we can control z-level --- src/jsMain/kotlin/components/form.kt | 120 ++++++----- src/jsMain/kotlin/components/layout.kt | 7 + src/jsMain/kotlin/components/markdown.kt | 6 + src/jsMain/kotlin/components/modals.kt | 89 ++------ src/jsMain/kotlin/metrics/metrics.kt | 8 +- src/jsMain/kotlin/search/search.kt | 6 +- .../searchpluginconfig/elasticsearch.kt | 39 ++++ .../searchpluginconfig/httpgetplugin.kt | 25 ++- .../searchpluginconfig/httppostplugin.kt | 18 ++ .../searchpluginconfig/searchpluginconfig.kt | 10 +- .../kotlin/searchpluginconfig/shared.kt | 195 ++++++++++-------- src/jsMain/kotlin/testcases/testcases.kt | 8 +- 12 files changed, 299 insertions(+), 232 deletions(-) diff --git a/src/jsMain/kotlin/components/form.kt b/src/jsMain/kotlin/components/form.kt index bad9afb..05ef9c7 100644 --- a/src/jsMain/kotlin/components/form.kt +++ b/src/jsMain/kotlin/components/form.kt @@ -20,25 +20,29 @@ fun RenderContext.textField( scope: (ScopeContext.() -> Unit) = {}, initialize: InputField.() -> Unit ) { - inputField("flex flex-col border p-2 items-center w-full", id = id, scope = scope) { - initialize(this) - div("flex flex-row items-center w-full") { - label?.let { l -> - inputLabel("italic mr-5 w-2/6 text-right") { - +l + border { + inputField("flex flex-col items-center w-full", id = id, scope = scope) { + initialize(this) + div("flex flex-row items-center w-full") { + label?.let { l -> + inputLabel("italic mr-5 w-2/6 text-right") { + +l + } } - } - inputTextfield("""w-4/6 basis-160 bg-blueBright-100 border border-blueBright-300 text-blueBright-900 - |text-sm rounded-lg focus:ring-blueBright-500 focus:border-blueBright-500 p-2.5""".trimMargin()) { - placeHolder?.let { pl -> - placeholder(pl) + inputTextfield( + """w-4/6 basis-160 bg-blueBright-100 border border-blueBright-300 text-blueBright-900 + |text-sm rounded-lg focus:ring-blueBright-500 focus:border-blueBright-500 p-2.5""".trimMargin() + ) { + placeHolder?.let { pl -> + placeholder(pl) + } } } - } - description?.let { - inputDescription("block w-full") { - div { - +description + description?.let { + inputDescription("block w-full") { + div { + +description + } } } } @@ -53,28 +57,30 @@ fun RenderContext.textAreaField( scope: (ScopeContext.() -> Unit) = {}, initialize: TextArea.() -> Unit ) { - textArea("flex flex-col border p-2 items-center w-full", id = id, scope = scope) { - initialize(this) - div("flex flex-row items-center w-full") { - label?.let { l -> - textareaLabel("italic mr-5 w-2/6 text-right") { - +l + border { + textArea("flex flex-col items-center w-full", id = id, scope = scope) { + initialize(this) + div("flex flex-row items-center w-full") { + label?.let { l -> + textareaLabel("italic mr-5 w-2/6 text-right") { + +l + } } - } - textareaTextfield( - """w-4/6 basis-160 bg-blueBright-100 border border-blueBright-300 + textareaTextfield( + """w-4/6 basis-160 bg-blueBright-100 border border-blueBright-300 |text-blueBright-900 text-sm rounded-lg focus:ring-blueBright-500 |focus:border-blueBright-500 p-2""".trimMargin() - ) { - placeHolder?.let { pl -> - placeholder(pl) + ) { + placeHolder?.let { pl -> + placeholder(pl) + } } } - } - description?.let { - textareaDescription("block w-full") { - div { - +description + description?.let { + textareaDescription("block w-full") { + div { + +description + } } } } @@ -88,42 +94,44 @@ fun RenderContext.switchField( scope: (ScopeContext.() -> Unit) = {}, initialize: SwitchWithLabel.() -> Unit ) { - switchWithLabel("flex flex-col items-center w-full", id = id, scope = scope) { - initialize(this) + border { + switchWithLabel("flex flex-col place-items-center w-full", id = id, scope = scope) { + initialize(this) - div("flex flex-row items-center w-full") { - label?.let { - switchLabel("italic mr-5 w-2/6 text-right") { - +label + div("flex flex-row items-center w-full") { + label?.let { + switchLabel("italic mr-5 w-2/6 text-right") { + +label + } } - } - div("w-4/6") { - switchToggle( - """relative inline-flex flex-shrink-0 h-6 w-11 + div("w-4/6") { + switchToggle( + """relative inline-flex flex-shrink-0 h-6 w-11 | cursor-pointer rounded-full | border-2 border-transparent ring-1 ring-blueBright-400 | transition-colors ease-in-out duration-200 | focus:outline-none focus:ring-4 focus:ring-blueBright-600""".trimMargin() - ) { + ) { - span("sr-only") { +"Use setting" } - className(enabled.map { if (it) "bg-blueBright-800" else "bg-blueBright-200" }) - span( - """inline-block h-5 w-5 + span("sr-only") { +"Use setting" } + className(enabled.map { if (it) "bg-blueBright-800" else "bg-blueBright-200" }) + span( + """inline-block h-5 w-5 | rounded-full bg-white shadow pointer-events-none | ring-0 | transform transition ease-in-out duration-200""".trimMargin() - ) { - className(enabled.map { if (it) "translate-x-5" else "translate-x-0" }) - attr(Aria.hidden, "true") + ) { + className(enabled.map { if (it) "translate-x-5" else "translate-x-0" }) + attr(Aria.hidden, "true") + } } } } - } - description?.let { - switchDescription("block w-full") { - div { - +description + description?.let { + switchDescription("block w-full") { + div { + +description + } } } } diff --git a/src/jsMain/kotlin/components/layout.kt b/src/jsMain/kotlin/components/layout.kt index 647c1ef..d4bede6 100644 --- a/src/jsMain/kotlin/components/layout.kt +++ b/src/jsMain/kotlin/components/layout.kt @@ -11,6 +11,13 @@ fun RenderContext.row(content: HtmlTag.() -> Unit) { fun RenderContext.rowCemtered(content: HtmlTag.() -> Unit) { div("flex flex-row gap-2 align-middle place-items-center mx-auto w-fit", content = content) } +fun RenderContext.leftRightRow(content: HtmlTag.() -> Unit) { + div("flex flex-row w-full place-items-center justify-between", content = content) +} + +fun RenderContext.border(content: HtmlTag.() -> Unit) { + div("rounded-lg border-2 border-blueBright-100 my-2 p-2 w-full", content = content) +} fun RenderContext.centeredMainPanel(content: HtmlTag.() -> Unit) = div("""flex flex-col grow items-left space-y-1 w-5/6 m-auto diff --git a/src/jsMain/kotlin/components/markdown.kt b/src/jsMain/kotlin/components/markdown.kt index 6fbaa43..1e11c68 100644 --- a/src/jsMain/kotlin/components/markdown.kt +++ b/src/jsMain/kotlin/components/markdown.kt @@ -35,4 +35,10 @@ fun RenderContext.markdownFile(file:String, baseClass: String?=null) { // make sure we render lists with bullets, tailwind seems to not like this; so use css }.domNode.innerHTML = renderMarkdown(it) } +} + +fun RenderContext.markdownDiv(markdown:String, baseClass: String?=null) { + div(baseClass) { + // make sure we render lists with bullets, tailwind seems to not like this; so use css + }.domNode.innerHTML = renderMarkdown(markdown) } \ No newline at end of file diff --git a/src/jsMain/kotlin/components/modals.kt b/src/jsMain/kotlin/components/modals.kt index faad864..335d868 100644 --- a/src/jsMain/kotlin/components/modals.kt +++ b/src/jsMain/kotlin/components/modals.kt @@ -1,9 +1,6 @@ package components -import dev.fritz2.core.HtmlTag -import dev.fritz2.core.RenderContext -import dev.fritz2.core.storeOf -import dev.fritz2.core.transition +import dev.fritz2.core.* import dev.fritz2.headless.components.modal import dev.fritz2.headless.foundation.setInitialFocus import koin @@ -12,19 +9,19 @@ import org.w3c.dom.HTMLDivElement fun RenderContext.overlay( - baseClass: String? = "absolute top-48 left-1/2 z-50 bg-white min-h-48 w-96 p-5 flex flex-col justify-between over-flow-auto", + baseClass: String? = "absolute top-48 left-1/2 bg-white min-h-48 w-96 p-5 flex flex-col justify-between over-flow-auto", content: HtmlTag.() -> Unit ) { - div("absolute h-screen w-screen top-0 left-0 bg-gray-300 bg-opacity-90 z-40") { + div("absolute h-screen w-screen top-0 left-0 bg-gray-300 bg-opacity-90 z-20") { div(baseClass, content = content) } } fun RenderContext.overlayLarge( - baseClass: String? = "mx-auto z-50 bg-white h-screen w-5/6 p-5 flex flex-col overflow-y-auto", + baseClass: String? = "mx-auto bg-white h-screen w-5/6 p-5 flex flex-col overflow-y-auto", content: HtmlTag.() -> Unit ) { - div("absolute h-screen w-screen top-0 left-0 bg-gray-300 bg-opacity-90 z-40") { + div("absolute h-screen w-screen top-0 left-0 bg-gray-300 bg-opacity-90 z-20") { div(baseClass, content = content) } } @@ -39,7 +36,7 @@ suspend fun confirm( val openStateStore = storeOf(true) modal { openState(openStateStore) - modalPanel("w-full fixed z-10 inset-0 overflow-y-auto") { + modalPanel("w-full fixed inset-0 overflow-y-auto") { div("flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0") { modalOverlay("fixed inset-0 bg-blueMuted-300 bg-opacity-75 transition-opacity") { transition( @@ -105,68 +102,20 @@ suspend fun confirm( } -fun RenderContext.infoModal(title: String = "Title TODO", markdown: String, close: String = "Close") { - val openStateStore = storeOf(false) +fun RenderContext.infoPopup(title: String = "Title TODO", markdown: String) { + val infoPopoverOpenStore = storeOf(false) secondaryButton { - +"?" - clicks.map { true } handledBy openStateStore.update + +"???" + clicks.map { true } handledBy infoPopoverOpenStore.update } - modal { - openState(openStateStore) - modalPanel("w-full fixed z-10 inset-0 overflow-y-auto") { - div("flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0") { - modalOverlay("fixed inset-0 bg-blueMuted-300 bg-opacity-75 transition-opacity") { - transition( - "ease-out duration-300", - "opacity-0", - "opacity-100", - "ease-in duration-200", - "opacity-100", - "opacity-0" - ) - clicks.map { false } handledBy openStateStore.update - } - /* */ - span("hidden sm:inline-block sm:align-middle sm:h-screen") { - attr("aria-hidden", "true") - +" " - } - div( - """inline-block align-bottom sm:align-middle w-full sm:max-w-4xl px-6 py-5 sm:p-14 - | bg-white rounded-lg - | shadow-xl transform transition-all - | text-left overflow-hidden""".trimMargin() - ) { - transition( - "ease-out duration-300", - "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95", - "opacity-100 translate-y-0 sm:scale-100", - "ease-in duration-200", - "opacity-100 translate-y-0 sm:scale-100", - "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" - ) - div("mt-3 text-center sm:mt-0 sm:text-left") { - modalTitle("text-white bg-blueBright-700 p-2 items-center") { - paraCentered { - +title - setInitialFocus() - } - } - div("mt-2") { - - div { - - }.domNode.innerHTML = renderMarkdown(markdown) - div("flex flex-row place-items-center mx-auto w-fit overflow-y-auto") { - primaryButton { - +close - clicks handledBy { - openStateStore.update(false) - } - } - } - } - } + infoPopoverOpenStore.data.render {opened -> + if(opened) { + overlayLarge { + h1 { +title } + markdownDiv(markdown) + primaryButton { + +"Close" + clicks.map { false } handledBy infoPopoverOpenStore.update } } } @@ -177,7 +126,7 @@ fun busyPopupMountPoint() { val busyStore = koin.get() modal { openState(busyStore) - modalPanel("w-full fixed z-10 inset-0 overflow-y-auto") { + modalPanel("w-full fixed z-50 inset-0 overflow-y-auto") { div("flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0") { modalOverlay("fixed inset-0 bg-blueMuted-300 bg-opacity-75 transition-opacity") { transition( diff --git a/src/jsMain/kotlin/metrics/metrics.kt b/src/jsMain/kotlin/metrics/metrics.kt index 1be68b6..32dd6ff 100644 --- a/src/jsMain/kotlin/metrics/metrics.kt +++ b/src/jsMain/kotlin/metrics/metrics.kt @@ -88,7 +88,8 @@ fun RenderContext.metrics() { jsonFileImport(ListSerializer(MetricsOutput.serializer())) { decoded -> metricsOutputStore.update(decoded) } - infoModal("Exploring Metrics",""" + infoPopup( + "Exploring Metrics", """ The metrics screen is of course the whole point of this application. After you've configured your search plugin and created your test cases, you can run and explore metrics in this screen. @@ -121,7 +122,8 @@ fun RenderContext.metrics() { You can download results in json format and re-import it to explore the metrics later. Note, future versions of this tool may add the ability to compare metrics as well. - """.trimIndent()) + """.trimIndent() + ) } @@ -155,7 +157,7 @@ private fun RenderContext.metricResult( h2 { +metricConfiguration.name } - infoModal(metricConfiguration.metric.title, metricConfiguration.metric.explanation) + infoPopup(metricConfiguration.metric.title, metricConfiguration.metric.explanation) } div { +"Metric: ${+metricResult.metric}" } diff --git a/src/jsMain/kotlin/search/search.kt b/src/jsMain/kotlin/search/search.kt index d8ea1b8..89eff30 100644 --- a/src/jsMain/kotlin/search/search.kt +++ b/src/jsMain/kotlin/search/search.kt @@ -105,7 +105,8 @@ fun RenderContext.searchScreen() { stores.map { (f, s) -> f to s.current }.toMap() } handledBy activeSearchPluginConfigurationStore.search } - infoModal("The Search Tool",""" + infoPopup( + "The Search Tool", """ You can use the search tool to explore your search service and convert the searches you do into test cases. @@ -121,7 +122,8 @@ fun RenderContext.searchScreen() { that means you already have a test case with the same search context. If so, you can modify it in the test cases screen. The id of each test case is a content hash of the search context. - """.trimIndent()) + """.trimIndent() + ) } searchResults() diff --git a/src/jsMain/kotlin/searchpluginconfig/elasticsearch.kt b/src/jsMain/kotlin/searchpluginconfig/elasticsearch.kt index 60ca413..d6c51da 100644 --- a/src/jsMain/kotlin/searchpluginconfig/elasticsearch.kt +++ b/src/jsMain/kotlin/searchpluginconfig/elasticsearch.kt @@ -128,7 +128,46 @@ fun RenderContext.elasticsearchEditor( settingsGenerator = settingsGenerator, editConfigurationStore = editConfigurationStore, queryTemplateStore = queryTemplateStore, + "Configuring thw Elasticsearch Plugin", + """ + This plugin allows you to query Elasticsearch or Opensearch. + + ## CORS headers for ELasticsearch + + Important: **make sure your elasticsearch server is configured to send cors headers**. Without + this your browser will not allow this application to send requests to Elasticsearch. + + If you use docker-compose, you can add these settings: + + ``` + http.cors.enabled: "true" + http.cors.allow-origin: |- + "*" + http.cors.allow-methods: "OPTIONS, HEAD, GET, POST, PUT, DELETE" + http.cors.allow-headers: "X-Requested-With, X-Auth-Token, Content-Type, Content-Length, Authorization, Access-Control-Allow-Headers, Accept" + + ``` + ## Search context & query + + You can configure a templated string to use as the query. Just prototype your query in for example + the Kibana dev console and copy it over. + + Any variables, which you should surround with `{{ my_variable }}`, will be added to your + search context variables. + + You can further tweak those and configure default values. + + To extract information from the response you need to provide json paths to construct a label. + You can use multiple nested fields (relative to `_source`). + + ## Compatibility + + This plugin uses [kt-search](https://github.com/jillesvangurp/kt-search) and should work with + Elasticsearch 7 or newer as well as Opensearch 1 or newer. + + """.trimIndent() ) + } } diff --git a/src/jsMain/kotlin/searchpluginconfig/httpgetplugin.kt b/src/jsMain/kotlin/searchpluginconfig/httpgetplugin.kt index a8ae811..8b3c339 100644 --- a/src/jsMain/kotlin/searchpluginconfig/httpgetplugin.kt +++ b/src/jsMain/kotlin/searchpluginconfig/httpgetplugin.kt @@ -37,8 +37,10 @@ fun RenderContext.httpGetPluginEditor( textField("https://mydomain.com/mysearch", "url", "Url to your API") { value(urlStore) } - + h2 { +"Request Parameters" } + para { +"These will be used for your search context." } mapEditor(searchContextParamsStore) + h2 { +"Headers" } mapEditor(headersStore) textField( @@ -75,7 +77,26 @@ fun RenderContext.httpGetPluginEditor( metricConfigurationsStore = metricConfigurationsStore, settingsGenerator = settingsGenerator, editConfigurationStore = editConfigurationStore, - queryTemplateStore = null + queryTemplateStore = null, + helpTitle = "Configuring a REST GET API", + helpText = """ + This plugin is intended for APIs that return a Json object with a list of hits + in response to an HTTP GET. You can configure the URL + and specify headers. + + You can configure a templated string to use as the payload. Any variables, + which you should surround with `{{ my_variable }}`, will be added to your + search context variables. + + You can further tweak those and configure default values. + + To extract information from the response you need to provide json paths: + + - The path to the list with hits. + - The relative path to the id field. For example `documentId` + - A relative path to field that may be used as a label. For example `author.name`. + """.trimIndent() + ) } } diff --git a/src/jsMain/kotlin/searchpluginconfig/httppostplugin.kt b/src/jsMain/kotlin/searchpluginconfig/httppostplugin.kt index 0f58d44..9e2ac11 100644 --- a/src/jsMain/kotlin/searchpluginconfig/httppostplugin.kt +++ b/src/jsMain/kotlin/searchpluginconfig/httppostplugin.kt @@ -92,6 +92,24 @@ fun RenderContext.httpPostPluginEditor( settingsGenerator = settingsGenerator, editConfigurationStore = editConfigurationStore, queryTemplateStore = bodyTemplateStore, + helpTitle = "Configuring a REST POST API", + helpText = """ + This plugin is intended for APIs that return a Json object with a list of hits + in response to an HTTP POST with a request payload. You can configure the URL + and specify headers. + + You can configure a templated string to use as the payload. Any variables, + which you should surround with `{{ my_variable }}`, will be added to your + search context variables. + + You can further tweak those and configure default values. + + To extract information from the response you need to provide json paths: + + - The path to the list with hits. + - The relative path to the id field. For example `documentId` + - A relative path to field that may be used as a label. For example `author.name`. + """.trimIndent() ) } } diff --git a/src/jsMain/kotlin/searchpluginconfig/searchpluginconfig.kt b/src/jsMain/kotlin/searchpluginconfig/searchpluginconfig.kt index 15893a3..5dfc907 100644 --- a/src/jsMain/kotlin/searchpluginconfig/searchpluginconfig.kt +++ b/src/jsMain/kotlin/searchpluginconfig/searchpluginconfig.kt @@ -10,10 +10,8 @@ import examples.quotesearch.movieQuotesSearchPluginConfig import koin import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map -import kotlinx.serialization.builtins.serializer import kotlinx.serialization.encodeToString import org.koin.core.module.dsl.singleOf -import org.koin.core.qualifier.StringQualifier import org.koin.core.qualifier.named import org.koin.dsl.module import org.w3c.dom.HTMLDivElement @@ -63,7 +61,7 @@ fun RenderContext.pluginConfiguration() { ) } val showMetricsEditor = storeOf(false) - div("flex flex-row w-full place-items-center justify-between") { + leftRightRow { div { +pluginConfig.name } @@ -99,7 +97,7 @@ fun RenderContext.pluginConfiguration() { } } div("w-full place-items-end mt-10") { - div("flex flex-row w-full place-items-center justify-between") { + leftRightRow { // switchField("Show Demo Plugins") { // value(showDemoContentStore) // } @@ -167,7 +165,7 @@ fun RenderContext.pluginConfiguration() { } private fun HtmlTag.help() { - infoModal( + infoPopup( title = "Configuration Screen", markdown = """ The configuration screen is where you can configure your search plugins and metrics. @@ -249,7 +247,7 @@ fun RenderContext.createOrEditPlugin(editConfigurationStore: Store>, settingsGenerator: () -> JsonObject, editConfigurationStore: Store, - queryTemplateStore: Store? + queryTemplateStore: Store?, + helpTitle:String, + helpText: String, ) { val pluginConfigurationStore = koin.get() val searchContextFieldsStore = SearchContextFieldsStore(existing?.fieldConfig.orEmpty()) @@ -54,6 +56,8 @@ fun RenderContext.pluginEditorButtonsAndSearchContextEditor( editConfigurationStore.update(null) } } + infoPopup(helpTitle,helpText) +// infoModal(helpTitle,helpText) } } @@ -100,110 +104,125 @@ fun RenderContext.templateVarEditor( is SearchContextField.StringField -> storeOf(field.placeHolder) } - row { - textField("", "name") { - value(nameStore) - } - defaultValueStore.data.render { defaultValue -> - when (field) { - is SearchContextField.BoolField -> { - val boolStore = storeOf(defaultValue.toBoolean()) - boolStore.data handledBy { - defaultValueStore.update(it.toString()) - } - switchField { - value(boolStore) - } - } - else -> { - textField("", "Default Value") { - value(defaultValueStore) - } - textField("", "PlaceHolder") { - value(placeHolderStore) + border { + row { + textField("", "name") { + value(nameStore) + } + defaultValueStore.data.render { defaultValue -> + when (field) { + is SearchContextField.BoolField -> { + val boolStore = storeOf(defaultValue.toBoolean()) + boolStore.data handledBy { + defaultValueStore.update(it.toString()) + } + switchField { + value(boolStore) + } } + else -> { + textField("", "Default Value") { + value(defaultValueStore) + } + textField("", "PlaceHolder") { + value(placeHolderStore) + } + + } } } - } - primaryButton { - +"Change" - clicks.map { - when (typeStore.current) { - SearchContextField.BoolField::class.simpleName!! -> { - SearchContextField.BoolField( - name = nameStore.current, defaultValue = defaultValueStore.current.toBoolean() - ) - } + primaryButton { + +"Change" + clicks.map { + when (typeStore.current) { + SearchContextField.BoolField::class.simpleName!! -> { + SearchContextField.BoolField( + name = nameStore.current, defaultValue = defaultValueStore.current.toBoolean() + ) + } - SearchContextField.IntField::class.simpleName!! -> { - SearchContextField.IntField( - name = nameStore.current, - defaultValue = defaultValueStore.current.toIntOrNull() ?: 0, - placeHolder = placeHolderStore.current, - ) + SearchContextField.IntField::class.simpleName!! -> { + SearchContextField.IntField( + name = nameStore.current, + defaultValue = defaultValueStore.current.toIntOrNull() ?: 0, + placeHolder = placeHolderStore.current, + ) - } + } - else -> { - SearchContextField.StringField( - name = nameStore.current, - defaultValue = defaultValueStore.current, - placeHolder = placeHolderStore.current, - ) + else -> { + SearchContextField.StringField( + name = nameStore.current, + defaultValue = defaultValueStore.current, + placeHolder = placeHolderStore.current, + ) + } } + } handledBy { updatedField -> + searchContextFieldsStore.update(searchContextFieldsStore.current.map { + if (it.name == updatedField.name) { + updatedField + } else { + it + } + }) } - } handledBy { updatedField -> - searchContextFieldsStore.update(searchContextFieldsStore.current.map { - if (it.name == updatedField.name) { - updatedField - } else { - it - } - }) } - } - secondaryButton(iconSource = SvgIconSource.Cross) { - clicks handledBy { - searchContextFieldsStore.update(searchContextFieldsStore.current.filter { it.name != field.name }) + secondaryButton(iconSource = SvgIconSource.Cross) { + clicks handledBy { + searchContextFieldsStore.update(searchContextFieldsStore.current.filter { it.name != field.name }) + } } } - } - row { - typeStore.data.render { fieldType -> - div { - primaryButton { - +"int" - disabled(fieldType == SearchContextField.IntField::class.simpleName!!) - clicks.map { SearchContextField.IntField::class.simpleName!! } handledBy typeStore.update - } - primaryButton { - +"bool" - disabled(fieldType == SearchContextField.BoolField::class.simpleName!!) - clicks.map { SearchContextField.BoolField::class.simpleName!! } handledBy typeStore.update - } - primaryButton { - +"string" - disabled(fieldType == SearchContextField.StringField::class.simpleName!!) - clicks.map { SearchContextField.StringField::class.simpleName!! } handledBy typeStore.update + div("w-full") { + row { + typeStore.data.render { fieldType -> + leftRightRow { + para { + +"Set Field Type:" + } + row { + primaryButton { + +"int" + disabled(fieldType == SearchContextField.IntField::class.simpleName!!) + clicks.map { SearchContextField.IntField::class.simpleName!! } handledBy typeStore.update + } + primaryButton { + +"bool" + disabled(fieldType == SearchContextField.BoolField::class.simpleName!!) + clicks.map { SearchContextField.BoolField::class.simpleName!! } handledBy typeStore.update + } + primaryButton { + +"string" + disabled(fieldType == SearchContextField.StringField::class.simpleName!!) + clicks.map { SearchContextField.StringField::class.simpleName!! } handledBy typeStore.update + } + } + } } } } } } - row { - val fieldNameStore = storeOf("") - textField("", "field") { - value(fieldNameStore) + leftRightRow { + para { + +"Add more search context fields" } - fieldNameStore.data.render { fn -> - primaryButton(iconSource = SvgIconSource.Plus) { - disabled(fn.isBlank() || fn in fields.map { it.name }) - clicks handledBy { - console.log("adding $fn") - searchContextFieldsStore.update(fields + SearchContextField.StringField(fn, "", "")) + row { + val fieldNameStore = storeOf("") + textField("", "field") { + value(fieldNameStore) + } + fieldNameStore.data.render { fn -> + primaryButton(iconSource = SvgIconSource.Plus) { + disabled(fn.isBlank() || fn in fields.map { it.name }) + clicks handledBy { + console.log("adding $fn") + searchContextFieldsStore.update(fields + SearchContextField.StringField(fn, "", "")) + } } } } @@ -213,7 +232,7 @@ fun RenderContext.templateVarEditor( } fun RenderContext.mapEditor(store: Store>) { - div("border-2 border-blueBright-300") { + border { val keyStore = storeOf("") val valueStore = storeOf("") store.data.render { headers -> @@ -233,10 +252,10 @@ fun RenderContext.mapEditor(store: Store>) { } } row { - textField("", "Header Name") { + textField("", "Name") { value(keyStore) } - textField("", "Header Name") { + textField("", "Value") { value(valueStore) } keyStore.data.render { key -> diff --git a/src/jsMain/kotlin/testcases/testcases.kt b/src/jsMain/kotlin/testcases/testcases.kt index 9a7cf54..1a17399 100644 --- a/src/jsMain/kotlin/testcases/testcases.kt +++ b/src/jsMain/kotlin/testcases/testcases.kt @@ -11,9 +11,7 @@ import dev.fritz2.core.disabled import dev.fritz2.core.storeOf import dev.fritz2.headless.components.toast import dev.fritz2.remote.http -import examples.quotesearch.MovieQuote import koin -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.datetime.Clock import kotlinx.serialization.builtins.ListSerializer @@ -141,14 +139,14 @@ fun RenderContext.testCases() { .let> { body -> DEFAULT_JSON.decodeFromString(body) }.let { testCases -> - ratedSearchesStore.update(testCases) - } + ratedSearchesStore.update(testCases) + } } } } } } - infoModal( + infoPopup( "Creating Test Cases", """ This screen allows you to review and modify your test cases. When you create a test case from the search screen, the results simply get rated in descending order. You can use this