diff --git a/.gitignore b/.gitignore index 3836f423..b538631e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - +.vscode # dependencies /node_modules /.pnp diff --git a/public/index.html b/public/index.html index 9a2ac092..f4fc4876 100644 --- a/public/index.html +++ b/public/index.html @@ -24,6 +24,8 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> + + Parseable Log Storage diff --git a/src/components/Dropdown/index.js b/src/components/Dropdown/index.js new file mode 100644 index 00000000..89d32541 --- /dev/null +++ b/src/components/Dropdown/index.js @@ -0,0 +1,84 @@ +import { Listbox } from "@headlessui/react"; +import { SelectorIcon } from "@heroicons/react/solid"; +import Button from "../DropdownButton"; + +const Dropdown = ({ name, disabled, value, setValue }) => { + const data = [ + { + name: "1 sec", + value: 1, + }, + { + name: "2 sec", + value: 2, + }, + { + name: "5 sec", + value: 5, + }, + { + name: "10 sec", + value: 10, + }, + { + name: "20 sec", + value: 20, + }, + { + name: "1 min", + value: 60, + }, + { + name: "None", + value: null, + }, + ]; + + return ( +
+ + + +
+ {disabled ? "None" : data.find((obj) => obj.value === value).name} +
+
+ + {data.map((obj) => ( + + {({ active, selected }) => ( +
+ ); +}; + +export default Dropdown; diff --git a/src/components/DropdownButton/index.js b/src/components/DropdownButton/index.js new file mode 100644 index 00000000..ef14d4eb --- /dev/null +++ b/src/components/DropdownButton/index.js @@ -0,0 +1,21 @@ +import classNames from "classnames"; +import { CheckIcon } from "@heroicons/react/solid"; + +const Button = ({ text, active, selected }) => { + return ( +
  • + {text} + {selected && ( + + )} +
  • + ); +}; + +export default Button; diff --git a/src/components/MultiSelectDropdown/index.js b/src/components/MultiSelectDropdown/index.js new file mode 100644 index 00000000..d125c4a2 --- /dev/null +++ b/src/components/MultiSelectDropdown/index.js @@ -0,0 +1,54 @@ +import { Listbox } from "@headlessui/react"; +import { ChevronDownIcon } from "@heroicons/react/outline"; +import Button from "../DropdownButton"; +import Pill from "../Pill"; + +const MultipleSelectDropdown = ({ + name, + values, + setSelectedValues, + data, + removeValue, +}) => ( +
    + + +
    + + {values.length > 0 + ? values.map((val) => ( + removeValue(val)} /> + )) + : "Select Tags"} + + + + + {data.length !== 0 ? ( + data.map((value) => ( + + {({ active, selected }) => ( +
    +
    +
    +); + +export default MultipleSelectDropdown; diff --git a/src/components/MultipleListBox/index.js b/src/components/MultipleListBox/index.js deleted file mode 100644 index 76be8966..00000000 --- a/src/components/MultipleListBox/index.js +++ /dev/null @@ -1,35 +0,0 @@ -import { useState } from "react"; -import { Listbox } from "@headlessui/react"; - -const people = [ - { id: 1, name: "Durward Reynolds" }, - { id: 2, name: "Kenton Towne" }, - { id: 3, name: "Therese Wunsch" }, - { id: 4, name: "Benedict Kessler" }, - { id: 5, name: "Katelyn Rohan" }, -]; - -export default function MultipleListBox() { - const [selectedPeople, setSelectedPeople] = useState([]); - - return ( - - - {selectedPeople.length > 0 - ? selectedPeople.map((person) => ( - - {person.name} - - )) - : "Select Tags"} - - - {people.map((person) => ( - - {person.name} - - ))} - - - ); -} diff --git a/src/components/Pill/index.js b/src/components/Pill/index.js new file mode 100644 index 00000000..1983d61f --- /dev/null +++ b/src/components/Pill/index.js @@ -0,0 +1,15 @@ +import { XCircleIcon } from "@heroicons/react/solid"; + +const Pill = ({ text, closable, onClose }) => { + return ( + + {text} + + + ); +}; + +export default Pill; diff --git a/src/components/SearchableDropdown/index.js b/src/components/SearchableDropdown/index.js new file mode 100644 index 00000000..ea2ed7dd --- /dev/null +++ b/src/components/SearchableDropdown/index.js @@ -0,0 +1,66 @@ +import { memo, useState } from "react"; +import { Combobox } from "@headlessui/react"; +import { SelectorIcon } from "@heroicons/react/solid"; +import DropdownButton from "../DropdownButton"; + +const SearchableDropdown = ({ data, setValue, value, label }) => { + const [query, setQuery] = useState(""); + // TODO: Throw error on no name + // TODO: Remove value from prop + + const filteredData = + query === "" + ? data + : data.filter((obj) => { + return obj.name.toLowerCase().includes(query.toLowerCase()); + }); + + return ( +
    + + +
    + setQuery(event.target.value)} + displayValue={(value) => + data.length && value ? value.name : "No data found" + } + /> + + + + {!!filteredData ? ( + filteredData.map((data) => ( + + {({ active, selected }) => ( + + )} + + )) + ) : ( +
    No data
    + )} +
    +
    +
    +
    + ); +}; + +export default memo(SearchableDropdown); diff --git a/src/index.css b/src/index.css index e4e00dbf..d94b4eac 100644 --- a/src/index.css +++ b/src/index.css @@ -2,10 +2,28 @@ @tailwind components; @tailwind utilities; -.custom-focus{ - @apply focus:ring-yellowButton focus:border-yellowButton focus:ring-1 focus:outline-0; +@layer components { + .focus { + @apply focus:ring-secondary-700 focus:border-secondary-700 focus:ring-1 focus:outline-0; + } + + .text-input { + @apply text-gray-800 font-medium text-sm; + } + + .text-label { + @apply text-xs font-medium text-gray-900; + } + + .input { + @apply w-full rounded-lg py-2 px-3 border-2 bg-gray-50 border-gray-400 text-input focus; + } +} + +.custom-focus { + @apply focus:ring-yellowButton focus:border-yellowButton focus:ring-1 focus:outline-0; } .custom-input { @apply w-full rounded py-2 px-3 border border-grey font-medium text-textBlack; -} \ No newline at end of file +} diff --git a/src/page/Dashboard/DatePicker.js b/src/page/Dashboard/DatePicker.js index e1d040ac..b93944e5 100644 --- a/src/page/Dashboard/DatePicker.js +++ b/src/page/Dashboard/DatePicker.js @@ -8,8 +8,8 @@ const FORMAT = "DD-MM-YYYY HH:mm"; const Calendar = ({ setStartDate, setEndDate, start, end }) => { const [dateRange, setDateRange] = useState([ - moment(start).unix(), - moment(end).unix(), + moment(start).toDate(), + moment(end).toDate(), ]); const [startDate, endDate] = dateRange; const ExampleCustomInput = forwardRef(({ value, onClick }, ref) => ( @@ -20,6 +20,7 @@ const Calendar = ({ setStartDate, setEndDate, start, end }) => { /> )); + console.log({startDate,endDate}) return ( setIsOpen(!isOpen)} className={ - "search-button flex disabled:text-gray-300 mt-1 h-[2.65rem] custom-focus text-left" + "input rounded-r-none flex border-r-0 disabled:text-gray-300 mt-1 h-[2.5rem] text-left w-80" } > {range === 7 ? ( - + {moment(fromDate).format(FORMAT)} - {moment(toDate).format(FORMAT)} ) : ( @@ -134,7 +134,7 @@ const DateRangeSelector = ({
    -
    -
    diff --git a/src/page/Dashboard/DateSearchField.js b/src/page/Dashboard/DateSearchField.js new file mode 100644 index 00000000..63596378 --- /dev/null +++ b/src/page/Dashboard/DateSearchField.js @@ -0,0 +1,60 @@ +import { Combobox } from "@headlessui/react"; +import Calendar from "./DateRangeSeletor"; +import { SearchIcon } from "@heroicons/react/solid"; + +const DateSearchField = ({ + range, + setStartTime, + setEndTime, + startTime, + endTime, + setRange, + getRange, + searchSelected, + setSearchSelected, + setSearchOpen, + setSearchQuery, +}) => ( +
    + +
    + + { + setSearchSelected(e); + setSearchOpen(true); + }} + > +
    +
    + 'Search'} + placeholder="Search" + onChange={(event) => setSearchQuery(event.target.value)} + /> + + +
    +
    +
    +
    +
    +); + +export default DateSearchField; diff --git a/src/page/Dashboard/RefreshInterval.js b/src/page/Dashboard/RefreshInterval.js new file mode 100644 index 00000000..8c926b28 --- /dev/null +++ b/src/page/Dashboard/RefreshInterval.js @@ -0,0 +1,48 @@ +import Dropdown from "../../components/Dropdown"; + +const RefreshInterval = ({ range, interval, setInterval }) => { + const refreshIntervalArray = [ + { + name: "1 sec", + value: 1, + }, + { + name: "2 sec", + value: 2, + }, + { + name: "5 sec", + value: 5, + }, + { + name: "10 sec", + value: 10, + }, + { + name: "20 sec", + value: 20, + }, + { + name: "1 min", + value: 60, + }, + { + name: "None", + value: null, + }, + ]; + + return ( +
    + +
    + ); +}; + +export default RefreshInterval; diff --git a/src/page/Dashboard/Table.js b/src/page/Dashboard/Table.js new file mode 100644 index 00000000..f167e6aa --- /dev/null +++ b/src/page/Dashboard/Table.js @@ -0,0 +1,119 @@ +import BeatLoader from "react-spinners/BeatLoader"; + +function hasSubArray(master, sub) { + master.sort(); + sub.sort(); + return sub.every( + ( + (i) => (v) => + (i = master.indexOf(v, i) + 1) + )(0) + ); +} + +const Table = ({ + selectedLogSchema, + logQueries, + selectedTags, + searchQuery, + setOpen, + setClickedRow, + addAvailableTags, +}) => { + return ( + <> + + + + {selectedLogSchema?.map((name) => ( + + ))} + + + {logQueries.isLoading && + (!logQueries.data || + !logQueries.data?.data || + logQueries.data?.data?.length === 0) ? ( + + + + + + ) : ( + + {logQueries?.data?.pages?.map && + logQueries.data.pages.map( + (page) => + page?.data?.map && + page?.data?.map( + (data, index) => + hasSubArray(data.p_tags?.split("^"), selectedTags) && + (searchQuery === "" || + JSON.stringify(data) + .toLowerCase() + .includes(searchQuery.toLowerCase())) && ( + { + console.log(JSON.stringify(data)); + setOpen(true); + setClickedRow(data); + }} + className="cursor-pointer hover:bg-slate-100 hover:shadow" + key={index} + > + {selectedLogSchema.map((schema) => ( + + ))} + {data.p_tags?.split("^").forEach((tag) => { + addAvailableTags(tag); + })} + + ) + ) + )} + + + + + )} +
    + {name} +
    + +
    + {data[schema] || ""} +
    + +
    + + ); +}; + +export default Table; diff --git a/src/page/Dashboard/TagFilters.js b/src/page/Dashboard/TagFilters.js new file mode 100644 index 00000000..047f3ecb --- /dev/null +++ b/src/page/Dashboard/TagFilters.js @@ -0,0 +1,20 @@ +import MultiSelectDropdown from "../../components/MultiSelectDropdown"; + +const TagFilters = ({ + selectedTags, + setSelectedTags, + availableTags, + removeTag, +}) => ( +
    + +
    +); + +export default TagFilters; diff --git a/src/page/Dashboard/index.js b/src/page/Dashboard/index.js index f904aace..b016ac1c 100644 --- a/src/page/Dashboard/index.js +++ b/src/page/Dashboard/index.js @@ -1,14 +1,8 @@ import moment from "moment"; -import { useState, Fragment, useEffect } from "react"; +import { useState, useEffect } from "react"; import Layout from "../../components/Layout"; import SideDialog from "../../components/SideDialog"; -import { Listbox, Transition } from "@headlessui/react"; -import { SearchIcon } from "@heroicons/react/solid"; -import { ChevronDownIcon } from "@heroicons/react/outline"; -import { Combobox } from "@headlessui/react"; -import { CheckIcon, XCircleIcon, SelectorIcon } from "@heroicons/react/solid"; -import BeatLoader from "react-spinners/BeatLoader"; -import { Menu } from "@headlessui/react"; +import Table from "./Table"; import { useGetLogStream, useGetLogStreamSchema, @@ -16,24 +10,10 @@ import { } from "../../utils/api"; import "./index.css"; import Field from "./FieldBox"; -import Calendar from "./DateRangeSeletor"; - -const override = { - display: "block", - margin: "0 auto", - borderColor: "red", -}; - -function hasSubArray(master, sub) { - master.sort(); - sub.sort(); - return sub.every( - ( - (i) => (v) => - (i = master.indexOf(v, i) + 1) - )(0) - ); -} +import TagFilters from "./TagFilters"; +import RefreshInterval from "./RefreshInterval"; +import DateSearchField from "./DateSearchField"; +import SearchableDropdown from "../../components/SearchableDropdown"; const Dashboard = () => { const getCurrentTime = () => { @@ -55,7 +35,6 @@ const Dashboard = () => { const getRange = () => { return { - // "Live tracking": [moment(start), moment(end)], "Past 10 Minutes": [ getCurrentTime().subtract(10, "minutes"), getCurrentTime(), @@ -81,7 +60,6 @@ const Dashboard = () => { const [open, setOpen] = useState(false); const [clickedRow, setClickedRow] = useState({}); const [searchOpen, setSearchOpen] = useState(false); - const [query, setQuery] = useState(""); const [searchQuery, setSearchQuery] = useState(""); const [searchSelected, setSearchSelected] = useState({}); const [interval, setInterval] = useState(null); @@ -91,6 +69,8 @@ const Dashboard = () => { const [selectedTags, setSelectedTags] = useState([]); const [startTime, setStartTime] = useState( getCurrentTime().subtract(10, "minutes") + // .utcOffset("+00:00") + // .format("YYYY-MM-DDThh:mm:ss), ); const addAvailableTags = (label) => { @@ -103,39 +83,7 @@ const Dashboard = () => { const [endTime, setEndTime] = useState(getCurrentTime()); - const refreshInterval = [ - { - name: "1 sec", - value: 1, - }, - { - name: "2 sec", - value: 2, - }, - { - name: "5 sec", - value: 5, - }, - { - name: "10 sec", - value: 10, - }, - { - name: "20 sec", - value: 20, - }, - { - name: "1 min", - value: 60, - }, - { - name: "None", - value: null, - }, - ]; - let rangeArr = [ - // "Live tracking", "Past 10 Minutes", "Past 1 Hour", "Past 5 Hours", @@ -223,22 +171,6 @@ const Dashboard = () => { }; }, [fetchNextPage]); - const getFilteredArray = (data, searchString, key) => { - if (!data) { - return []; - } - if (!searchString) { - return data; - } else { - return data.filter((data) => - data[key] - .toLowerCase() - .replace(/\s+/g, "") - .includes(searchQuery.toLowerCase().replace(/\s+/g, "")) - ); - } - }; - const removeTag = (tag) => { setSelectedTags([...selectedTags.filter((item) => item !== tag)]); }; @@ -253,275 +185,37 @@ const Dashboard = () => { >
    -
    -
    -
    - - { - setSelectedLogStream(e); - }} - > -
    - - logStream.isError || !logStream?.data?.data.length - ? "No log streams found" - : stream.name - } - onChange={(event) => setQuery(event.target.value)} - /> - - - setQuery("")} - > - - {getFilteredArray( - logStream?.data?.data, - query, - "name" - ).length === 0 && query !== "" ? ( -
    - No log streams found -
    - ) : ( - getFilteredArray( - logStream?.data?.data, - query, - "name" - ).map((stream, index) => ( - - `relative cursor-default select-none py-2 pl-10 pr-4 ${ - active - ? "bg-bluePrimary text-white" - : "text-gray-900" - }` - } - value={stream} - > - {({ selected, active }) => ( - <> - - {stream.name} - - {selected ? ( - - - ) : null} - - )} - - )) - )} -
    -
    -
    -
    -
    -
    -
    - -
    - - { - setSearchSelected(e); - setSearchOpen(true); - }} - > -
    -
    - 'Search'} - placeholder="Search" - onChange={(event) => - setSearchQuery(event.target.value) - } - /> - - -
    -
    -
    -
    -
    -
    - - - -
    - {range === 7 - ? "None" - : refreshInterval.find((obj) => obj.value === interval) - .name} -
    -
    - - {refreshInterval.map((interval) => ( - - {({ active, selected }) => ( -
    setInterval(interval.value)} - className={`block custom-focus cursor-pointer hover:bg-bluePrimary hover:text-white text-sm font-semibold select-none py-2 px-4 text-gray-700`} - > - {interval.name} -
    - )} -
    - ))} -
    -
    -
    - -
    - - -
    - - {selectedTags.length > 0 - ? selectedTags.map((tag) => ( - - {tag} - removeTag(tag)} - className="hover:text-gray-600 transform duration-200 text-gray-700 w-4 absolute top-1 right-1" - /> - - )) - : "Select Tags"} - - - - - - {availableTags.length !== 0 ? ( - availableTags.map((person, personIdx) => ( - - `relative cursor-default select-none py-2 px-2 ${ - active - ? "bg-bluePrimary text-white" - : "text-gray-900" - }` - } - value={person} - > - {({ selected }) => ( - <> - - {selected ? ( -
    - -
    - ) : ( -
    - )} - {person} -
    - - )} -
    - )) - ) : ( - Nothing Found - )} -
    -
    -
    -
    -
    +
    + + + +
    @@ -534,93 +228,19 @@ const Dashboard = () => { selectedLogSchema={selectedLogSchema} />
    +
    +
    - - - - {selectedLogSchema?.map((name) => ( - - ))} - - - {logQueries.isLoading && - (!logQueries.data || - !logQueries.data?.data || - logQueries.data?.data?.length === 0) ? ( - - - - - - ) : ( - - {logQueries?.data?.pages?.map && - logQueries.data.pages.map( - (page) => - page?.data?.map && - page?.data?.map( - (data, index) => - hasSubArray( - data.p_tags?.split("^"), - selectedTags - ) && - (searchQuery === "" || - JSON.stringify(data) - .toLowerCase() - .includes(searchQuery.toLowerCase())) && ( - { - console.log(JSON.stringify(data)); - setOpen(true); - setClickedRow(data); - }} - className="cursor-pointer hover:bg-slate-100 hover:shadow" - key={index} - > - {selectedLogSchema.map((schema) => ( - - ))} - {data.p_tags?.split("^").forEach((tag) => { - addAvailableTags(tag); - })} - - ) - ) - )} - - - - - )} -
    - {name} -
    - -
    - {data[schema] || ""} -
    - -
    + diff --git a/src/utils/api/index.js b/src/utils/api/index.js index 288e1e22..76f4f832 100644 --- a/src/utils/api/index.js +++ b/src/utils/api/index.js @@ -5,7 +5,7 @@ export const getServerURL = () => { }; export const get = async (url) => { - return await axios.get(getServerURL() + url, { + return axios.get(getServerURL() + url, { headers: { Authorization: "Basic " + localStorage.getItem("auth"), }, @@ -13,7 +13,7 @@ export const get = async (url) => { }; export const post = async (url, data, signal) => { - return await axios.post( + return axios.post( getServerURL() + url, data, { @@ -26,11 +26,11 @@ export const post = async (url, data, signal) => { }; export const getLogStream = async () => { - return await get("api/v1/logstream"); + return get("api/v1/logstream"); }; export const queryLogs = async (streamName, startTime, endTime) => { - return await post("api/v1/query", { + return post("api/v1/query", { query: `select * from ${streamName}`, startTime: startTime, endTime: endTime, diff --git a/src/utils/api/logstreams.js b/src/utils/api/logstreams.js index 6e2f9afa..ef98d6b4 100644 --- a/src/utils/api/logstreams.js +++ b/src/utils/api/logstreams.js @@ -6,7 +6,7 @@ import { } from "./constants"; const getLogStream = async () => { - return await get(LOG_STREAMS_URL); + return get(LOG_STREAMS_URL); }; export const useGetLogStream = (option = {}) => @@ -14,7 +14,7 @@ export const useGetLogStream = (option = {}) => const getLogStreamSchema = async (streamName) => { - return await get(`${LOG_STREAMS_URL}/${streamName}/schema`); + return get(`${LOG_STREAMS_URL}/${streamName}/schema`); }; export const useGetLogStreamSchema = (streamName, option = {}) => diff --git a/src/utils/helpers/dates.js b/src/utils/helpers/dates.js new file mode 100644 index 00000000..e69de29b diff --git a/src/utils/helpers/index.js b/src/utils/helpers/index.js new file mode 100644 index 00000000..9f66484e --- /dev/null +++ b/src/utils/helpers/index.js @@ -0,0 +1 @@ +export * from "./dates.js"; diff --git a/tailwind.config.js b/tailwind.config.js index 4c0dd49e..05d780a3 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,8 +1,14 @@ /** @type {import('tailwindcss').Config} */ + +const defaultTheme = require("tailwindcss/defaultTheme"); + module.exports = { content: ["./src/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"], theme: { extend: { + fontFamily: { + sans: ["Inter var", ...defaultTheme.fontFamily.sans], + }, colors: { grey: "#bababa", bluePrimary: "#1A237E", @@ -10,7 +16,32 @@ module.exports = { codeBack: "#242424", drawerBlue: "#171F6F", textBlack: "#4a4a4a", - iconGrey: "#9ca3af" + iconGrey: "#9ca3af", + gray: { + DEFAULT: "#E0E0E0", + 50: "#FAFAFA", + 100: "#F5F5F5", + 200: "#EEEEEE", + 300: "#E0E0E0", + 400: "#BDBDBD", + 500: "#9E9E9E", + 600: "#757575", + 700: "#616161", + 800: "#424242", + 900: "#212121", + }, + primary: { + DEFAULT: "#1A237E", + 200: "#4192DF", + 400: "#1A237E", + 700: "#10143E", + }, + secondary: { + DEFAULT: "#F29C38", + 200: "#f6ba74", + 400: "#F29C38", + 700: "#c27d2d", + }, }, backgroundImage: { "login-back": "url('assets/images/Path 369.svg')",