未验证 提交 8fa21f67 编写于 作者: M Méril 提交者: GitHub

feat(Console): migrate grid/chart wrapper to React + add row count (#386)

上级 78dd24ca
......@@ -12750,6 +12750,11 @@
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
"dev": true
},
"use-media": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-media/-/use-media-1.4.0.tgz",
"integrity": "sha512-XsgyUAf3nhzZmEfhc5MqLHwyaPjs78bgytpVJ/xDl0TF4Bptf3vEpBNBBT/EIKOmsOc8UbuECq3mrP3mt1QANA=="
},
"util": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
......
......@@ -50,7 +50,8 @@
"rxjs": "6.5.5",
"slim-select": "1.26.0",
"styled-components": "5.1.1",
"styled-icons": "10.2.1"
"styled-icons": "10.2.1",
"use-media": "1.4.0"
},
"devDependencies": {
"@babel/cli": "7.10.1",
......
import { darken } from "polished"
import React, { forwardRef, MouseEvent, ReactNode, Ref } from "react"
import { MouseEvent, ReactNode } from "react"
import styled, { css } from "styled-components"
import type { Color } from "types"
......@@ -10,18 +10,22 @@ import { bezierTransition } from "../Transition"
type Size = "sm" | "md"
type Type = "button" | "submit"
const defaultProps: Readonly<{
const size: Size = "md"
const type: Type = "button"
const defaultProps = {
size,
type,
}
export type ButtonProps = Readonly<{
children?: ReactNode
className?: string
disabled?: boolean
onClick?: (event: MouseEvent) => void
size: Size
type: Type
}> = {
size: "md",
type: "button",
}
export type ButtonProps = Partial<typeof defaultProps>
size?: Size
type?: Type
}>
type ThemeShape = {
background: Color
......@@ -82,7 +86,7 @@ const getTheme = (
}
`
const Primary = styled.button<ButtonProps>`
export const PrimaryButton = styled.button<ButtonProps>`
${baseCss};
${getTheme(
{
......@@ -103,16 +107,9 @@ const Primary = styled.button<ButtonProps>`
)};
`
const PrimaryButtonWithRef = (
props: ButtonProps,
ref: Ref<HTMLButtonElement>,
) => <Primary {...props} ref={ref} />
export const PrimaryButton = forwardRef(PrimaryButtonWithRef)
PrimaryButton.defaultProps = defaultProps
const Secondary = styled.button<ButtonProps>`
export const SecondaryButton = styled.button<ButtonProps>`
${baseCss};
${getTheme(
{
......@@ -133,16 +130,9 @@ const Secondary = styled.button<ButtonProps>`
)};
`
const SecondaryButtonWithRef = (
props: ButtonProps,
ref: Ref<HTMLButtonElement>,
) => <Secondary {...props} ref={ref} />
export const SecondaryButton = forwardRef(SecondaryButtonWithRef)
SecondaryButton.defaultProps = defaultProps
const Success = styled.button<ButtonProps>`
export const SuccessButton = styled.button<ButtonProps>`
${baseCss};
${getTheme(
{
......@@ -163,16 +153,9 @@ const Success = styled.button<ButtonProps>`
)};
`
const SuccessButtonWithRef = (
props: ButtonProps,
ref: Ref<HTMLButtonElement>,
) => <Success {...props} ref={ref} />
export const SuccessButton = forwardRef(SuccessButtonWithRef)
SuccessButton.defaultProps = defaultProps
const Error = styled.button<ButtonProps>`
export const ErrorButton = styled.button<ButtonProps>`
${baseCss};
${getTheme(
{
......@@ -193,11 +176,4 @@ const Error = styled.button<ButtonProps>`
)};
`
const ErrorButtonWithRef = (
props: ButtonProps,
ref: Ref<HTMLButtonElement>,
) => <Error {...props} ref={ref} />
export const ErrorButton = forwardRef(ErrorButtonWithRef)
ErrorButton.defaultProps = defaultProps
export * from "./useKeyPress"
export * from "./useMediaQuery"
export * from "./usePopperStyles"
export * from "./useTransition"
import React, { createContext, ReactNode, useContext, useMemo } from "react"
import useMedia from "use-media"
type Props = Readonly<{
children: ReactNode
}>
const mediaQueries = {
smScreen: "(max-width: 767px)",
}
const MediaQueryContext = createContext({
smScreen: false,
})
export const MediaQueryProvider = ({ children }: Props) => {
const smScreen = useMedia(mediaQueries.smScreen)
const value = useMemo(() => ({ smScreen }), [smScreen])
return (
<MediaQueryContext.Provider value={value}>
{children}
</MediaQueryContext.Provider>
)
}
export const useMediaQuery = () => useContext(MediaQueryContext)
......@@ -11,7 +11,7 @@ type Props = Readonly<{
export const PaneMenu = styled.div`
position: relative;
display: flex;
height: 41px;
height: 4rem;
padding: 0 1rem;
align-items: center;
background: ${color("draculaBackgroundDarker")};
......
import React, {
MouseEvent as ReactMouseEvent,
TouchEvent as ReactTouchEvent,
useCallback,
useEffect,
useRef,
useState,
} from "react"
import styled from "styled-components"
import styled, { createGlobalStyle, css } from "styled-components"
import { DragIndicator } from "@styled-icons/material/DragIndicator"
import { color } from "utils"
const PreventUserSelectionHorizontal = createGlobalStyle`
html {
user-select: none;
cursor: ew-resize !important;
pointer-events: none;
}
`
const PreventUserSelectionVertical = createGlobalStyle`
html {
user-select: none;
cursor: row-resize !important;
pointer-events: none;
}
`
type Props = Readonly<{
direction: "vertical" | "horizontal"
max?: number
min?: number
onChange: (x: number) => void
onChange: (value: number) => void
}>
const DragIcon = styled(DragIndicator)`
const HorizontalDragIcon = styled(DragIndicator)`
position: absolute;
color: ${color("gray1")};
`
const Wrapper = styled.div`
const VerticalDragIcon = styled(HorizontalDragIcon)`
transform: rotate(90deg);
`
const wrapperStyles = css`
display: flex;
width: 8px;
height: 100%;
align-items: center;
justify-content: center;
border: 1px solid rgba(0, 0, 0, 0.1);
background: ${color("draculaBackgroundDarker")};
border: 1px solid rgba(255, 255, 255, 0.03);
border-top: none;
border-bottom: none;
color: ${color("gray1")};
&:hover {
background: ${color("draculaSelection")};
cursor: ew-resize;
color: ${color("draculaForeground")};
}
`
const Ghost = styled(Wrapper)`
const HorizontalWrapper = styled.div`
${wrapperStyles};
width: 1rem;
height: 100%;
border-top: none;
border-bottom: none;
cursor: ew-resize;
`
const VerticalWrapper = styled.div`
${wrapperStyles};
width: 100%;
height: 1rem;
border-left: none;
border-right: none;
cursor: row-resize;
`
const ghostStyles = css`
position: absolute;
width: 8px;
top: 0;
bottom: 0;
z-index: 20;
background: ${color("draculaPurple")};
&:hover {
background: ${color("draculaPurple")};
cursor: ew-resize;
}
`
type Position = Readonly<{
x: number
}>
const HorizontalGhost = styled.div`
${ghostStyles};
width: 1rem;
top: 0;
bottom: 0;
`
const VerticalGhost = styled.div`
${ghostStyles};
height: 1rem;
left: 0;
right: 0;
`
export const Splitter = ({ max, min, onChange }: Props) => {
export const Splitter = ({ direction, max, min, onChange }: Props) => {
const [offset, setOffset] = useState(0)
const [originalPosition, setOriginalPosition] = useState(0)
const [ghostPosition, setGhostPosition] = useState(0)
const [pressed, setPressed] = useState(false)
const [left, setLeft] = useState<number>(0)
const [xOffset, setXOffset] = useState<number>(0)
const splitter = useRef<HTMLDivElement | null>(null)
const [position, setPosition] = useState<Position>({ x: 0 })
const handleMouseMove = useCallback(
(event: MouseEvent) => {
(event: TouchEvent | MouseEvent) => {
event.stopPropagation()
event.preventDefault()
const clientPosition = direction === "horizontal" ? "clientX" : "clientY"
const side = direction === "horizontal" ? "outerWidth" : "outerHeight"
let position = 0
if (event instanceof TouchEvent) {
position = event.touches[0][clientPosition]
}
if (event instanceof MouseEvent) {
position = event[clientPosition]
}
if (
(min &&
max &&
event.clientX > min &&
event.clientX < window.outerWidth - max) ||
(!min && max && event.clientX < window.outerWidth - max) ||
(!max && min && event.clientX > min) ||
(min && max && position > min && position < window[side] - max) ||
(!min && max && position < window[side] - max) ||
(!max && min && position > min) ||
(!min && !max)
) {
setPosition({
x: event.clientX,
})
setGhostPosition(position)
}
},
[max, min],
[direction, max, min],
)
const handleMouseUp = useCallback(() => {
document.removeEventListener("mouseup", handleMouseUp)
document.removeEventListener("mousemove", handleMouseMove)
document.removeEventListener("touchend", handleMouseUp)
document.removeEventListener("touchmove", handleMouseMove)
setPressed(false)
}, [handleMouseMove])
const handleMouseDown = useCallback(
(event: ReactMouseEvent<HTMLDivElement>) => {
(event: ReactTouchEvent | ReactMouseEvent) => {
if (splitter.current && splitter.current.parentElement) {
const { x } = splitter.current.parentElement.getBoundingClientRect()
setLeft(event.clientX - x)
setXOffset(x)
const clientPosition =
direction === "horizontal" ? "clientX" : "clientY"
const coordinate = direction === "horizontal" ? "x" : "y"
const offset = splitter.current.parentElement.getBoundingClientRect()[
coordinate
]
let position = 0
if (event.nativeEvent instanceof TouchEvent) {
position = event.nativeEvent.touches[0][clientPosition]
}
if (event.nativeEvent instanceof MouseEvent) {
position = event.nativeEvent[clientPosition]
}
setOriginalPosition(position)
setOffset(offset)
setPressed(true)
document.addEventListener("mouseup", handleMouseUp)
document.addEventListener("mousemove", handleMouseMove)
document.addEventListener("mousemove", handleMouseMove, {
passive: true,
})
document.addEventListener("touchend", handleMouseUp)
document.addEventListener("touchmove", handleMouseMove, {
passive: true,
})
}
},
[handleMouseMove, handleMouseUp],
[direction, handleMouseMove, handleMouseUp],
)
useEffect(() => {
if (!pressed && position.x) {
onChange(position.x - left - xOffset)
setLeft(0)
setPosition({ x: 0 })
if (!pressed && ghostPosition) {
onChange(ghostPosition - originalPosition)
setOriginalPosition(0)
setGhostPosition(0)
}
}, [onChange, position, pressed, left, xOffset])
}, [onChange, ghostPosition, pressed, originalPosition])
if (direction === "horizontal") {
return (
<>
<HorizontalWrapper
onMouseDown={handleMouseDown}
onTouchStart={handleMouseDown}
ref={splitter}
>
<HorizontalDragIcon size="16px" />
</HorizontalWrapper>
{ghostPosition > 0 && (
<>
<HorizontalGhost
style={{
left: `${ghostPosition - offset}px`,
}}
/>
<PreventUserSelectionHorizontal />
</>
)}
</>
)
}
return (
<>
<Wrapper onMouseDown={handleMouseDown} ref={splitter}>
<DragIcon size="12px" />
</Wrapper>
{position.x > 0 && (
<Ghost
style={{
left: `${position.x - xOffset}px`,
}}
/>
<VerticalWrapper
onMouseDown={handleMouseDown}
onTouchStart={handleMouseDown}
ref={splitter}
>
<VerticalDragIcon size="16px" />
</VerticalWrapper>
{ghostPosition > 0 && (
<>
<VerticalGhost
style={{
top: `${ghostPosition - offset}px`,
}}
/>
<PreventUserSelectionVertical />
</>
)}
</>
)
......
import { darken } from "polished"
import React, { forwardRef, Ref } from "react"
import styled, { css } from "styled-components"
import type { Color } from "types"
......@@ -8,17 +7,21 @@ import { color } from "utils"
import { ButtonProps, getButtonSize, PrimaryButton } from "../Button"
import { bezierTransition } from "../Transition"
const defaultProps: ButtonProps &
Readonly<{
direction: "top" | "right" | "bottom" | "left"
selected: boolean
}> = {
type Direction = "top" | "right" | "bottom" | "left"
const direction: Direction = "bottom"
const defaultProps = {
...PrimaryButton.defaultProps,
direction: "bottom",
direction,
selected: false,
}
type Props = typeof defaultProps
type Props = Readonly<{
direction?: Direction
selected?: boolean
}> &
ButtonProps
type ThemeShape = {
background: Color
......@@ -30,13 +33,20 @@ const baseCss = css<Props>`
height: ${getButtonSize};
padding: 0 1rem;
align-items: center;
justify-content: center;
background: transparent;
border: none;
font-weight: 400;
outline: 0;
line-height: 1.15;
opacity: ${({ selected }) => (selected ? "1" : "0.5")};
border: none;
${({ direction }) =>
`border-${direction || defaultProps.direction}: 2px solid transparent;`};
${bezierTransition};
svg + span {
margin-left: 1rem;
}
`
const getTheme = (normal: ThemeShape, hover: ThemeShape) =>
......@@ -44,7 +54,10 @@ const getTheme = (normal: ThemeShape, hover: ThemeShape) =>
background: ${color(normal.background)};
color: ${color(normal.color)};
${({ direction, selected, theme }) =>
selected && `border-${direction}: 2px solid ${theme.color.draculaPink};`};
selected &&
`border-${direction || defaultProps.direction}-color: ${
theme.color.draculaPink
};`};
&:focus {
box-shadow: inset 0 0 0 1px ${color("draculaForeground")};
......@@ -61,7 +74,7 @@ const getTheme = (normal: ThemeShape, hover: ThemeShape) =>
}
`
const Primary = styled.button<Props>`
export const PrimaryToggleButton = styled.button<Props>`
${baseCss};
${getTheme(
{
......@@ -75,11 +88,4 @@ const Primary = styled.button<Props>`
)};
`
const PrimaryToggleButtonWithRef = (
props: Props,
ref: Ref<HTMLButtonElement>,
) => <Primary {...props} ref={ref} />
export const PrimaryToggleButton = forwardRef(PrimaryToggleButtonWithRef)
PrimaryToggleButton.defaultProps = defaultProps
......@@ -18,7 +18,7 @@
<div id="root"></div>
<div id="page-wrapper" class="gray-bg">
{{> partials/console}}
<div class="js-sql-panel" id="console"></div>
{{> partials/import}}
<div id="footer"></div>
</div>
......
......@@ -8,6 +8,7 @@ import { applyMiddleware, compose, createStore } from "redux"
import { createEpicMiddleware } from "redux-observable"
import { ThemeProvider } from "styled-components"
import { MediaQueryProvider } from "components"
import { actions, rootEpic, rootReducer } from "store"
import { StoreAction, StoreShape } from "types"
......@@ -28,7 +29,9 @@ store.dispatch(actions.console.bootstrap())
ReactDOM.render(
<Provider store={store}>
<ThemeProvider theme={theme}>
<Layout />
<MediaQueryProvider>
<Layout />
</MediaQueryProvider>
</ThemeProvider>
</Provider>,
document.getElementById("root"),
......
import Clipboard from "clipboard"
import $ from "jquery"
import * as qdb from "./globals"
const divSqlPanel = $(".js-sql-panel")
const divExportUrl = $(".js-export-url")
const win = $(window)
const grid = $("#grid")
const quickVis = $("#quick-vis")
const toggleChartBtn = $("#js-toggle-chart")
const toggleGridBtn = $("#js-toggle-grid")
let topHeight = 350
function resize() {
$("#editor").css("flex-basis", topHeight)
}
function loadSplitterPosition() {
if (typeof Storage !== "undefined") {
const n = localStorage.getItem("splitter.position")
if (n) {
topHeight = parseInt(n)
if (!topHeight) {
topHeight = 350
}
}
}
}
function saveSplitterPosition() {
if (typeof Storage !== "undefined") {
localStorage.setItem("splitter.position", topHeight)
}
}
function toggleVisibility(x, name) {
if (name === "console") {
visible = true
divSqlPanel.show()
} else {
visible = false
divSqlPanel.hide()
}
}
function toggleChart() {
toggleChartBtn.addClass("active")
toggleGridBtn.removeClass("active")
grid.css("display", "none")
quickVis.css("display", "flex")
}
function toggleGrid() {
toggleChartBtn.removeClass("active")
toggleGridBtn.addClass("active")
grid.css("display", "flex")
quickVis.css("display", "none")
}
export function setupConsoleController(bus) {
win.bind("resize", resize)
bus.on(qdb.MSG_QUERY_DATASET, function (e, m) {
divExportUrl.val(qdb.toExportUrl(m.query))
})
divExportUrl.click(function () {
this.select()
})
/* eslint-disable no-new */
new Clipboard(".js-export-copy-url")
$(".js-query-refresh").click(function () {
$(".js-query-refresh .fa").addClass("fa-spin")
bus.trigger("grid.refresh")
})
// named splitter
bus.on("splitter.console.resize", function (x, e) {
topHeight += e
win.trigger("resize")
bus.trigger("preferences.save")
})
bus.on("preferences.save", saveSplitterPosition)
bus.on("preferences.load", loadSplitterPosition)
bus.on(qdb.MSG_ACTIVE_PANEL, toggleVisibility)
const grid = $("#grid")
const quickVis = $("#quick-vis")
grid.grid(bus)
quickVis.quickVis(bus)
$("#console-splitter").splitter(bus, "console", 200, 200)
// wire query publish
toggleChartBtn.click(toggleChart)
toggleGridBtn.click(toggleGrid)
bus.on(qdb.MSG_QUERY_DATASET, toggleGrid)
}
......@@ -2,59 +2,39 @@ import $ from "jquery"
import * as qdb from "./globals"
const divImportPanel = $(".js-import-panel")
const importTopPanel = $("#import-top")
const importDetail = $("#import-detail")
const importMenu = $("#import-menu")[0]
const footer = $("#footer")[0]
const canvasPanel = importTopPanel.find(".ud-canvas")
const w = $(window)
let visible = false
let upperHalfHeight = 450
let upperHalfHeight = 515
function resize() {
if (visible) {
const h = w[0].innerHeight
const footerHeight = footer.offsetHeight
qdb.setHeight(importTopPanel, upperHalfHeight)
qdb.setHeight(
importDetail,
h - footerHeight - upperHalfHeight - importMenu.offsetHeight - 10,
)
let r1 = importTopPanel[0].getBoundingClientRect()
let r2 = canvasPanel[0].getBoundingClientRect()
// qdb.setHeight(importTopPanel, upperHalfHeight);
qdb.setHeight(canvasPanel, upperHalfHeight - (r2.top - r1.top) - 10)
}
}
function toggleVisibility(x, name) {
if (name === "import") {
visible = true
divImportPanel.show()
w.trigger("resize")
} else {
visible = false
divImportPanel.hide()
}
const footer = $(".footer")
const importTopPanel = $("#import-top")
const canvasPanel = importTopPanel.find(".ud-canvas")
const importDetail = $("#import-detail")
const importMenu = $("#import-menu")[0]
const h = $(window)[0].innerHeight
const footerHeight = footer.offsetHeight
qdb.setHeight(importTopPanel, upperHalfHeight)
qdb.setHeight(
importDetail,
h - footerHeight - upperHalfHeight - importMenu.offsetHeight - 50,
)
let r1 = importTopPanel[0].getBoundingClientRect()
let r2 = canvasPanel[0].getBoundingClientRect()
}
function splitterResize(x, p) {
upperHalfHeight += p
w.trigger("resize")
$(window).trigger("resize")
}
export function setupImportController(bus) {
w.bind("resize", resize)
$(window).bind("resize", resize)
$("#dragTarget").dropbox(bus)
$("#import-file-list").importManager(bus)
$("#import-detail").importEditor(bus)
$("#import-splitter").splitter(bus, "import", 470, 300)
$("#import-splitter").splitter(bus, "import", 420, 250)
bus.on("splitter.import.resize", splitterResize)
bus.on(qdb.MSG_ACTIVE_PANEL, toggleVisibility)
}
......@@ -25,18 +25,13 @@ let messageBus = $({})
window.bus = messageBus
$(document).ready(function () {
setupConsoleController(messageBus)
setupImportController(messageBus)
function exportClick(e) {
e.preventDefault()
messageBus.trigger("grid.publish.query")
}
$(".js-query-export").click(exportClick)
messageBus.trigger("preferences.load")
const win = $(window)
win.trigger("resize")
})
messageBus.on("react.ready", () => {
setupConsoleController(messageBus)
setupImportController(messageBus)
})
<div class="js-sql-panel">
<div class="console-wrapper" id="editor"></div>
<div id="console-splitter" class="splitter">
<svg viewBox="0 0 24 24" height="12px" width="12px" aria-hidden="true" focusable="false" fill="currentColor" xmlns="http://www.w3.org/2000/svg" class="sc-AxjAm gXIMwt Splitter__DragIcon-kWuanA jGdWIe"><path fill="none" d="M0 0h24v24H0V0z"></path><path d="M11 18c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm-2-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"></path></svg>
</div>
<div class="result-wrapper">
<div class="menu-bar">
<button id="js-toggle-grid" class="active button-toggle">
<i class="fa fa-bars"></i>
<span>Grid</span>
</button>
<button id="js-toggle-chart" class="button-toggle">
<i class="fa fa-chart-pie"></i>
<span>Chart</span>
</button>
<div class="flex-separator"></div>
<button class="js-query-refresh button-primary">
<i class="fa fa-sync"></i>
</button>
<button class="js-query-export button-primary">
<i class="fa fa-download"></i>
<span>CSV</span>
</button>
</div>
<div id="grid">
<div class="qg-header-row"></div>
<div class="qg-viewport">
<div class="qg-canvas"></div>
</div>
</div>
<div id="quick-vis">
<div class="quick-vis-controls">
<form class="v-fit" role="form">
<div class="form-group">
<label>Chart type</label>
<select id="_qvis_frm_chart_type">
<option>bar</option>
<option>line</option>
<option>area</option>
</select>
</div>
<div class="form-group">
<label>Labels</label>
<select id="_qvis_frm_axis_x"></select>
</div>
<div class="form-group">
<label>Series</label>
<select id="_qvis_frm_axis_y" multiple></select>
</div>
<button class="button-primary js-chart-draw" id="_qvis_frm_draw">
<i class="fa fa-play"></i>
<span>Draw</span>
</button>
</form>
</div>
<div class="quick-vis-canvas"></div>
</div>
</div>
</div>
......@@ -96,6 +96,7 @@ const Ace = () => {
.then((result) => {
setRequest(undefined)
dispatch(actions.query.stopRunning())
dispatch(actions.query.setResult(result))
if (result.type === QuestDB.Type.DDL) {
dispatch(
......
......@@ -2,9 +2,9 @@ import docsearch from "docsearch.js"
import React, { useCallback, useEffect, useState } from "react"
import { useDispatch, useSelector } from "react-redux"
import styled from "styled-components"
import { ControllerPlay } from "@styled-icons/entypo/ControllerPlay"
import { ControllerStop } from "@styled-icons/entypo/ControllerStop"
import { Plus } from "@styled-icons/entypo/Plus"
import { Play } from "@styled-icons/remix-line/Play"
import { Stop } from "@styled-icons/remix-line/Stop"
import { Add } from "@styled-icons/remix-line/Add"
import {
ErrorButton,
......@@ -85,14 +85,14 @@ const Menu = () => {
<Wrapper>
{running && (
<ErrorButton onClick={handleClick}>
<ControllerStop size="18px" />
<Stop size="18px" />
<span>Cancel</span>
</ErrorButton>
)}
{!running && (
<SuccessButton onClick={handleClick}>
<ControllerPlay size="18px" />
<Play size="18px" />
<span>Run</span>
</SuccessButton>
)}
......@@ -104,7 +104,7 @@ const Menu = () => {
onToggle={handleToggle}
trigger={
<QueryPickerButton onClick={handleClick}>
<Plus size="18px" />
<Add size="18px" />
<span>Saved queries</span>
</QueryPickerButton>
}
......
......@@ -22,7 +22,7 @@ const Wrapper = styled.div`
padding: 0.6rem 0;
flex-direction: column;
background: ${color("draculaBackgroundDarker")};
box-shadow: rgb(0, 0, 0) 0px 5px 8px;
box-shadow: ${color("black")} 0px 5px 8px;
border: 1px solid ${color("black")};
border-radius: 4px;
`
......
......@@ -81,7 +81,7 @@ const QueryResult = ({ compiler, count, execute, fetch, rowCount }: Props) => {
<Wrapper _height={95} duration={TransitionDuration.FAST}>
<div>
<Text color="draculaForeground">
{rowCount} rows in&nbsp;
{rowCount} row{rowCount > 1 ? "s" : ""} in&nbsp;
{formatTiming(fetch)}&nbsp;
</Text>
(
......
......@@ -29,7 +29,7 @@ const Footer = () => (
rel="noreferrer"
target="_blank"
>
<Github size="22px" />
<Github size="18px" />
</Link>
</Icons>
</>
......
import React, { useCallback, useState } from "react"
import React, { useCallback, useEffect, useRef, useState } from "react"
import { createPortal } from "react-dom"
import styled from "styled-components"
import { Splitter } from "components"
import { BusEvent } from "utils"
import Editor from "../Editor"
import Footer from "../Footer"
import Notifications from "../Notifications"
import Result from "../Result"
import Schema from "../Schema"
import Sidebar from "../Sidebar"
const Top = styled.div<{
basis?: number
}>`
position: relative;
display: flex;
flex-grow: 0;
flex-shrink: 1;
${({ basis }) => basis && `flex-basis: ${basis}px`};
overflow: hidden;
`
const Layout = () => {
const editorNode = document.getElementById("editor")
const consoleNode = document.getElementById("console")
const footerNode = document.getElementById("footer")
const notificationsNode = document.getElementById("notifications")
const [schemaWidthOffset, setSchemaWidthOffset] = useState<number>()
const handleSplitterChange = useCallback((offset) => {
const [topHeightOffset, setTopHeightOffset] = useState<number>()
const topElement = useRef<HTMLDivElement | null>(null)
const handleSchemaSplitterChange = useCallback((offset) => {
setSchemaWidthOffset(offset)
}, [])
const handleResultSplitterChange = useCallback((change: number) => {
if (topElement.current) {
const offset = topElement.current.getBoundingClientRect().height + change
localStorage.setItem("splitter.position", `${offset}`)
setTopHeightOffset(offset)
setTimeout(() => {
window.bus.trigger(BusEvent.MSG_ACTIVE_PANEL)
}, 0)
}
}, [])
useEffect(() => {
const size = parseInt(localStorage.getItem("splitter.position") || "0", 10)
if (size) {
setTopHeightOffset(size)
} else {
setTopHeightOffset(350)
}
}, [])
useEffect(() => {
if (topElement.current) {
window.bus.trigger(BusEvent.REACT_READY)
}
}, [topElement])
return (
<>
<Sidebar />
{editorNode &&
{consoleNode &&
createPortal(
<>
<Schema widthOffset={schemaWidthOffset} />
<Splitter max={300} min={200} onChange={handleSplitterChange} />
<Editor />
<Top basis={topHeightOffset} ref={topElement}>
<Schema widthOffset={schemaWidthOffset} />
<Splitter
direction="horizontal"
max={300}
min={200}
onChange={handleSchemaSplitterChange}
/>
<Editor />
</Top>
<Splitter
direction="vertical"
max={300}
min={200}
onChange={handleResultSplitterChange}
/>
<Result />
</>,
editorNode,
consoleNode,
)}
{footerNode && createPortal(<Footer />, footerNode)}
{notificationsNode && createPortal(<Notifications />, notificationsNode)}
......
......@@ -138,12 +138,12 @@ const Notification = ({ createdAt, line1, title, type, ...rest }: Props) => {
/>
)}
<CloseIcon onClick={handleCloseClick} size="16px" />
<CloseIcon onClick={handleCloseClick} size="18px" />
{pinned ? (
<Unpin onClick={handlePinClick} size="14px" />
<Unpin onClick={handlePinClick} size="16px" />
) : (
<Pin onClick={handlePinClick} size="14px" />
<Pin onClick={handlePinClick} size="16px" />
)}
</Wrapper>
</CSSTransition>
......
......@@ -12,7 +12,7 @@ const NotificationsStyles = createGlobalStyle`
.notifications{
position: fixed;
top: 4rem;
right: 1.5rem;
right: 1rem;
display: flex;
flex-direction: column;
z-index: 10;
......
import React, { useCallback, useEffect, useState } from "react"
import { useSelector } from "react-redux"
import styled from "styled-components"
import { Download2 } from "@styled-icons/remix-line/Download2"
import { Grid } from "@styled-icons/remix-line/Grid"
import { PieChart } from "@styled-icons/remix-line/PieChart"
import { Refresh } from "@styled-icons/remix-line/Refresh"
import {
PaneContent,
PaneWrapper,
PaneMenu,
PopperHover,
PrimaryToggleButton,
SecondaryButton,
Text,
Tooltip,
} from "components"
import { selectors } from "store"
import { BusEvent, color } from "utils"
import * as QuestDB from "utils/questdb"
const Menu = styled(PaneMenu)`
justify-content: space-between;
`
const Wrapper = styled(PaneWrapper)`
overflow: hidden;
`
const Content = styled(PaneContent)`
color: ${color("draculaForeground")};
*::selection {
background: ${color("draculaRed")};
color: ${color("draculaForeground")};
}
`
const ButtonWrapper = styled.div`
display: flex;
align-items: center;
${/* sc-selector */ SecondaryButton}:not(:last-child) {
margin-right: 1rem;
}
${/* sc-selector */ Text} {
margin-right: 2rem;
}
`
const ToggleButton = styled(PrimaryToggleButton)`
height: 4rem;
width: 10rem;
`
const Result = () => {
const [selected, setSelected] = useState<"chart" | "grid">("grid")
const [count, setCount] = useState<number | undefined>()
const handleToggle = useCallback(() => {
setSelected(selected === "grid" ? "chart" : "grid")
}, [selected])
const handleExportClick = useCallback(() => {
window.bus.trigger("grid.publish.query")
}, [])
const handleRefreshClick = useCallback(() => {
window.bus.trigger("grid.refresh")
}, [])
const result = useSelector(selectors.query.getResult)
useEffect(() => {
if (result?.type === QuestDB.Type.DQL) {
setCount(result.count)
}
}, [result])
useEffect(() => {
const grid = document.getElementById("grid")
const chart = document.getElementById("quick-vis")
if (!grid || !chart) {
return
}
if (selected === "grid") {
grid.style.display = "flex"
chart.style.display = "none"
window.bus.trigger(BusEvent.MSG_ACTIVE_PANEL)
} else {
grid.style.display = "none"
chart.style.display = "flex"
}
}, [selected])
return (
<Wrapper>
<Menu>
<ButtonWrapper>
<ToggleButton onClick={handleToggle} selected={selected === "grid"}>
<Grid size="18px" />
<span>Grid</span>
</ToggleButton>
<ToggleButton onClick={handleToggle} selected={selected === "chart"}>
<PieChart size="18px" />
<span>Chart</span>
</ToggleButton>
</ButtonWrapper>
<ButtonWrapper>
{count && (
<Text color="draculaForeground">
{`${count} row${count > 1 ? "s" : ""}`}
</Text>
)}
<PopperHover
delay={350}
placement="bottom"
trigger={
<SecondaryButton onClick={handleRefreshClick}>
<Refresh size="18px" />
</SecondaryButton>
}
>
<Tooltip>Refresh</Tooltip>
</PopperHover>
<PopperHover
delay={350}
placement="bottom"
trigger={
<SecondaryButton onClick={handleExportClick}>
<Download2 size="18px" />
<span>CSV</span>
</SecondaryButton>
}
>
<Tooltip>Download result as a CSV file</Tooltip>
</PopperHover>
</ButtonWrapper>
</Menu>
<Content>
<div id="grid">
<div className="qg-header-row" />
<div className="qg-viewport">
<div className="qg-canvas" />
</div>
</div>
<div id="quick-vis">
<div className="quick-vis-controls">
<form className="v-fit" role="form">
<div className="form-group">
<label>Chart type</label>
<select id="_qvis_frm_chart_type">
<option>bar</option>
<option>line</option>
<option>area</option>
</select>
</div>
<div className="form-group">
<label>Labels</label>
<select id="_qvis_frm_axis_x" />
</div>
<div className="form-group">
<label>Series</label>
<select id="_qvis_frm_axis_y" multiple />
</div>
<button
className="button-primary js-chart-draw"
id="_qvis_frm_draw"
>
<i className="fa fa-play" />
<span>Draw</span>
</button>
</form>
</div>
<div className="quick-vis-canvas" />
</div>
</Content>
</Wrapper>
)
}
export default Result
import React, { MouseEvent, ReactNode, useCallback } from "react"
import styled from "styled-components"
import { Code } from "@styled-icons/entypo/Code"
import { Info } from "@styled-icons/entypo/Info"
import { CodeSSlash } from "@styled-icons/remix-line/CodeSSlash"
import { Information } from "@styled-icons/remix-line/Information"
import {
PopperHover,
......@@ -45,11 +45,15 @@ const Wrapper = styled.div<Pick<Props, "expanded">>`
padding-left: 1rem;
transition: background ${TransitionDuration.REG}ms;
&:hover ${/* sc-selector */ PlusButton} {
&:hover
${/* sc-selector */ PlusButton},
&:active
${/* sc-selector */ PlusButton} {
opacity: 1;
}
&:hover {
&:hover,
&:active {
background: ${color("draculaSelection")};
}
......@@ -67,7 +71,7 @@ const Spacer = styled.span`
flex: 1;
`
const InfoIcon = styled(Info)`
const InfoIcon = styled(Information)`
color: ${color("draculaPurple")};
`
......@@ -115,7 +119,7 @@ const Row = ({
)}
<PlusButton onClick={handleClick} size="sm" tooltip={tooltip}>
<Code size="16px" />
<CodeSSlash size="16px" />
<span>Add</span>
</PlusButton>
......
......@@ -2,8 +2,8 @@ import React, { useCallback, useEffect, useRef, useState } from "react"
import { from, combineLatest, of } from "rxjs"
import { delay, startWith } from "rxjs/operators"
import styled, { css } from "styled-components"
import { Database } from "@styled-icons/entypo/Database"
import { Loader3 } from "@styled-icons/remix-fill/Loader3"
import { Database2 } from "@styled-icons/remix-line/Database2"
import { Loader3 } from "@styled-icons/remix-line/Loader3"
import { Refresh } from "@styled-icons/remix-line/Refresh"
import {
......@@ -43,15 +43,21 @@ const Menu = styled(PaneMenu)`
justify-content: space-between;
`
const Header = styled(Text)`
display: flex;
align-items: center;
`
const Content = styled(PaneContent)<{
_loading: boolean
}>`
display: block;
font-family: ${({ theme }) => theme.fontMonospace};
overflow: auto;
${({ _loading }) => _loading && loadingStyles};
`
const DatabaseIcon = styled(Database)`
const DatabaseIcon = styled(Database2)`
margin-right: 1rem;
`
......@@ -121,17 +127,17 @@ const Schema = ({ widthOffset }: Props) => {
return (
<Wrapper basis={width} ref={element}>
<Menu>
<Text color="draculaForeground">
<Header color="draculaForeground">
<DatabaseIcon size="18px" />
Tables
</Text>
</Header>
<PopperHover
delay={350}
placement="bottom"
trigger={
<SecondaryButton onClick={fetchTables}>
<Refresh size="16px" />
<Refresh size="18px" />
</SecondaryButton>
}
>
......
import React, { useCallback, useEffect, useState } from "react"
import { useSelector } from "react-redux"
import styled from "styled-components"
import { Code } from "@styled-icons/entypo/Code"
import { Upload } from "@styled-icons/entypo/Upload"
import { CodeSSlash } from "@styled-icons/remix-line/CodeSSlash"
import { Upload2 } from "@styled-icons/remix-line/Upload2"
import { PopperHover, PrimaryToggleButton, Tooltip } from "components"
import { selectors } from "store"
......@@ -10,14 +10,16 @@ import { color } from "utils"
const Wrapper = styled.div`
display: flex;
flex: 0 0 45px;
height: calc(100% - 4rem);
flex: 0 0 4.5rem;
flex-direction: column;
border-right: 1px solid rgba(0, 0, 0, 0.1);
`
const Logo = styled.div`
position: relative;
display: flex;
flex: 0 0 41px;
flex: 0 0 4rem;
background: ${color("black")};
z-index: 1;
......@@ -96,7 +98,7 @@ const Sidebar = () => {
onClick={handleConsoleClick}
selected={selected === "console"}
>
<Code size="18px" />
<CodeSSlash size="18px" />
</Navigation>
}
>
......@@ -115,7 +117,7 @@ const Sidebar = () => {
onClick={handleImportClick}
selected={selected === "import"}
>
<Upload size="16px" />
<Upload2 size="18px" />
</Navigation>
</DisabledNavigation>
) : (
......@@ -124,7 +126,7 @@ const Sidebar = () => {
onClick={handleImportClick}
selected={selected === "import"}
>
<Upload size="16px" />
<Upload2 size="18px" />
</Navigation>
)
}
......
import type { ReactNode } from "react"
import type { QueryRawResult } from "utils/questdb"
import {
NotificationShape,
NotificationType,
......@@ -27,6 +29,11 @@ const removeNotification = (payload: Date): QueryAction => ({
type: QueryAT.REMOVE_NOTIFICATION,
})
const setResult = (payload: QueryRawResult): QueryAction => ({
payload,
type: QueryAT.SET_RESULT,
})
const stopRunning = (): QueryAction => ({
type: QueryAT.STOP_RUNNING,
})
......@@ -39,6 +46,7 @@ export default {
addNotification,
cleanupNotifications,
removeNotification,
setResult,
stopRunning,
toggleRunning,
}
......@@ -46,6 +46,13 @@ const query = (state = initialState, action: QueryAction): QueryStateShape => {
}
}
case QueryAT.SET_RESULT: {
return {
...state,
result: action.payload,
}
}
case QueryAT.STOP_RUNNING: {
return {
...state,
......
import { NotificationShape, StoreShape } from "types"
import type { QueryRawResult } from "utils/questdb"
const getNotifications: (store: StoreShape) => NotificationShape[] = (store) =>
store.query.notifications
const getResult: (store: StoreShape) => undefined | QueryRawResult = (store) =>
store.query.result
const getRunning: (store: StoreShape) => boolean = (store) =>
store.query.running
export default {
getNotifications,
getResult,
getRunning,
}
import type { ReactNode } from "react"
import type { QueryRawResult } from "utils/questdb"
export enum NotificationType {
ERROR = "error",
INFO = "info",
......@@ -16,6 +18,7 @@ export type NotificationShape = Readonly<{
export type QueryStateShape = Readonly<{
notifications: NotificationShape[]
result?: QueryRawResult
running: boolean
}>
......@@ -23,6 +26,7 @@ export enum QueryAT {
ADD_NOTIFICATION = "QUERY/ADD_NOTIFICATION",
CLEANUP_NOTIFICATIONS = "QUERY/CLEANUP_NOTIFICATIONS",
REMOVE_NOTIFICATION = "QUERY/REMOVE_NOTIFICATION",
SET_RESULT = "QUERY/SET_RESULT",
STOP_RUNNING = "QUERY/STOP_RUNNING",
TOGGLE_RUNNING = "QUERY/TOGGLE_RUNNING",
}
......@@ -41,6 +45,11 @@ type RemoveNotificationAction = Readonly<{
type: QueryAT.REMOVE_NOTIFICATION
}>
type SetResultAction = Readonly<{
payload: QueryRawResult
type: QueryAT.SET_RESULT
}>
type StopRunningAction = Readonly<{
type: QueryAT.STOP_RUNNING
}>
......@@ -53,5 +62,6 @@ export type QueryAction =
| AddNotificationAction
| CleanupNotificationsAction
| RemoveNotificationAction
| SetResultAction
| StopRunningAction
| ToggleRunningAction
......@@ -280,6 +280,7 @@ ol.unstyled {
background: #21222c;
flex: 0 0 40px;
color: #f8f8f2;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.footer.fixed_full {
......@@ -328,7 +329,7 @@ body.body-small .footer.fixed {
.page-heading {
border-top: 0;
padding: 0 10px 0 10px;
height: 41px;
height: 4rem;
display: flex;
align-items: center;
padding: 0 15px;
......
......@@ -259,17 +259,11 @@
flex: 1;
overflow: hidden;
background: #282a36;
color: #f8f8f2;
::selection {
background: #ff5555;
color: #f9f9f2;
}
}
.menu-bar {
display: flex;
height: 41px;
height: 4rem;
padding: 0 1rem;
align-items: center;
background: #21222c;
......
......@@ -32,6 +32,7 @@
.qg-header-row {
overflow: hidden;
height: 33px;
}
.qg-header-row,
......
......@@ -46,8 +46,19 @@ $drag-drop-border: #c6c6c6;
.ud-canvas {
position: relative;
height: 180px;
overflow: auto;
flex: 1;
}
#import-file-list {
display: flex;
flex-direction: column;
flex: 1;
}
#import-top {
display: flex;
flex-direction: column;
}
.ud-row {
......
......@@ -348,7 +348,7 @@ body.mini-navbar .navbar-default .nav > li > .nav-second-level li a {
.logo-element {
position: relative;
display: flex;
height: 41px;
height: 4rem;
align-items: stretch;
justify-content: stretch;
font-size: 18px;
......
......@@ -22,34 +22,29 @@
*
******************************************************************************/
.splitter,
.qs-ghost {
cursor: row-resize;
.splitter, .qs-ghost {
cursor: row-resize;
}
.splitter {
display: flex;
width: 100%;
height: 8px;
max-height: 8px;
flex: 0 0 8px;
align-items: center;
justify-content: center;
background: #21222c;
border: 1px solid rgba(255, 255, 255, 0.03);
border-left: none;
border-right: none;
width: 100%;
flex: 0 0 6px;
height: 6px;
margin: 3px 0;
border: none;
background: #585858;
}
.splitter svg {
transform: rotate(90deg);
.splitter.qs-dragging {
opacity: 0;
}
.splitter:hover {
background: #44475a;
background: #6a6a6a;
}
.qs-ghost {
background: #bd93f9;
z-index: 20;
background: #6a6a6a;
opacity: 0.4;
z-index: 10;
}
......@@ -12,4 +12,5 @@ export enum BusEvent {
MSG_QUERY_FIND_N_EXEC = "query.build.execute",
MSG_QUERY_OK = "query.out.ok",
MSG_QUERY_RUNNING = "query.out.running",
REACT_READY = "react.ready",
}
......@@ -51,7 +51,12 @@ export type ErrorResult = RawErrorResult & {
type: Type.ERROR
}
export type Result<T extends Record<string, any>> =
export type QueryRawResult =
| (Omit<RawDqlResult, "ddl"> & { type: Type.DQL })
| DdlResult
| ErrorResult
export type QueryResult<T extends Record<string, any>> =
| {
columns: ColumnDefinition[]
count: number
......@@ -122,7 +127,7 @@ export class Client {
this._controllers = []
}
async query<T>(query: string, options?: Options): Promise<Result<T>> {
async query<T>(query: string, options?: Options): Promise<QueryResult<T>> {
const result = await this.queryRaw(query, options)
if (result.type === Type.DQL) {
......@@ -151,12 +156,7 @@ export class Client {
return result
}
async queryRaw(
query: string,
options?: Options,
): Promise<
(Omit<RawDqlResult, "ddl"> & { type: Type.DQL }) | DdlResult | ErrorResult
> {
async queryRaw(query: string, options?: Options): Promise<QueryRawResult> {
const controller = new AbortController()
const payload = {
...options,
......@@ -245,7 +245,7 @@ export class Client {
})
}
async showTables(): Promise<Result<Table>> {
async showTables(): Promise<QueryResult<Table>> {
const response = await this.query<Table>("SHOW TABLES;")
if (response.type === Type.DQL) {
......@@ -268,7 +268,7 @@ export class Client {
return response
}
async showColumns(table: string): Promise<Result<Column>> {
async showColumns(table: string): Promise<QueryResult<Column>> {
return await this.query<Column>(`SHOW COLUMNS FROM '${table}';`)
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册