From 12981619a8d5fc38f1675d84ad466d3dc1e547a2 Mon Sep 17 00:00:00 2001 From: jose-donato <43375532+jose-donato@users.noreply.github.com> Date: Mon, 13 Mar 2023 17:50:48 +0000 Subject: [PATCH] Interactive tables using react (#4386) * feat: experimenting tables with react * feat: adding new interactive tables * feat: adding more table features * small changes * update * Update backend.py * Update backend.py * fix: dark mode? * feat: lot of table improvements * fix: my b * fix: load only when prod * feat: naive detect links and dates * handle index and column names * increased min width * codespell * mypy, pylint * fix: added new chart, export, etc * testing * updates * fix : add PLOT_OPEN_EXPORT, USE_INTERACTIVE_DF to user dataclass * bump pywry version, readd removed `plots_backend().start()` * req files * Update unit-test.yml * Update unit-test.yml * Update unit-test.yml * updates * fixing some date things * update table * fix formatting with timezones * fixing some long prints and tables * Mickey Mouse Co-authored-by: teh_coderer Co-authored-by: jose-donato * fix: ?!?!? * adding limit arg to all print_rich_table * tests * lint * simple by Default on interactive charts * playing with some css (thks jose) * Fix #4426 * Edit some structure * fix sdk generation * Update backend.py * fa menu tested and working * fix #4460 * fix #4460 drop indices * update tests * update tests * Update helper_funcs.py * tests * Update unit-test.yml * Update etf.ipynb --------- Co-authored-by: teh_coderer Co-authored-by: andrewkenreich Co-authored-by: teh_coderer Co-authored-by: jose-donato Co-authored-by: James Maslek --- .../tables/src/components/Table.tsx | 837 ++++++++++++++++++ openbb_terminal/alternative/oss/runa_view.py | 2 - openbb_terminal/core/plots/backend.py | 15 +- .../cryptocurrency/defi/defi_controller.py | 1 - .../cryptocurrency/defi/llama_view.py | 1 - .../discovery/discovery_controller.py | 1 - .../discovery/pycoingecko_view.py | 1 - openbb_terminal/economy/fred_view.py | 5 +- openbb_terminal/helper_funcs.py | 48 + openbb_terminal/reports/templates/etf.ipynb | 4 +- openbb_terminal/stocks/stocks_controller.py | 2 +- ...test_check_for_updates[2.0.0-2.0.0rc1].txt | 6 +- .../test_check_for_updates[v0.0.1-v0.0.1].txt | 6 +- .../test_check_for_updates[v0.0.2-v0.0.1].txt | 6 +- ...test_check_for_updates[v1.10.0-v1.9.0].txt | 6 +- ...test_check_for_updates[v1.9.0-v1.10.0].txt | 6 +- 16 files changed, 921 insertions(+), 26 deletions(-) create mode 100644 frontend-components/tables/src/components/Table.tsx diff --git a/frontend-components/tables/src/components/Table.tsx b/frontend-components/tables/src/components/Table.tsx new file mode 100644 index 000000000000..f25ded1bedbf --- /dev/null +++ b/frontend-components/tables/src/components/Table.tsx @@ -0,0 +1,837 @@ +//@ts-nocheck +import { + ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + Row, + SortingState, + useReactTable, +} from "@tanstack/react-table"; +import { rankItem } from "@tanstack/match-sorter-utils"; +import { utils, writeFile } from "xlsx"; +import { useDrag, useDrop } from "react-dnd"; +import clsx from "clsx"; +import { FC, useEffect, useMemo, useReducer, useRef, useState } from "react"; +import { useVirtual } from "react-virtual"; +import domtoimage from "dom-to-image"; +import Chart from "./Chart"; + +function formatNumberMagnitude(number: number) { + if (number < 10) { + return number.toFixed(4); + } else if (number < 100) { + return number.toFixed(3); + } else if (number < 1000) { + return number.toFixed(2); + } + const abs = Math.abs(number); + if (abs >= 1000000000) { + return `${(number / 1000000000).toFixed(2)}B`; + } else if (abs >= 1000000) { + return `${(number / 1000000).toFixed(2)}M`; + } else if (abs >= 1000) { + return `${(number / 1000).toFixed(2)}K`; + } else { + return number.toFixed(2); + } +} + +const FONT_SIZES_CLASSES = { + small: { + overall: "text-sm", + header: "text-sm", + cell: "text-xs", + }, + medium: { + overall: "text-base", + header: "text-base", + cell: "text-sm", + }, + large: { + overall: "text-base", + header: "text-lg", + cell: "text-base", + }, +}; + +const reorderColumn = ( + draggedColumnId: string, + targetColumnId: string, + columnOrder: string[] +) => { + columnOrder.splice( + columnOrder.indexOf(targetColumnId), + 0, + columnOrder.splice(columnOrder.indexOf(draggedColumnId), 1)[0] as string + ); + return [...columnOrder]; +}; + +const DraggableColumnHeader: FC<{ + header: any; + table: any; + advanced: boolean; +}> = ({ header, table, advanced }) => { + const { getState, setColumnOrder } = table; + const { columnOrder } = getState(); + const { column } = header; + + const [, dropRef] = useDrop({ + accept: "column", + drop: (draggedColumn: any) => { + const newColumnOrder = reorderColumn( + draggedColumn.id, + column.id, + columnOrder + ); + setColumnOrder(newColumnOrder); + }, + }); + + const [{ isDragging }, dragRef, previewRef] = useDrag({ + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + item: () => column, + type: "column", + }); + + return ( + +
+ {header.isPlaceholder ? null : ( + <> +
+ {flexRender(header.column.columnDef.header, header.getContext())} + + {{ + asc: "🔼", + desc: "🔽", + }[header.column.getIsSorted() as string] ?? null} + + {advanced && } +
+ {advanced && header.column.getCanFilter() ? ( +
+ +
+ ) : null} + + )} +
+ + ); +}; + +export const fuzzyFilter = (row, columnId, value, addMeta) => { + const itemRank = rankItem(row.getValue(columnId), value); + addMeta(itemRank); + return itemRank; +}; + +export default function Table({ data, columns }: any) { + const [fontSize, setFontSize] = useState("medium"); + + const increaseFontSize = () => { + if (fontSize === "small") { + setFontSize("medium"); + } else if (fontSize === "medium") { + setFontSize("large"); + } + }; + + const decreaseFontSize = () => { + if (fontSize === "large") { + setFontSize("medium"); + } else if (fontSize === "medium") { + setFontSize("small"); + } + }; + + const [advanced, setAdvanced] = useState(false); + const rerender = useReducer(() => ({}), {})[1]; + const [sorting, setSorting] = useState([]); + const [globalFilter, setGlobalFilter] = useState(""); + const rtColumns = useMemo( + () => [ + { + id: "select", + size: 0, + header: ({ table }) => ( + + ), + cell: ({ row }) => ( +
+ +
+ ), + }, + ...columns.map((column: any) => ({ + accessorKey: column, + id: column, + header: column, + footer: column, + cell: ({ row }: any) => { + const value = row.original[column]; + const valueType = typeof value; + const probablyDate = + column.toLowerCase().includes("date") || + column.toLowerCase() === "index"; + const probablyLink = + valueType === "string" && value.startsWith("http"); + + //TODO - Parse as HTML to make links work if string doesn't start with http + //TODO - Max Column Size + if (probablyLink) { + return ( + + {value} + + ); + } + if (probablyDate) { + if (typeof value === "string") { + return

{value}

; + } + + try { + var dateFormatted = new Date(value).toISOString(); + + // TODO - Remove 00:00:00 from date + + dateFormatted = + dateFormatted.split("T")[0] + + " " + + dateFormatted.split("T")[1].split(".")[0]; + return

{dateFormatted}

; + } catch (e) { + return

{value}

; + } + } + const valueFormatted = + valueType === "number" ? formatNumberMagnitude(value) : value; + return ( +

0, + "text-[#F87171]": value < 0, + "text-[#404040]": value === 0, + })} + > + {value !== 0 + ? value > 0 + ? `${valueFormatted}` + : `${valueFormatted}` + : valueFormatted} +

+ ); + }, + })), + ], + [] + ); + + const [columnOrder, setColumnOrder] = useState( + rtColumns.map((column) => column.id as string) + ); + + const resetOrder = () => + setColumnOrder(columns.map((column) => column.id as string)); + + const table = useReactTable({ + data, + defaultColumn: { + minSize: 0, + size: 0, + }, + columns: rtColumns, + state: { + sorting, + globalFilter, + columnOrder, + }, + onColumnOrderChange: setColumnOrder, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onGlobalFilterChange: setGlobalFilter, + }); + + const tableContainerRef = useRef(null); + const { rows } = table.getRowModel(); + const rowVirtualizer = useVirtual({ + parentRef: tableContainerRef, + size: rows.length, + overscan: 10, + }); + const { virtualItems: virtualRows, totalSize } = rowVirtualizer; + + const paddingTop = virtualRows.length > 0 ? virtualRows?.[0]?.start || 0 : 0; + const paddingBottom = + virtualRows.length > 0 + ? totalSize - (virtualRows?.[virtualRows.length - 1]?.end || 0) + : 0; + + const saveToFile = (blob, fileName) => { + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.setAttribute("href", url); + link.setAttribute("download", fileName); + link.style.visibility = "hidden"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + const downloadData = (type: "csv" | "xlsx") => { + const headers = columns; + const rows = data.map((row) => headers.map((column) => row[column])); + const csvData = [headers, ...rows]; + if (type === "csv") { + const csvContent = csvData.map((e) => e.join(",")).join("\n"); + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); + saveToFile(blob, `${window.title}.csv`); + } else { + const wb = utils.book_new(); + const ws = utils.aoa_to_sheet(csvData); + utils.book_append_sheet(wb, ws, "Sheet1"); + writeFile(wb, `${window.title}.xlsx`); + } + }; + + const downloadImage = () => { + const table = document.getElementById("table"); + domtoimage.toBlob(table).then(function (blob) { + saveToFile(blob, `${window.title}.png`); + }); + }; + + const [dialog, setDialog] = useState(null); + + return ( + <> + {dialog && ( + <> +
setDialog(null)} + className="fixed inset-0 bg-black bg-opacity-50 z-40 backdrop-filter backdrop-blur-sm" + /> +
+ +
+ + )} +
+
+
+ + OpenBB + + + + +
+ + setAdvanced(!advanced)} + /> +
+
+ {advanced && ( + <> +
+ + setGlobalFilter(String(value))} + className="_input" + placeholder="Search all columns..." + /> +
+ +
+ + +
+
+
+ +
+ {table.getAllLeafColumns().map((column) => { + return ( +
+ +
+ ); + })} +
+ + )} +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + ); + })} + + ))} + + + {paddingTop > 0 && ( + + + )} + {virtualRows.map((virtualRow, idx) => { + const row = rows[virtualRow.index]; + return ( + + {row.getVisibleCells().map((cell) => { + return ( + + ); + })} + + ); + })} + {paddingBottom > 0 && ( + + + )} + + + {table.getFooterGroups().map((footerGroup) => ( + + {footerGroup.headers.map((header) => ( + + ))} + + ))} + +
+
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
+
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.footer, + header.getContext() + )} +
+
+ {advanced && ( +
+ + + + + +
Page
+ + {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} + +
+ + | Go to page: + { + const page = e.target.value + ? Number(e.target.value) - 1 + : 0; + table.setPageIndex(page); + }} + className="border p-1 rounded w-16" + /> + + +
+ )} + + + + + +
+
+ + ); +} + +function Filter({ + column, + table, + numberOfColumns, +}: { + column: any; + table: any; + numberOfColumns: number; +}) { + const firstValue = table + .getPreFilteredRowModel() + .flatRows[0]?.getValue(column.id); + + const columnFilterValue = column.getFilterValue(); + + const isProbablyDate = + column.id.toLowerCase().includes("date") || + column.id.toLowerCase() === "index"; + + if (isProbablyDate) { + function getTime(value) { + if (!value) return null; + const date = new Date(value); + const year = date.getFullYear(); + const month = + date.getMonth() + 1 > 9 + ? date.getMonth() + 1 + : `0${date.getMonth() + 1}`; + const day = date.getDate() > 9 ? date.getDate() : `0${date.getDate()}`; + return `${year}-${month}-${day}`; + } + + return ( +
+ { + const value = new Date(e.target.value).getTime(); + column.setFilterValue((old: [string, string]) => [value, old?.[1]]); + }} + placeholder={`Start date`} + className="_input" + /> + { + const value = new Date(e.target.value).getTime(); + column.setFilterValue((old: [string, string]) => [old?.[0], value]); + }} + placeholder={`End date`} + className="_input" + /> +
+ ); + } + + return typeof firstValue === "number" ? ( +
4, + "flex-row": numberOfColumns <= 4, + })} + > + + column.setFilterValue((old: [number, number]) => [ + e.target.value, + old?.[1], + ]) + } + placeholder={`Min`} + className="_input" + /> + + column.setFilterValue((old: [number, number]) => [ + old?.[0], + e.target.value, + ]) + } + placeholder={`Max`} + className="_input" + /> +
+ ) : ( + column.setFilterValue(e.target.value)} + placeholder={`Search...`} + className="_input" + /> + ); +} + +type Props = { + value: string | number; + onChange: (value: string | number) => void; + debounce?: number; +} & Omit, "onChange">; + +const DebouncedInput: FC = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}) => { + const [value, setValue] = useState(initialValue); + + const handleInputChange = (event: React.ChangeEvent) => + setValue(event.target.value); + + useEffect(() => { + setValue(initialValue); + }, [initialValue]); + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value); + }, debounce); + + return () => clearTimeout(timeout); + }, [value]); + + return ; +}; + +function IndeterminateCheckbox({ + indeterminate, + className = "", + ...rest +}: { indeterminate?: boolean } & HTMLProps) { + const ref = useRef(null!); + + useEffect(() => { + if (typeof indeterminate === "boolean") { + ref.current.indeterminate = !rest.checked && indeterminate; + } + }, [ref, indeterminate]); + + return ( + + ); +} diff --git a/openbb_terminal/alternative/oss/runa_view.py b/openbb_terminal/alternative/oss/runa_view.py index 016d3726eb99..581d1ecf6b40 100644 --- a/openbb_terminal/alternative/oss/runa_view.py +++ b/openbb_terminal/alternative/oss/runa_view.py @@ -55,8 +55,6 @@ def display_rossindex( if df.empty: console.print("\nError in runa request\n") else: - plots_backend().send_table(df, title="Runa ROSS Index") - if sortby in runa_model.SORT_COLUMNS: df = df.sort_values(by=sortby, ascending=ascend) df = df.head(limit) diff --git a/openbb_terminal/core/plots/backend.py b/openbb_terminal/core/plots/backend.py index 1438d7391e3a..0812548b76af 100644 --- a/openbb_terminal/core/plots/backend.py +++ b/openbb_terminal/core/plots/backend.py @@ -230,10 +230,23 @@ def send_table(self, df_table: pd.DataFrame, title: str = "", source: str = ""): Source of the data, by default "" """ self.loop.run_until_complete(self.check_backend()) + + if title: + # We remove any html tags and markdown from the title + title = re.sub(r"<[^>]*>", "", title) + title = re.sub(r"\[\/?[a-z]+\]", "", title) + + # we get the length of each column using the max length of the column + # name and the max length of the column values as the column width columnwidth = [ - max(len(str(df_table[col].name)), df_table[col].astype(str).str.len().max()) + max( + len(str(df_table[col].name)), + df_table[col].astype(str).str.len().max(), + ) for col in df_table.columns + if hasattr(df_table[col], "name") and hasattr(df_table[col], "dtype") ] + # we add a percentage of max to the min column width columnwidth = [ int(x + (max(columnwidth) - min(columnwidth)) * 0.2) for x in columnwidth diff --git a/openbb_terminal/cryptocurrency/defi/defi_controller.py b/openbb_terminal/cryptocurrency/defi/defi_controller.py index 58b9da1c5a11..67b646d2dcf7 100644 --- a/openbb_terminal/cryptocurrency/defi/defi_controller.py +++ b/openbb_terminal/cryptocurrency/defi/defi_controller.py @@ -465,7 +465,6 @@ def call_ldapps(self, other_args: List[str]): ) if ns_parser: llama_view.display_defi_protocols( - interactive=ns_parser.interactive, limit=ns_parser.limit, sortby=ns_parser.sortby, ascend=ns_parser.reverse, diff --git a/openbb_terminal/cryptocurrency/defi/llama_view.py b/openbb_terminal/cryptocurrency/defi/llama_view.py index d86dbdb917fc..49b49fb5d411 100644 --- a/openbb_terminal/cryptocurrency/defi/llama_view.py +++ b/openbb_terminal/cryptocurrency/defi/llama_view.py @@ -77,7 +77,6 @@ def display_grouped_defi_protocols( @log_start_end(log=logger) def display_defi_protocols( sortby: str, - interactive: bool = False, limit: int = 20, ascend: bool = False, description: bool = False, diff --git a/openbb_terminal/cryptocurrency/discovery/discovery_controller.py b/openbb_terminal/cryptocurrency/discovery/discovery_controller.py index d9e5fc24b0e4..769614430c0d 100644 --- a/openbb_terminal/cryptocurrency/discovery/discovery_controller.py +++ b/openbb_terminal/cryptocurrency/discovery/discovery_controller.py @@ -162,7 +162,6 @@ def call_top(self, other_args): if ns_parser: if ns_parser.source == "CoinGecko": pycoingecko_view.display_coins( - interactive=ns_parser.interactive, sortby=" ".join(ns_parser.sortby), category=ns_parser.category, limit=ns_parser.limit, diff --git a/openbb_terminal/cryptocurrency/discovery/pycoingecko_view.py b/openbb_terminal/cryptocurrency/discovery/pycoingecko_view.py index 786fe5200322..e8b618046f30 100644 --- a/openbb_terminal/cryptocurrency/discovery/pycoingecko_view.py +++ b/openbb_terminal/cryptocurrency/discovery/pycoingecko_view.py @@ -32,7 +32,6 @@ @log_start_end(log=logger) def display_coins( category: str, - interactive: bool = False, limit: int = 250, sortby: str = "Symbol", export: str = "", diff --git a/openbb_terminal/economy/fred_view.py b/openbb_terminal/economy/fred_view.py index 0a0cfc2b18fe..df038e0bd5fc 100644 --- a/openbb_terminal/economy/fred_view.py +++ b/openbb_terminal/economy/fred_view.py @@ -369,12 +369,15 @@ def plot_cpi( fig.add_scatter(x=df.index, y=df[column].values, name=label) if raw: + # was a -iloc so we need to flip the index as we use head + df = df.sort_index(ascending=False) print_rich_table( - df.iloc[-10:], + df, title=title, show_index=True, floatfmt=".3f", export=bool(export), + limit=limit, ) export_data( diff --git a/openbb_terminal/helper_funcs.py b/openbb_terminal/helper_funcs.py index a99cc2dd021d..3872b82b4228 100644 --- a/openbb_terminal/helper_funcs.py +++ b/openbb_terminal/helper_funcs.py @@ -372,6 +372,54 @@ def _get_headers(_headers: Union[List[str], pd.Index]) -> List[str]: if columns_to_auto_color is None and rows_to_auto_color is None: df = df.applymap(lambda x: return_colored_value(str(x))) + def _get_headers(_headers: Union[List[str], pd.Index]) -> List[str]: + """Check if headers are valid and return them.""" + output = _headers + if isinstance(_headers, pd.Index): + output = list(_headers) + if len(output) != len(df.columns): + log_and_raise( + ValueError("Length of headers does not match length of DataFrame") + ) + return output + + if current_user.preferences.USE_INTERACTIVE_DF and plots_backend().isatty: + df_outgoing = df.copy() + # If headers are provided, use them + if headers is not None: + # We check if headers are valid + df_outgoing.columns = _get_headers(headers) + + if show_index and index_name not in df_outgoing.columns: + # If index name is provided, we use it + df_outgoing.index.name = index_name or "Index" + df_outgoing = df_outgoing.reset_index() + + for col in df_outgoing.columns: + if col == "": + df_outgoing = df_outgoing.rename(columns={col: " "}) + + plots_backend().send_table(df_table=df_outgoing, title=title) + return + + df = df.copy() if not limit else df.copy().iloc[:limit] + if current_user.preferences.USE_COLOR and automatic_coloring: + if columns_to_auto_color: + for col in columns_to_auto_color: + # checks whether column exists + if col in df.columns: + df[col] = df[col].apply(lambda x: return_colored_value(str(x))) + if rows_to_auto_color: + for row in rows_to_auto_color: + # checks whether row exists + if row in df.index: + df.loc[row] = df.loc[row].apply( + lambda x: return_colored_value(str(x)) + ) + + if columns_to_auto_color is None and rows_to_auto_color is None: + df = df.applymap(lambda x: return_colored_value(str(x))) + if current_user.preferences.USE_TABULATE_DF: table = Table(title=title, show_lines=True, show_header=show_header) diff --git a/openbb_terminal/reports/templates/etf.ipynb b/openbb_terminal/reports/templates/etf.ipynb index 470ce9b2a17f..2b5484ff71ee 100644 --- a/openbb_terminal/reports/templates/etf.ipynb +++ b/openbb_terminal/reports/templates/etf.ipynb @@ -19,8 +19,8 @@ "import numpy as np\n", "import datetime\n", "\n", - "from IPython.display import HTML\n", - "\n", + "from IPython.display import HTML \n", + " \n", "import openbb_terminal.config_terminal as cfg\n", "from openbb_terminal.reports import widget_helpers as widgets\n", "from openbb_terminal.sdk import openbb\n", diff --git a/openbb_terminal/stocks/stocks_controller.py b/openbb_terminal/stocks/stocks_controller.py index 5e9c95577794..d6dad80c8cfe 100644 --- a/openbb_terminal/stocks/stocks_controller.py +++ b/openbb_terminal/stocks/stocks_controller.py @@ -380,7 +380,7 @@ def call_candle(self, other_args: List[str]): dest="ticker", help="Ticker to analyze", type=str, - default=self.ticker, + default=None, required=not any(x in other_args for x in ["-h", "--help"]) and not self.ticker, ) diff --git a/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[2.0.0-2.0.0rc1].txt b/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[2.0.0-2.0.0rc1].txt index 4b594bf1bd9c..029d4ce3985c 100644 --- a/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[2.0.0-2.0.0rc1].txt +++ b/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[2.0.0-2.0.0rc1].txt @@ -1,3 +1,3 @@ -[yellow]Unable to check for updates... Check your internet connection and try again...[/yellow] - - +[yellow]Unable to check for updates... Check your internet connection and try again...[/yellow] + + diff --git a/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[v0.0.1-v0.0.1].txt b/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[v0.0.1-v0.0.1].txt index 4b594bf1bd9c..029d4ce3985c 100644 --- a/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[v0.0.1-v0.0.1].txt +++ b/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[v0.0.1-v0.0.1].txt @@ -1,3 +1,3 @@ -[yellow]Unable to check for updates... Check your internet connection and try again...[/yellow] - - +[yellow]Unable to check for updates... Check your internet connection and try again...[/yellow] + + diff --git a/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[v0.0.2-v0.0.1].txt b/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[v0.0.2-v0.0.1].txt index 4b594bf1bd9c..029d4ce3985c 100644 --- a/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[v0.0.2-v0.0.1].txt +++ b/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[v0.0.2-v0.0.1].txt @@ -1,3 +1,3 @@ -[yellow]Unable to check for updates... Check your internet connection and try again...[/yellow] - - +[yellow]Unable to check for updates... Check your internet connection and try again...[/yellow] + + diff --git a/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[v1.10.0-v1.9.0].txt b/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[v1.10.0-v1.9.0].txt index 4b594bf1bd9c..029d4ce3985c 100644 --- a/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[v1.10.0-v1.9.0].txt +++ b/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[v1.10.0-v1.9.0].txt @@ -1,3 +1,3 @@ -[yellow]Unable to check for updates... Check your internet connection and try again...[/yellow] - - +[yellow]Unable to check for updates... Check your internet connection and try again...[/yellow] + + diff --git a/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[v1.9.0-v1.10.0].txt b/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[v1.9.0-v1.10.0].txt index 4b594bf1bd9c..029d4ce3985c 100644 --- a/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[v1.9.0-v1.10.0].txt +++ b/tests/openbb_terminal/txt/test_terminal_helper/test_check_for_updates[v1.9.0-v1.10.0].txt @@ -1,3 +1,3 @@ -[yellow]Unable to check for updates... Check your internet connection and try again...[/yellow] - - +[yellow]Unable to check for updates... Check your internet connection and try again...[/yellow] + +