= { emptyArray() },
+): FC = FC { props ->
+ require(useServerPaging xor (props.getPageCount == null)) {
+ "Either use client-side paging or provide a function to get page count"
+ }
+ val (data, setData) = useState>(emptyArray())
+ val (pageCount, setPageCount) = useState(1)
+ val (pageIndex, setPageIndex) = useState(0)
+ val (isModalOpen, setIsModalOpen) = useState(false)
+ val (dataAccessException, setDataAccessException) = useState(null)
+ val scope = CoroutineScope(Dispatchers.Default)
+
+ val (sorting, setSorting) = useState(emptyArray())
+ val tableInstance: Table = useReactTable(options = jso> {
+ this.columns = useMemo { columns(props) }
+ this.data = data
+ this.getCoreRowModel = getCoreRowModel()
+ this.manualPagination = useServerPaging
+ if (useServerPaging) {
+ this.pageCount = pageCount
+ }
+ this.initialState = jso {
+ this.pagination = jso {
+ this.pageSize = initialPageSize
+ this.pageIndex = pageIndex
+ }
+ this.sorting = sorting
+ }
+ this.asDynamic().state = jso {
+ // Apparently, setting `initialState` is not enough and examples from tanstack-react-table use `state` in `TableOptions`.
+ // It's not present in kotlin-wrappers v.423 though.
+ this.sorting = sorting
+ }
+ this.onSortingChange = { updater ->
+ setSorting.invoke(updater)
+ }
+ this.getSortedRowModel = getSortedRowModel()
+ this.getPaginationRowModel = tanstack.table.core.getPaginationRowModel()
+ additionalOptions()
+ }.also { tableOptionsCustomizer(it) })
+
+ // list of entities, updates of which will cause update of the data retrieving effect
+ val dependencies: Array = if (useServerPaging) {
+ arrayOf(tableInstance.getState().pagination.pageIndex, tableInstance.getState().pagination.pageSize, pageCount)
+ } else {
+ // when all data is already available, we don't need to repeat `getData` calls
+ emptyArray()
+ } + getAdditionalDependencies(props)
+
+ useEffect(*dependencies) {
+ if (useServerPaging) {
+ scope.launch {
+ val newPageCount = props.getPageCount!!.invoke(tableInstance.getState().pagination.pageSize)
+ if (newPageCount != pageCount) {
+ setPageCount(newPageCount)
+ }
+ }
+ }
+ }
+
+ val context = useRequestStatusContext()
+
+ useEffect(*dependencies) {
+ scope.launch {
+ try {
+ setData(context.(props.getData)(
+ tableInstance.getState().pagination.pageIndex, tableInstance.getState().pagination.pageSize
+ ))
+ } catch (e: CancellationException) {
+ // this means, that view is re-rendering while network request was still in progress
+ // no need to display an error message in this case
+ } catch (e: HttpStatusException) {
+ // this is a normal situation which should be handled by responseHandler in `getData` itself.
+ // no need to display an error message in this case
+ } catch (e: Exception) {
+ // other exceptions are not handled by `responseHandler` and should be displayed separately
+ setIsModalOpen(true)
+ setDataAccessException(e)
+ }
+ }
+ cleanup {
+ if (scope.isActive) {
+ scope.cancel()
+ }
+ }
+ }
+
+ val navigate = useNavigate()
+
+ val commonHeader = useMemo {
+ Fragment.create {
+ props.commonHeaderBuilder?.invoke(
+ this,
+ tableInstance,
+ navigate
+ )
+ }
+ }
+
+ div {
+ className = ClassName("${if (isTransparentGrid) "" else "card shadow"} mb-4")
+ if (props.tableHeader != undefined) {
+ div {
+ className = ClassName("card-header py-3")
+ h6 {
+ className = ClassName("m-0 font-weight-bold text-primary text-center")
+ +props.tableHeader
+ }
+ }
+ }
+ div {
+ className = ClassName("${props.cardBodyClassName} card-body")
+ div {
+ className = ClassName("table-responsive")
+ // we sometimes have strange overflow with some monitor resolution in chrome
+ style = jso {
+ overflowX = Overflow.hidden
+ }
+ table {
+ className = ClassName("table ${if (isTransparentGrid) "" else "table-bordered"} mb-0")
+ width = 100.0
+ cellSpacing = "0"
+ thead {
+ +commonHeader
+ tableInstance.getHeaderGroups().map { headerGroup ->
+ tr {
+ id = headerGroup.id
+ headerGroup.headers.map { header: Header ->
+ val column = header.column
+ th {
+ className = ClassName("m-0 font-weight-bold text-center text-nowrap")
+ +renderHeader(header)
+ if (column.getCanSort()) {
+ style = style ?: jso()
+ style?.cursor = "pointer".unsafeCast()
+ span {
+ +when (column.getIsSorted()) {
+ SortDirection.asc -> " 🔽"
+ SortDirection.desc -> " 🔼"
+ else -> ""
+ }
+ }
+ onClick = column.getToggleSortingHandler()
+ }
+ }
+ }
+ }
+ }
+ }
+ tbody {
+ tableInstance.getRowModel().rows.map { row ->
+ tr {
+ spread(getRowProps(row))
+ row.getVisibleCells().map { cell ->
+ +renderCell(cell)
+ }
+ }
+ if (row.isExpanded) {
+ requireNotNull(renderExpandedRow) {
+ "`useExpanded` is used, but no method for expanded row is provided"
+ }
+ renderExpandedRow.invoke(this@tbody, tableInstance, row)
+ }
+ }
+ }
+ }
+
+ if (data.isEmpty()) {
+ div {
+ className = ClassName("col mt-4 mb-4")
+ div {
+ className = ClassName("row justify-content-center")
+ h6 {
+ className = ClassName("m-0 mt-3 font-weight-bold text-primary text-center")
+ +"Nothing was found"
+ }
+ }
+ div {
+ className = ClassName("row justify-content-center")
+ img {
+ src = "/img/sad_cat.png"
+ @Suppress("MAGIC_NUMBER")
+ style = jso {
+ width = 14.rem
+ }
+ }
+ }
+ }
+ }
+
+ if (tableInstance.getPageCount() > 1) {
+ div {
+ className = ClassName("wrapper container m-0 p-0 mt-2")
+ pagingControl(tableInstance, setPageIndex, pageIndex, pageCount, initialPageSize)
+
+ div {
+ className = ClassName("row ml-1")
+ +"Page "
+ em {
+ className = ClassName("ml-1")
+ +" ${tableInstance.getState().pagination.pageIndex + 1} of ${tableInstance.getPageCount()}"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ displayModal(
+ isModalOpen,
+ "Error",
+ "Error when fetching data: ${dataAccessException?.message}",
+ mediumTransparentModalStyle,
+ { setIsModalOpen(false) },
+ ) {
+ buttonBuilder("Close", "secondary") {
+ setIsModalOpen(false)
+ }
+ }
+}
diff --git a/save-frontend-common/src/main/kotlin/com/saveourtool/save/frontend/common/components/tables/TableUtils.kt b/save-frontend-common/src/main/kotlin/com/saveourtool/save/frontend/common/components/tables/TableUtils.kt
new file mode 100644
index 0000000000..0f51469dc7
--- /dev/null
+++ b/save-frontend-common/src/main/kotlin/com/saveourtool/save/frontend/common/components/tables/TableUtils.kt
@@ -0,0 +1,58 @@
+@file:Suppress(
+ "HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE",
+ "CUSTOM_GETTERS_SETTERS",
+ "MISSING_KDOC_ON_FUNCTION",
+ "MISSING_KDOC_TOP_LEVEL",
+)
+
+package com.saveourtool.save.frontend.common.components.tables
+
+import js.core.jso
+import react.ChildrenBuilder
+import react.StateSetter
+import react.useState
+import tanstack.table.core.CellContext
+import tanstack.table.core.ExpandedState
+import tanstack.table.core.Row
+import tanstack.table.core.RowData
+import tanstack.table.core.Table
+import tanstack.table.core.TableOptions
+import tanstack.table.core.TableState
+import tanstack.table.core.Updater
+import tanstack.table.core.getExpandedRowModel
+
+val CellContext.value: TValue get() = this.getValue()
+
+val CellContext.pageIndex get() = this.table.getState()
+ .pagination
+ .pageIndex
+
+val CellContext.pageSize get() = this.table.getState()
+ .pagination
+ .pageSize
+
+val Row.isExpanded get() = getIsExpanded()
+
+val Table.canPreviousPage get() = getCanPreviousPage()
+
+val Table.canNextPage get() = getCanNextPage()
+
+fun Table.visibleColumnsCount() = this.getVisibleFlatColumns().size
+
+fun StateSetter.invoke(updaterOrValue: Updater) =
+ if (jsTypeOf(updaterOrValue) == "function") {
+ this.invoke(updaterOrValue.unsafeCast<(T) -> T>())
+ } else {
+ this.invoke(updaterOrValue.unsafeCast())
+ }
+
+fun ChildrenBuilder.enableExpanding(tableOptions: TableOptions) {
+ val (expanded, setExpanded) = useState(jso())
+ tableOptions.initialState!!.expanded = expanded
+ tableOptions.asDynamic()
+ .state
+ .unsafeCast()
+ .expanded = expanded
+ tableOptions.onExpandedChange = { setExpanded.invoke(it) }
+ tableOptions.getExpandedRowModel = getExpandedRowModel()
+}
diff --git a/save-frontend-common/src/main/kotlin/com/saveourtool/save/frontend/common/components/views/AboutUsView.kt b/save-frontend-common/src/main/kotlin/com/saveourtool/save/frontend/common/components/views/AboutUsView.kt
new file mode 100644
index 0000000000..a39eb30db6
--- /dev/null
+++ b/save-frontend-common/src/main/kotlin/com/saveourtool/save/frontend/common/components/views/AboutUsView.kt
@@ -0,0 +1,240 @@
+/**
+ * View with some info about core team
+ */
+
+package com.saveourtool.save.frontend.common.components.views
+
+import com.saveourtool.save.frontend.common.components.RequestStatusContext
+import com.saveourtool.save.frontend.common.components.basic.cardComponent
+import com.saveourtool.save.frontend.common.components.basic.markdown
+import com.saveourtool.save.frontend.common.components.requestStatusContext
+import com.saveourtool.save.frontend.common.externals.fontawesome.faGithub
+import com.saveourtool.save.frontend.common.externals.fontawesome.fontAwesomeIcon
+import com.saveourtool.save.frontend.common.utils.particles
+
+import js.core.jso
+import react.*
+import react.dom.html.ReactHTML.a
+import react.dom.html.ReactHTML.div
+import react.dom.html.ReactHTML.h2
+import react.dom.html.ReactHTML.h4
+import react.dom.html.ReactHTML.h5
+import react.dom.html.ReactHTML.h6
+import react.dom.html.ReactHTML.img
+import web.cssom.ClassName
+import web.cssom.Color
+import web.cssom.rem
+
+/**
+ * [Props] of [AboutUsView]
+ */
+external interface AboutUsViewProps : Props
+
+/**
+ * [State] of [AboutUsView]
+ */
+external interface AboutUsViewState : State
+
+/**
+ * A component representing "About us" page
+ */
+@JsExport
+@OptIn(ExperimentalJsExport::class)
+open class AboutUsView : AbstractView() {
+ private val developers = listOf(
+ Developer("Vlad", "Frolov", "Cheshiriks", "Fullstack"),
+ Developer("Peter", "Trifanov", "petertrr", "Arch"),
+ Developer("Andrey", "Shcheglov", "0x6675636b796f75676974687562", "Backend"),
+ Developer("Sasha", "Frolov", "sanyavertolet", "Fullstack"),
+ Developer("Andrey", "Kuleshov", "akuleshov7", "Ideas 😎"),
+ Developer("Nariman", "Abdullin", "nulls", "Fullstack"),
+ Developer("Alexey", "Votintsev", "Arrgentum", "Frontend"),
+ Developer("Kirill", "Gevorkyan", "kgevorkyan", "Backend"),
+ Developer("Dmitriy", "Morozovsky", "icemachined", "Sensei"),
+ ).sortedBy { it.name }
+
+ /**
+ * padding is removed for this card, because of the responsive images (avatars)
+ */
+ protected val devCard = cardComponent(hasBg = true, isPaddingBottomNull = true)
+
+ /**
+ * card with an info about SAVE with padding
+ */
+ protected val infoCard = cardComponent(hasBg = true, isPaddingBottomNull = true, isNoPadding = false)
+
+ override fun ChildrenBuilder.render() {
+ particles()
+ renderViewHeader()
+ renderSaveourtoolInfo()
+ renderDevelopers(NUMBER_OF_COLUMNS)
+ }
+
+ /**
+ * Simple title above the information card
+ */
+ protected fun ChildrenBuilder.renderViewHeader() {
+ h2 {
+ className = ClassName("text-center mt-3")
+ style = jso {
+ color = Color("#FFFFFF")
+ }
+ +"About us"
+ }
+ }
+
+ /**
+ * Info rendering
+ */
+ protected open fun ChildrenBuilder.renderSaveourtoolInfo() {
+ div {
+ div {
+ className = ClassName("mt-3 d-flex justify-content-center align-items-center")
+ div {
+ className = ClassName("col-6 p-0")
+ infoCard {
+ div {
+ className = ClassName("m-2 d-flex justify-content-around align-items-center")
+ div {
+ className = ClassName("m-2 d-flex align-items-center align-self-stretch flex-column")
+ img {
+ src = "/img/save-logo-no-bg.png"
+ @Suppress("MAGIC_NUMBER")
+ style = jso {
+ width = 8.rem
+ }
+ className = ClassName("img-fluid mt-auto mb-auto")
+ }
+ a {
+ className = ClassName("text-center mt-auto mb-2 align-self-end")
+ href = "mailto:$SAVEOURTOOL_EMAIL"
+ +SAVEOURTOOL_EMAIL
+ }
+ }
+ markdown(saveourtoolDescription, "flex-wrap")
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @param columns
+ */
+ @Suppress("MAGIC_NUMBER")
+ protected fun ChildrenBuilder.renderDevelopers(columns: Int) {
+ div {
+ h4 {
+ className = ClassName("text-center mb-1 mt-4 text-white")
+ +"Active contributors"
+ }
+ div {
+ className = ClassName("mt-3 d-flex justify-content-around align-items-center")
+ div {
+ className = ClassName("col-6 p-1")
+ val numberOfRows = developers.size / columns
+ for (rowIndex in 0..numberOfRows) {
+ div {
+ className = ClassName("row")
+ for (colIndex in 0 until columns) {
+ div {
+ className = ClassName("col-${12 / columns} p-2")
+ developers.getOrNull(columns * rowIndex + colIndex)?.let {
+ renderDeveloperCard(it)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @param developer
+ */
+ open fun ChildrenBuilder.renderDeveloperCard(developer: Developer) {
+ devCard {
+ div {
+ className = ClassName("p-3")
+ div {
+ className = ClassName("d-flex justify-content-center")
+ img {
+ src = "$GITHUB_AVATAR_LINK${developer.githubNickname}?size=$DEFAULT_AVATAR_SIZE"
+ className = ClassName("img-fluid border border-dark rounded-circle m-0")
+ @Suppress("MAGIC_NUMBER")
+ style = jso {
+ width = 10.rem
+ }
+ }
+ }
+ div {
+ className = ClassName("mt-2")
+ h5 {
+ className = ClassName("d-flex justify-content-center text-center")
+ +developer.name
+ }
+ h5 {
+ className = ClassName("d-flex justify-content-center text-center")
+ +developer.surname
+ }
+ h6 {
+ className = ClassName("text-center")
+ +developer.description
+ }
+ a {
+ style = jso {
+ fontSize = 2.rem
+ }
+ className = ClassName("d-flex justify-content-center")
+ href = "$GITHUB_LINK${developer.githubNickname}"
+ fontAwesomeIcon(faGithub)
+ }
+ }
+ }
+ }
+ }
+
+ companion object :
+ RStatics>(AboutUsView::class) {
+ protected const val DEFAULT_AVATAR_SIZE = "200"
+ protected const val GITHUB_AVATAR_LINK = "https://avatars.githubusercontent.com/"
+ protected const val GITHUB_LINK = "https://github.com/"
+ protected const val MAX_NICKNAME_LENGTH = 15
+ protected const val NUMBER_OF_COLUMNS = 3
+ protected const val SAVEOURTOOL_EMAIL = "saveourtool@gmail.com"
+ protected val saveourtoolDescription = """
+ # Save Our Tool!
+
+ Our community is mainly focused on Static Analysis tools and the eco-system related to such kind of tools.
+ We love Kotlin and mostly everything we develop is connected with Kotlin JVM, Kotlin JS or Kotlin Native.
+
+ ### Main Repositories:
+ - [diktat](${GITHUB_LINK}saveourtool/diktat) - Automated code checker&fixer for Kotlin
+ - [save-cli](${GITHUB_LINK}saveourtool/save-cli) - Unified test framework for Static Analyzers and Compilers
+ - [save-cloud](${GITHUB_LINK}saveourtool/save-cloud) - Cloud eco-system for CI/CD and benchmarking of Static Analyzers
+ - [awesome-benchmarks](${GITHUB_LINK}saveourtool/awesome-benchmarks) - Curated list of benchmarks for different types of testing
+
+ """.trimIndent()
+
+ init {
+ AboutUsView.contextType = requestStatusContext
+ }
+ }
+}
+
+/**
+ * @property name developer's name
+ * @property githubNickname nickname of developer on GitHub
+ * @property description brief developer description
+ * @property surname
+ */
+@JsExport
+data class Developer(
+ val name: String,
+ val surname: String,
+ val githubNickname: String,
+ val description: String = "",
+)
diff --git a/save-frontend-common/src/main/kotlin/com/saveourtool/save/frontend/common/components/views/AbstractView.kt b/save-frontend-common/src/main/kotlin/com/saveourtool/save/frontend/common/components/views/AbstractView.kt
new file mode 100644
index 0000000000..1b914f03c2
--- /dev/null
+++ b/save-frontend-common/src/main/kotlin/com/saveourtool/save/frontend/common/components/views/AbstractView.kt
@@ -0,0 +1,52 @@
+package com.saveourtool.save.frontend.common.components.views
+
+import com.saveourtool.save.frontend.common.utils.ComponentWithScope
+import com.saveourtool.save.frontend.common.utils.Style
+
+import react.*
+
+import kotlinx.browser.document
+
+/**
+ * Abstract view class that should be used in all functional views
+ */
+abstract class AbstractView(private val style: Style = Style.SAVE_DARK) : ComponentWithScope
() {
+ // A small hack to avoid duplication of main content-wrapper from App.kt
+ // We will have custom background only for sign-up and sign-in views
+ override fun componentDidMount() {
+ document.getElementById("main-body")?.apply {
+ className = when (style) {
+ Style.SAVE_DARK, Style.SAVE_LIGHT -> className.replace("vuln", "save")
+ Style.VULN_DARK, Style.VULN_LIGHT -> className.replace("save", "vuln")
+ Style.INDEX -> className.replace("vuln", "save")
+ }
+ }
+
+ document.getElementById("content-wrapper")?.setAttribute(
+ "style",
+ "background: ${style.globalBackground}"
+ )
+
+ configureTopBar(style)
+ }
+
+ private fun configureTopBar(style: Style) {
+ val topBar = document.getElementById("navigation-top-bar")
+ topBar?.setAttribute(
+ "class",
+ "navbar navbar-expand ${style.topBarBgColor} navbar-dark topbar ${style.marginBottomForTopBar} " +
+ "static-top shadow mr-1 ml-1 rounded"
+ )
+
+ topBar?.setAttribute(
+ "style",
+ "background: ${style.topBarTransparency}"
+ )
+
+ val container = document.getElementById("common-save-container")
+ container?.setAttribute(
+ "class",
+ "container-fluid ${style.borderForContainer}"
+ )
+ }
+}
diff --git a/save-frontend-common/src/main/kotlin/com/saveourtool/save/frontend/common/components/views/FallbackView.kt b/save-frontend-common/src/main/kotlin/com/saveourtool/save/frontend/common/components/views/FallbackView.kt
new file mode 100644
index 0000000000..029df85b93
--- /dev/null
+++ b/save-frontend-common/src/main/kotlin/com/saveourtool/save/frontend/common/components/views/FallbackView.kt
@@ -0,0 +1,113 @@
+/**
+ * Support rendering something as a fallback
+ */
+
+package com.saveourtool.save.frontend.common.components.views
+
+import com.saveourtool.save.frontend.common.utils.Style
+import com.saveourtool.save.frontend.common.utils.buttonBuilder
+
+import js.core.jso
+import react.*
+import react.dom.html.ReactHTML.a
+import react.dom.html.ReactHTML.div
+import react.dom.html.ReactHTML.img
+import react.dom.html.ReactHTML.p
+import react.router.Navigate
+import web.cssom.*
+
+import kotlinx.browser.document
+import kotlinx.browser.window
+
+/**
+ * Props of fallback component
+ */
+external interface FallbackViewProps : Props {
+ /**
+ * Text displayed in big letters
+ */
+ var bigText: String?
+
+ /**
+ * Small text for more vebose description
+ */
+ var smallText: String?
+
+ /**
+ * Whether link to the start page should be a `