onCellClick(dataItem, date, index) : null
- // }
/>
)}
diff --git a/aim/web/ui/src/components/HeatMap/HeatMapStyle.scss b/aim/web/ui/src/components/HeatMap/HeatMapStyle.scss
index 9571c9bc7c..2c47957855 100644
--- a/aim/web/ui/src/components/HeatMap/HeatMapStyle.scss
+++ b/aim/web/ui/src/components/HeatMap/HeatMapStyle.scss
@@ -1,8 +1,5 @@
@use 'styles/abstracts/index' as *;
-.CalendarHeatmap {
-}
-
.CalendarHeatmap__map {
display: grid;
grid-template-columns: 1em 1fr;
@@ -26,6 +23,9 @@
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
+ position: sticky;
+ left: 0;
+ background-color: $white;
}
}
@@ -49,16 +49,10 @@
width: 100%;
height: 100%;
box-sizing: border-box;
- box-shadow: inset 0 0 0 1px #ffff;
- border: 1px solid #1473e6;
border-radius: 3px;
transition: box-shadow 50ms ease;
cursor: pointer;
- &:hover {
- box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.3);
- }
-
&.CalendarHeatmap__cell--dummy {
opacity: 0;
pointer-events: none;
@@ -67,28 +61,24 @@
}
&.CalendarHeatmap__cell--scale-0 {
- background: #fff;
- border: 1px solid #cdcfd3;
+ background: $pico-10;
+ cursor: default;
}
&.CalendarHeatmap__cell--scale-1 {
background: #abcaf6;
- border: 1px solid #abcaf6;
}
&.CalendarHeatmap__cell--scale-2 {
background: #77a8ef;
- border: 1px solid #abcaf6;
}
&.CalendarHeatmap__cell--scale-3 {
background: #3578e6;
- border: 1px solid #abcaf6;
}
&.CalendarHeatmap__cell--scale-4 {
background: #225ae0;
- border: 1px solid #abcaf6;
}
}
@@ -96,8 +86,12 @@
margin-top: 1em;
display: flex;
align-items: center;
- .CalendarHeatmap__cell__wrapper {
- margin-right: 0.25em;
+ justify-content: flex-end;
+ .CalendarHeatmap__cell {
+ cursor: default;
+ &__wrapper {
+ margin-right: 0.25em;
+ }
}
span {
line-height: 12px;
diff --git a/aim/web/ui/src/components/IllustrationBlock/IllustrationBlock.tsx b/aim/web/ui/src/components/IllustrationBlock/IllustrationBlock.tsx
index f20a8992de..fe10847e89 100644
--- a/aim/web/ui/src/components/IllustrationBlock/IllustrationBlock.tsx
+++ b/aim/web/ui/src/components/IllustrationBlock/IllustrationBlock.tsx
@@ -22,6 +22,7 @@ function IllustrationBlock({
type = IllustrationsEnum.ExploreData,
className = '',
size = 'small',
+ showImage = true,
}: IIllustrationBlockProps): React.FunctionComponentElement
{
const [imgLoaded, setImgLoaded] = React.useState(false);
@@ -32,19 +33,22 @@ function IllustrationBlock({
return (
-
- {image || (
-
- )}
-
+ {showImage ? (
+
+ {image || (
+
+ )}
+
+ ) : null}
+
> {
+ /**
+ * @description The info of the release note
+ * @type string
+ * @example '[feat] Add support for new metrics'
+ */
+ info: string;
+ /**
+ * @description tag name of the release note
+ * @type string
+ * @example 'v3.13.0'
+ */
+ tagName: string;
+}
diff --git a/aim/web/ui/src/components/ReleaseNoteItem/ReleaseNoteItem.scss b/aim/web/ui/src/components/ReleaseNoteItem/ReleaseNoteItem.scss
new file mode 100644
index 0000000000..8186f21865
--- /dev/null
+++ b/aim/web/ui/src/components/ReleaseNoteItem/ReleaseNoteItem.scss
@@ -0,0 +1,45 @@
+@use 'src/styles/abstracts' as *;
+
+.ReleaseNoteItem {
+ display: flex;
+ padding-left: 18px;
+ margin-bottom: $space-sm;
+ position: relative;
+ text-decoration: none;
+ p {
+ word-break: break-word;
+ }
+ &__tagName {
+ color: $primary-color;
+ font-weight: 600;
+ }
+ &::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 3px;
+ width: 7px;
+ height: 7px;
+ z-index: 1;
+ border-radius: $border-radius-circle;
+ background-color: $primary-color;
+ }
+ &::after {
+ content: '';
+ position: absolute;
+ left: 3px;
+ top: 3px;
+ height: 100%;
+ width: 1px;
+ background-color: $pico-20;
+ }
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ &:hover {
+ text-decoration: underline;
+ text-decoration-color: $text-color;
+ cursor: pointer;
+ }
+}
diff --git a/aim/web/ui/src/components/ReleaseNoteItem/ReleaseNoteItem.tsx b/aim/web/ui/src/components/ReleaseNoteItem/ReleaseNoteItem.tsx
new file mode 100644
index 0000000000..b361f140d7
--- /dev/null
+++ b/aim/web/ui/src/components/ReleaseNoteItem/ReleaseNoteItem.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+
+import { Text } from 'components/kit';
+
+import { IReleaseNoteItemProps } from './ReleaseNoteItem.d';
+
+import './ReleaseNoteItem.scss';
+
+function ReleaseNoteItem({
+ info,
+ tagName,
+ ...rest
+}: IReleaseNoteItemProps): React.FunctionComponentElement {
+ return (
+
+
+ {tagName} - {info}
+
+
+ );
+}
+
+export default React.memo(ReleaseNoteItem);
diff --git a/aim/web/ui/src/components/SelectTag/SelectTag.tsx b/aim/web/ui/src/components/SelectTag/SelectTag.tsx
index 2203044744..5be8f90170 100644
--- a/aim/web/ui/src/components/SelectTag/SelectTag.tsx
+++ b/aim/web/ui/src/components/SelectTag/SelectTag.tsx
@@ -108,7 +108,7 @@ function SelectTag({
color='primary'
className='SelectTag__createTag'
>
- Create Tag
+ Create New Tag
diff --git a/aim/web/ui/src/components/SideBar/SideBar.tsx b/aim/web/ui/src/components/SideBar/SideBar.tsx
index b0bde5c3ac..a113596bb8 100644
--- a/aim/web/ui/src/components/SideBar/SideBar.tsx
+++ b/aim/web/ui/src/components/SideBar/SideBar.tsx
@@ -42,9 +42,8 @@ function SideBar(): React.FunctionComponentElement
{
@@ -59,7 +58,7 @@ function SideBar(): React.FunctionComponentElement {
to={() => getPathFromStorage(path)}
exact={true}
isActive={(m, location) =>
- location.pathname.startsWith(path)
+ location.pathname.split('/')[1] === path.split('/')[1]
}
activeClassName={'Sidebar__NavLink--active'}
className='Sidebar__NavLink'
diff --git a/aim/web/ui/src/components/StatisticsBar/StatisticsBar.d.ts b/aim/web/ui/src/components/StatisticsBar/StatisticsBar.d.ts
new file mode 100644
index 0000000000..a55fbea766
--- /dev/null
+++ b/aim/web/ui/src/components/StatisticsBar/StatisticsBar.d.ts
@@ -0,0 +1,20 @@
+type StatisticsBarItem = {
+ percent: number;
+ color: string;
+ label?: string;
+ highlighted?: boolean;
+};
+
+export interface IStatisticsBarProps {
+ data: Array;
+ width?: number | string;
+ height?: number | string;
+ onMouseOver?: (id: string, source: string) => void;
+ onMouseLeave?: () => void;
+}
+
+export interface IBarStyle {
+ width: string;
+ left: number;
+ backgroundColor: string;
+}
diff --git a/aim/web/ui/src/components/StatisticsBar/StatisticsBar.scss b/aim/web/ui/src/components/StatisticsBar/StatisticsBar.scss
new file mode 100644
index 0000000000..641a615501
--- /dev/null
+++ b/aim/web/ui/src/components/StatisticsBar/StatisticsBar.scss
@@ -0,0 +1,27 @@
+@use 'src/styles/abstracts' as *;
+
+.StatisticsBar {
+ position: relative;
+ background-color: $pico-2;
+ border-radius: $border-radius-sm;
+ &__item {
+ display: inline-flex;
+ height: 100%;
+ position: absolute;
+ transition: all 0.18s ease-out;
+ &:first-child {
+ border-top-left-radius: $border-radius-sm;
+ border-bottom-left-radius: $border-radius-sm;
+ }
+ &:last-child {
+ border-top-right-radius: $border-radius-sm;
+ border-bottom-right-radius: $border-radius-sm;
+ }
+ &.highlighted {
+ z-index: 1;
+ height: calc(100% + 2px);
+ margin-top: -1px;
+ box-shadow: 0 0 0 2px white;
+ }
+ }
+}
diff --git a/aim/web/ui/src/components/StatisticsBar/StatisticsBar.tsx b/aim/web/ui/src/components/StatisticsBar/StatisticsBar.tsx
new file mode 100644
index 0000000000..66c91ca1c2
--- /dev/null
+++ b/aim/web/ui/src/components/StatisticsBar/StatisticsBar.tsx
@@ -0,0 +1,61 @@
+import * as React from 'react';
+import classNames from 'classnames';
+
+import { Tooltip } from '@material-ui/core';
+
+import { IBarStyle, IStatisticsBarProps } from '.';
+
+import './StatisticsBar.scss';
+
+function StatisticsBar({
+ data = [],
+ width = '100%',
+ height = 8,
+ onMouseOver,
+ onMouseLeave,
+}: IStatisticsBarProps) {
+ const onSafeMouseOver = React.useCallback(
+ (id: string) => {
+ if (typeof onMouseOver === 'function') {
+ onMouseOver(id, 'bar');
+ }
+ },
+ [onMouseOver],
+ );
+ const barStyles = React.useMemo(() => {
+ const styles: IBarStyle[] = [];
+ for (let i = 0; i < data.length; i++) {
+ const item = data[i];
+ const prevItemLeftPos = styles[i - 1]?.left || 0;
+ const prevItemPercent = data[i - 1]?.percent || 0;
+ const style = {
+ width: `${item.percent.toFixed(2)}%`,
+ left: i === 0 ? 0 : prevItemLeftPos + prevItemPercent,
+ backgroundColor: item.color,
+ };
+ styles.push(style);
+ }
+ return styles;
+ }, [data]);
+ return (
+
+ {Object.values(data).map(
+ ({ percent, color, label = '', highlighted }, i) =>
+ percent ? (
+
+ onSafeMouseOver(label)}
+ />
+
+ ) : null,
+ )}
+
+ );
+}
+
+StatisticsBar.displayName = 'StatisticsBar';
+
+export default React.memo(StatisticsBar);
diff --git a/aim/web/ui/src/components/StatisticsBar/index.ts b/aim/web/ui/src/components/StatisticsBar/index.ts
new file mode 100644
index 0000000000..353e7bbd8e
--- /dev/null
+++ b/aim/web/ui/src/components/StatisticsBar/index.ts
@@ -0,0 +1,5 @@
+import StatisticsBar from './StatisticsBar';
+
+export * from './StatisticsBar.d';
+
+export default StatisticsBar;
diff --git a/aim/web/ui/src/components/StatisticsCard/StatisticsCard.d.ts b/aim/web/ui/src/components/StatisticsCard/StatisticsCard.d.ts
new file mode 100644
index 0000000000..6994c20bcf
--- /dev/null
+++ b/aim/web/ui/src/components/StatisticsCard/StatisticsCard.d.ts
@@ -0,0 +1,16 @@
+import { IconName } from 'components/kit/Icon';
+
+export interface IStatisticsCardProps {
+ label: string;
+ count: number;
+ title?: string;
+ icon?: IconName;
+ iconBgColor?: string;
+ cardBgColor?: string;
+ onMouseOver?: (id: string, source: string) => void;
+ onMouseLeave?: () => void;
+ navLink?: string;
+ highlighted?: boolean;
+ outlined?: boolean;
+ isLoading?: boolean;
+}
diff --git a/aim/web/ui/src/components/StatisticsCard/StatisticsCard.scss b/aim/web/ui/src/components/StatisticsCard/StatisticsCard.scss
new file mode 100644
index 0000000000..0fc3095b0d
--- /dev/null
+++ b/aim/web/ui/src/components/StatisticsCard/StatisticsCard.scss
@@ -0,0 +1,56 @@
+@use 'src/styles/abstracts' as *;
+
+.StatisticsCard {
+ padding: $space-xs;
+ display: inline-flex;
+ align-items: center;
+ justify-content: flex-start;
+ border-radius: $border-radius-sm;
+ min-width: toRem(132px);
+ max-width: toRem(132px);
+ border: $border-transparent;
+ transition: all 0.18s ease-out;
+ position: relative;
+ &__iconWrapper {
+ width: 2rem;
+ min-width: 2rem;
+ height: 2rem;
+ border-radius: 50%;
+ margin-right: $space-xs;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ }
+ &__info {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ &__label,
+ &__count {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ color: $pico;
+ user-select: none;
+ }
+ &__label {
+ text-transform: capitalize;
+ }
+ }
+ &__soonBadge {
+ position: absolute;
+ top: -$space-xs;
+ right: -$space-xxs;
+ text-align: center;
+ white-space: nowrap;
+ padding: $space-xxxxxs $space-xxxs;
+ color: $pico;
+ border-radius: $border-radius-xss;
+ background-color: white;
+ box-shadow: 0 1px 2px 0 #00000029;
+ user-select: none;
+ }
+ &.highlighted {
+ cursor: pointer;
+ }
+}
diff --git a/aim/web/ui/src/components/StatisticsCard/StatisticsCard.tsx b/aim/web/ui/src/components/StatisticsCard/StatisticsCard.tsx
new file mode 100644
index 0000000000..768fd81484
--- /dev/null
+++ b/aim/web/ui/src/components/StatisticsCard/StatisticsCard.tsx
@@ -0,0 +1,97 @@
+import * as React from 'react';
+import { useHistory } from 'react-router-dom';
+import classNames from 'classnames';
+
+import { Icon, Text } from 'components/kit';
+
+import hexToRgbA from 'utils/hexToRgbA';
+
+import { IStatisticsCardProps } from './index';
+
+import './StatisticsCard.scss';
+
+function StatisticsCard({
+ label,
+ title = '',
+ count,
+ icon,
+ iconBgColor = '#000000',
+ cardBgColor = hexToRgbA(iconBgColor, 0.1),
+ onMouseOver,
+ onMouseLeave,
+ navLink,
+ highlighted,
+ outlined = false,
+ isLoading = false,
+}: IStatisticsCardProps) {
+ const history = useHistory();
+ const onSafeMouseOver = React.useCallback(
+ (id: string) => {
+ if (typeof onMouseOver === 'function') {
+ onMouseOver(id, 'card');
+ }
+ },
+ [onMouseOver],
+ );
+ const styles = {
+ card: {
+ borderColor: outlined ? iconBgColor : 'transparent',
+ backgroundColor: highlighted ? iconBgColor : cardBgColor,
+ },
+ iconWrapper: {
+ backgroundColor: highlighted ? '#fff' : iconBgColor,
+ },
+ iconColor: highlighted ? iconBgColor : '#fff',
+ label: highlighted ? { color: '#fff' } : {},
+ count: highlighted ? { color: '#fff' } : {},
+ };
+ return (
+ navLink && history.push(navLink)}
+ onMouseLeave={onMouseLeave}
+ onMouseOver={() => onSafeMouseOver(label)}
+ className={classNames('StatisticsCard', { highlighted })}
+ style={styles.card}
+ >
+ {!navLink && (
+
+ Coming soon
+
+ )}
+
+ {icon && (
+
+
+
+ )}
+
+
+ {label}
+
+
+
+ {isLoading ? '--' : count}
+
+
+
+ );
+}
+
+StatisticsCard.displayName = 'StatisticsCard';
+
+export default React.memo(StatisticsCard);
diff --git a/aim/web/ui/src/components/StatisticsCard/index.ts b/aim/web/ui/src/components/StatisticsCard/index.ts
new file mode 100644
index 0000000000..e957ea8654
--- /dev/null
+++ b/aim/web/ui/src/components/StatisticsCard/index.ts
@@ -0,0 +1,5 @@
+import StatisticsCard from './StatisticsCard';
+
+export * from './StatisticsCard.d';
+
+export default StatisticsCard;
diff --git a/aim/web/ui/src/components/Table/Table.tsx b/aim/web/ui/src/components/Table/Table.tsx
index 43faf97d7e..a1b6f9fc72 100644
--- a/aim/web/ui/src/components/Table/Table.tsx
+++ b/aim/web/ui/src/components/Table/Table.tsx
@@ -4,6 +4,7 @@
import React from 'react';
import { isEmpty, isEqual, isNil } from 'lodash-es';
import { useResizeObserver } from 'hooks';
+import _ from 'lodash-es';
import { Button, Icon, Text } from 'components/kit';
import ControlPopover from 'components/ControlPopover/ControlPopover';
@@ -92,6 +93,7 @@ const Table = React.forwardRef(function Table(
columnsColorScales,
onRowsVisibilityChange,
visualizationElementType,
+ noColumnActions,
...props
}: ITableProps,
ref,
@@ -529,7 +531,7 @@ const Table = React.forwardRef(function Table(
const rightPane = tableContainerRef.current?.querySelector(
'.Table__pane--right',
);
- let availableSpace = 0;
+ let availableSpace = tableContainerRef.current?.offsetWidth ?? 0;
if (leftPane || rightPane) {
availableSpace =
@@ -743,6 +745,14 @@ const Table = React.forwardRef(function Table(
}
}, [appName, sameValueColumns, hiddenColumns]);
+ const selectedRunsQuery: string = React.useMemo(() => {
+ if (!_.isEmpty(selectedRows)) {
+ return `run.hash in [${_.uniq(
+ Object.values(selectedRows)?.map((row: any) => `"${row.runHash}"`),
+ ).join(',')}]`;
+ }
+ }, [selectedRows]);
+
// The right check is !props.isInfiniteLoading && (isLoading || isNil(rowData))
// but after setting isInfiniteLoading to true, the rowData becomes null, unnecessary renders happening
// @TODO sanitize this point
@@ -852,7 +862,7 @@ const Table = React.forwardRef(function Table(
)}
- ) : !isEmpty(selectedRows) && multiSelect ? (
+ ) : !hideHeaderActions && !isEmpty(selectedRows) && multiSelect ? (
@@ -947,7 +957,7 @@ const Table = React.forwardRef(function Table(
@@ -983,13 +993,16 @@ const Table = React.forwardRef(function Table(
columns={columnsData.filter((col) => !col.isHidden)}
onGroupExpandToggle={onGroupExpandToggle}
onRowHover={rowHoverHandler}
- onRowClick={rowClickHandler}
+ onRowClick={
+ showRowClickBehaviour ? rowClickHandler : undefined
+ }
listWindow={listWindow}
multiSelect={multiSelect}
selectedRows={selectedRows || {}}
onRowSelect={onRowSelect}
columnsColorScales={columnsColorScales}
onToggleColumnsColorScales={onToggleColumnsColorScales}
+ noColumnActions={noColumnActions}
{...props}
/>
@@ -1067,6 +1080,7 @@ const Table = React.forwardRef(function Table(
size={illustrationConfig?.size || 'xLarge'}
content={illustrationConfig?.content || ''}
title={illustrationConfig?.title || ''}
+ showImage={illustrationConfig?.showImage}
/>
)}
diff --git a/aim/web/ui/src/components/kit/Badge/Badge.d.ts b/aim/web/ui/src/components/kit/Badge/Badge.d.ts
index 9eba07d2e3..8f2321d55d 100644
--- a/aim/web/ui/src/components/kit/Badge/Badge.d.ts
+++ b/aim/web/ui/src/components/kit/Badge/Badge.d.ts
@@ -2,7 +2,8 @@ import React from 'react';
import { IconName } from '../Icon/Icon.d';
-export interface IBadgeProps {
+export interface IBadgeProps
+ extends Partial
> {
id?: string;
label: string;
value?: string;
diff --git a/aim/web/ui/src/components/kit/DataList/DataList.d.ts b/aim/web/ui/src/components/kit/DataList/DataList.d.ts
index 4c2abfcb27..ec9ebbbd1a 100644
--- a/aim/web/ui/src/components/kit/DataList/DataList.d.ts
+++ b/aim/web/ui/src/components/kit/DataList/DataList.d.ts
@@ -1,3 +1,7 @@
+import React from 'react';
+
+import { IIllustrationConfig } from 'types/components/Table/Table';
+
export interface IDataListProps {
tableRef: React.RefObject;
tableData: any;
@@ -9,4 +13,6 @@ export interface IDataListProps {
rowHeight?: number;
height?: string;
tableClassName?: string;
+ toolbarItems?: React.FunctionComponentElement[];
+ disableMatchBar?: boolean;
}
diff --git a/aim/web/ui/src/components/kit/DataList/DataList.scss b/aim/web/ui/src/components/kit/DataList/DataList.scss
index 40548b861a..6934fe7772 100644
--- a/aim/web/ui/src/components/kit/DataList/DataList.scss
+++ b/aim/web/ui/src/components/kit/DataList/DataList.scss
@@ -5,6 +5,12 @@
flex-direction: column;
width: 100%;
max-height: 100%;
+ &__toolbarItems {
+ height: 2rem;
+ margin-left: $space-md;
+ display: flex;
+ align-items: center;
+ }
&__textsTable {
border: $border-grey;
border-radius: $border-radius-md;
@@ -55,7 +61,7 @@
&-cell {
position: relative;
- min-height: 28px;
+ min-height: 24px;
max-height: 300px;
padding: 0 $space-unit;
border-right: unset;
diff --git a/aim/web/ui/src/components/kit/DataList/DataList.tsx b/aim/web/ui/src/components/kit/DataList/DataList.tsx
index 22f09e1816..f3d58e38cd 100644
--- a/aim/web/ui/src/components/kit/DataList/DataList.tsx
+++ b/aim/web/ui/src/components/kit/DataList/DataList.tsx
@@ -31,6 +31,8 @@ function DataList({
rowHeight = 28,
height = '100vh',
tableClassName = '',
+ toolbarItems = [],
+ disableMatchBar = false,
}: IDataListProps): React.FunctionComponentElement {
const textSearch = useTextSearch({
rawData: tableData,
@@ -50,11 +52,11 @@ function DataList({
const reg = new RegExp(regex?.source ?? '', regex?.flags);
highlightedItem[searchableKey] =
regex === null
- ? item[searchableKey]
- : item[searchableKey]
- .split(regex)
- .filter((part: string) => part !== '')
- .map((part: string, i: number) => {
+ ? `${item[searchableKey]}`
+ : `${item[searchableKey]}`
+ ?.split(regex)
+ ?.filter((part: string) => part !== '')
+ ?.map((part: string, i: number) => {
return reg.test(part) ? (
{part}
@@ -77,15 +79,19 @@ function DataList({
return (
{withSearchBar && (
-
+
+
+
)}
@@ -34,75 +36,95 @@ function SearchBar({
isValidInput={isValidInput}
isDisabled={isDisabled}
/>
-
-
-
- {
- onMatchTypeChange(
- matchType === MatchTypes.Case ? null : MatchTypes.Case,
- );
- }}
- >
-
-
-
-
-
-
-
{
- onMatchTypeChange(
- matchType === MatchTypes.Word ? null : MatchTypes.Word,
- );
- }}
+ {disableMatchBar ? null : (
+
+
+
-
-
-
-
-
-
- {
+ onMatchTypeChange(
+ matchType === MatchTypes.Case ? null : MatchTypes.Case,
+ );
+ }}
+ >
+
+
+
+
+
+ {
- onMatchTypeChange(
- matchType === MatchTypes.RegExp ? null : MatchTypes.RegExp,
- );
- }}
>
-
-
-
-
-
+ {
+ onMatchTypeChange(
+ matchType === MatchTypes.Word ? null : MatchTypes.Word,
+ );
+ }}
+ >
+
+
+
+
+
+
+ {
+ onMatchTypeChange(
+ matchType === MatchTypes.RegExp
+ ? null
+ : MatchTypes.RegExp,
+ );
+ }}
+ >
+
+
+
+
+
+ )}
+
+ {!!toolbarItems?.length && (
+ {toolbarItems}
+ )}
);
diff --git a/aim/web/ui/src/components/kit/DataList/SearchBar/SearchInput/index.tsx b/aim/web/ui/src/components/kit/DataList/SearchBar/SearchInput/index.tsx
index cdf7417ab3..703a6bd758 100644
--- a/aim/web/ui/src/components/kit/DataList/SearchBar/SearchInput/index.tsx
+++ b/aim/web/ui/src/components/kit/DataList/SearchBar/SearchInput/index.tsx
@@ -24,14 +24,14 @@ function SearchInput({
className={classNames('SearchInput', { activeCloseButton: !!value })}
>
- Search for text
+ Search
}
style={{
- height: 32,
+ height: 28,
}}
/>
diff --git a/aim/web/ui/src/components/kit/DataList/SearchBar/types.d.ts b/aim/web/ui/src/components/kit/DataList/SearchBar/types.d.ts
index 7b4bba8693..5eea84e2d5 100644
--- a/aim/web/ui/src/components/kit/DataList/SearchBar/types.d.ts
+++ b/aim/web/ui/src/components/kit/DataList/SearchBar/types.d.ts
@@ -6,6 +6,8 @@ export interface ISearchBarProps {
onInputClear: () => void;
onMatchTypeChange: (value: MatchTypes | null) => void;
isDisabled: boolean;
+ toolbarItems?: React.FunctionComponentElement[];
+ disableMatchBar?: boolean;
}
export interface ISearchInputProps {
diff --git a/aim/web/ui/src/components/kit/Icon/Icon.d.ts b/aim/web/ui/src/components/kit/Icon/Icon.d.ts
index 128a11ba64..a83274b44f 100644
--- a/aim/web/ui/src/components/kit/Icon/Icon.d.ts
+++ b/aim/web/ui/src/components/kit/Icon/Icon.d.ts
@@ -28,6 +28,9 @@ export type IconName =
| 'pin-right'
| 'pin-left'
| 'pin'
+ | 'pin-to-top'
+ | 'flexible'
+ | 'pin-to-bottom'
| 'expand-horizontal'
| 'expand-vertical'
| 'arrow-up'
@@ -147,10 +150,11 @@ export type IconName =
| 'figures-explorer'
| 'group-column'
| 'image-group'
+ | 'partially-selected'
| 'arrow-left-contained'
| 'arrow-up-contained'
| 'arrow-right-contained'
| 'arrow-down-contained'
- | 'pin-to-top'
- | 'pin-to-bottom'
- | 'flexible';
+ | 'audio'
+ | 'distributions'
+ | 'dashboard';
diff --git a/aim/web/ui/src/components/kit/ListItem/Index.ts b/aim/web/ui/src/components/kit/ListItem/Index.ts
new file mode 100644
index 0000000000..3320afcbbc
--- /dev/null
+++ b/aim/web/ui/src/components/kit/ListItem/Index.ts
@@ -0,0 +1,5 @@
+import ListItem from './ListItem';
+
+export * from './ListItem';
+
+export default ListItem;
diff --git a/aim/web/ui/src/components/kit/ListItem/ListItem.d.ts b/aim/web/ui/src/components/kit/ListItem/ListItem.d.ts
new file mode 100644
index 0000000000..6d8fa4a2d4
--- /dev/null
+++ b/aim/web/ui/src/components/kit/ListItem/ListItem.d.ts
@@ -0,0 +1,11 @@
+import React from 'react';
+
+export interface IListItemProps
+ extends Partial> {
+ className?: string;
+ children?: React.ReactNode;
+ size?: IListITemSize;
+ onClick?: (event: React.MouseEvent) => void;
+}
+
+export type IListITemSize = 'small' | 'medium' | 'large';
diff --git a/aim/web/ui/src/components/kit/ListItem/ListItem.tsx b/aim/web/ui/src/components/kit/ListItem/ListItem.tsx
new file mode 100644
index 0000000000..af40f8d64d
--- /dev/null
+++ b/aim/web/ui/src/components/kit/ListItem/ListItem.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import styled from 'styled-components';
+
+import { IListItemProps, IListITemSize } from './ListItem.d';
+
+const heights: { small: string; medium: string; large: string } = {
+ small: '24px',
+ medium: '28px',
+ large: '32px',
+};
+
+const Container = styled.div`
+ display: flex;
+ align-items: center;
+ height: ${({ size }) => heights[size as IListITemSize]};
+ padding: 0 0.75rem;
+ border-radius: 0.25rem;
+ transition: all 0.18s ease-out;
+ cursor: pointer;
+ &:hover {
+ background-color: #f4f4f6;
+ color: #1473e6;
+ .Text {
+ color: #1473e6;
+ }
+ }
+ .Text {
+ max-width: 100%;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+`;
+
+function ListItem({
+ className = '',
+ size = 'medium',
+ children,
+ onClick,
+ ...rest
+}: IListItemProps) {
+ return (
+ null}
+ className={`ListItem ${className}`}
+ size={size}
+ {...rest}
+ >
+ {children}
+
+ );
+}
+
+export default React.memo(ListItem);
diff --git a/aim/web/ui/src/config/analytics/analyticsKeysMap.ts b/aim/web/ui/src/config/analytics/analyticsKeysMap.ts
index ac495bb00a..becb3b4124 100644
--- a/aim/web/ui/src/config/analytics/analyticsKeysMap.ts
+++ b/aim/web/ui/src/config/analytics/analyticsKeysMap.ts
@@ -317,14 +317,17 @@ export const ANALYTICS_EVENT_KEYS = {
pageView: '[BookmarksPage] Page view',
view: '[BookmarksPage] View bookmark',
},
- home: {
- pageView: '[HomePage] Page view',
- activityCellClick: '[HomePage] Click on Activity cell',
- createGithubIssue: '[HomePage] Click on create gitHub issue',
- slackCommunity: '[HomePage] Click on Join Aim slack community',
- docs: '[HomePage] Click on documentation icon',
- colab: '[HomePage] Click on colab notebook icon',
- liveDemo: '[HomePage] Click on Live demo icon',
+ dashboard: {
+ pageView: '[DashboardPage] Page view',
+ activityCellClick: '[DashboardPage] Click on Activity cell',
+ createGithubIssue: '[DashboardPage] Click on create gitHub issue',
+ slackCommunity: '[DashboardPage] Click on Join Aim slack community',
+ docs: '[DashboardPage] Click on documentation icon',
+ colab: '[DashboardPage] Click on colab notebook icon',
+ liveDemo: '[DashboardPage] Click on Live demo icon',
+ table: {
+ compareSelectedRuns: '[MetricsExplorer][Table] Compare selected runs',
+ },
},
sidebar: {
slack: '[SideBar] Click on slack community link',
diff --git a/aim/web/ui/src/config/dates/dates.ts b/aim/web/ui/src/config/dates/dates.ts
index a3b2e6fa9b..b28f0065ea 100644
--- a/aim/web/ui/src/config/dates/dates.ts
+++ b/aim/web/ui/src/config/dates/dates.ts
@@ -5,3 +5,6 @@ export const DATE_GIT_COMMIT = 'DD MMMM YYYY HH:MM A';
export const TABLE_DATE_FORMAT = 'HH:mm:ss · DD MMM, YY';
export const DATE_CHART_TICK = 'HH:mm:ss DD MMM, YY';
export const DATE_QUERY_FORMAT = 'YYYY, M, D';
+export const CONTRIBUTION_DAY_FORMAT = 'DD_MMM_YYYY';
+export const CONTRIBUTION_MONTH_FORMAT = 'MMMM_YYYY';
+export const CONTRIBUTION_TIME_FORMAT = 'HH:mm:ss A';
diff --git a/aim/web/ui/src/config/enums/routesEnum.ts b/aim/web/ui/src/config/enums/routesEnum.ts
index be156bbceb..2037fbc158 100644
--- a/aim/web/ui/src/config/enums/routesEnum.ts
+++ b/aim/web/ui/src/config/enums/routesEnum.ts
@@ -1,5 +1,5 @@
enum PathEnum {
- Home = '/',
+ Dashboard = '/',
Runs = '/runs',
Metrics = '/metrics',
Metrics_Id = '/metrics/:appId',
diff --git a/aim/web/ui/src/config/pageTitles/pageTitles.ts b/aim/web/ui/src/config/pageTitles/pageTitles.ts
index f53304a28b..cd78f148c6 100644
--- a/aim/web/ui/src/config/pageTitles/pageTitles.ts
+++ b/aim/web/ui/src/config/pageTitles/pageTitles.ts
@@ -1,5 +1,5 @@
const pageTitles = {
- HOME: '',
+ DASHBOARD: '',
RUNS_EXPLORER: 'Runs Explorer',
METRICS_EXPLORER: 'Metrics Explorer',
PARAMS_EXPLORER: 'Params Explorer',
diff --git a/aim/web/ui/src/config/references/index.ts b/aim/web/ui/src/config/references/index.ts
index eb73328cfe..b3b1f23986 100644
--- a/aim/web/ui/src/config/references/index.ts
+++ b/aim/web/ui/src/config/references/index.ts
@@ -1,7 +1,11 @@
+import { AIM_VERSION } from 'config/config';
+
const DOCUMENTATIONS = {
MAIN_PAGE: 'https://aimstack.readthedocs.io',
STABLE: 'https://aimstack.readthedocs.io/en/stable/',
AIM_QL: 'https://aimstack.readthedocs.io/en/latest/using/search.html',
+ SUPPORTED_TYPES:
+ 'https://aimstack.readthedocs.io/en/latest/quick_start/supported_types.html',
EXPLORERS: {
PARAMS: {
MAIN: 'https://aimstack.readthedocs.io/en/latest/ui/pages/explorers.html#params-explorer',
@@ -35,6 +39,26 @@ const DOCUMENTATIONS = {
'https://aimstack.readthedocs.io/en/latest/ui/pages/explorers.html',
},
},
+ INTEGRATIONS: {
+ PYTORCH_LIGHTNING:
+ 'https://aimstack.readthedocs.io/en/latest/quick_start/integrations.html#integration-with-pytorch-lightning',
+ HUGGING_FACE:
+ 'https://aimstack.readthedocs.io/en/latest/quick_start/integrations.html#integration-with-hugging-face',
+ KERAS:
+ 'https://aimstack.readthedocs.io/en/latest/quick_start/integrations.html#integration-with-keras-tf-keras',
+ KERAS_TUNER:
+ 'https://aimstack.readthedocs.io/en/latest/quick_start/integrations.html#integration-with-kerastuner',
+ XGBOOST:
+ 'https://aimstack.readthedocs.io/en/latest/quick_start/integrations.html#integration-with-xgboost',
+ CATBOOST:
+ 'https://aimstack.readthedocs.io/en/latest/quick_start/integrations.html#integration-with-catboost',
+ FASTAI:
+ 'https://aimstack.readthedocs.io/en/latest/quick_start/integrations.html#integration-with-fastai',
+ LIGHT_GBM:
+ 'https://aimstack.readthedocs.io/en/latest/quick_start/integrations.html#integration-with-lightgbm',
+ PYTORCH_IGNITE:
+ 'https://aimstack.readthedocs.io/en/latest/quick_start/integrations.html#integration-with-pytorch-ignite',
+ },
};
const DEMOS = {
@@ -48,4 +72,81 @@ const GUIDES = {
},
};
-export { DOCUMENTATIONS, GUIDES, DEMOS };
+/*
+ getDocsVersion() returns the version of the docs to be used in the links
+ */
+function getDocsVersion() {
+ let [majorVersion, minorVersion] = `${AIM_VERSION}`.split('.');
+ return `v${majorVersion}.${minorVersion}.0`;
+}
+
+const version: string = getDocsVersion();
+
+const DASHBOARD_PAGE_GUIDES: { name: string; url: string }[] = [
+ {
+ name: 'UI Runs Management',
+ url: `https://aimstack.readthedocs.io/en/${version}/ui/pages/run_management.html`,
+ },
+ {
+ name: 'UI Explorers',
+ url: `https://aimstack.readthedocs.io/en/${version}/ui/pages/explorers.html`,
+ },
+ {
+ name: 'UI Bookmarks',
+ url: `https://aimstack.readthedocs.io/en/${version}/ui/pages/bookmarks.html`,
+ },
+ {
+ name: 'UI Tags page',
+ url: `https://aimstack.readthedocs.io/en/${version}/ui/pages/tags.html`,
+ },
+ {
+ name: 'Manage runs',
+ url: `https://aimstack.readthedocs.io/en/${version}/using/manage_runs.html`,
+ },
+ {
+ name: 'Configure runs',
+ url: `https://aimstack.readthedocs.io/en/${version}/using/configure_runs.html`,
+ },
+ {
+ name: 'Query runs and objects',
+ url: `https://aimstack.readthedocs.io/en/${version}/using/query_runs.html`,
+ },
+ {
+ name: 'Query language basics',
+ url: `https://aimstack.readthedocs.io/en/${version}/using/search.html`,
+ },
+ {
+ name: 'Track experiments with aim remote server',
+ url: `https://aimstack.readthedocs.io/en/${version}/using/remote_tracking.html`,
+ },
+ {
+ name: 'Host Aim on Kubernetes (K8S)',
+ url: `https://aimstack.readthedocs.io/en/${version}/using/k8s_deployment.html`,
+ },
+ {
+ name: 'Run Aim UI on Jupyter Notebook',
+ url: `https://aimstack.readthedocs.io/en/${version}/using/jupyter_notebook_ui.html`,
+ },
+ {
+ name: 'Run Aim UI on SageMaker Notebook instance',
+ url: `https://aimstack.readthedocs.io/en/${version}/using/sagemaker_notebook_ui.html`,
+ },
+ {
+ name: 'Integration guides',
+ url: `https://aimstack.readthedocs.io/en/${version}/using/integration_guides.html`,
+ },
+ {
+ name: 'Data storage - where Aim data is collected',
+ url: `https://aimstack.readthedocs.io/en/${version}/understanding/data_storage.html`,
+ },
+ {
+ name: 'Storage indexing - how Aim data is indexed',
+ url: `https://aimstack.readthedocs.io/en/${version}/understanding/storage_indexing.html`,
+ },
+ {
+ name: 'Concepts',
+ url: `https://aimstack.readthedocs.io/en/${version}/understanding/concepts.html`,
+ },
+];
+
+export { DOCUMENTATIONS, GUIDES, DEMOS, DASHBOARD_PAGE_GUIDES };
diff --git a/aim/web/ui/src/config/table/tableConfigs.ts b/aim/web/ui/src/config/table/tableConfigs.ts
index 36a5ffb76c..6ea2147cd0 100644
--- a/aim/web/ui/src/config/table/tableConfigs.ts
+++ b/aim/web/ui/src/config/table/tableConfigs.ts
@@ -143,4 +143,5 @@ export const EXPLORE_SELECTED_RUNS_CONFIG: {
AppNameEnum.METRICS,
],
[AppNameEnum.IMAGES]: [AppNameEnum.RUNS, AppNameEnum.METRICS],
+ dashboard: [AppNameEnum.RUNS, AppNameEnum.METRICS, AppNameEnum.IMAGES],
};
diff --git a/aim/web/ui/src/hooks/useCodeHighlighter.ts b/aim/web/ui/src/hooks/useCodeHighlighter.ts
new file mode 100644
index 0000000000..94e4ab4c2a
--- /dev/null
+++ b/aim/web/ui/src/hooks/useCodeHighlighter.ts
@@ -0,0 +1,34 @@
+import React from 'react';
+
+import { useMonaco } from '@monaco-editor/react';
+
+import { getMonacoConfig } from 'config/monacoConfig/monacoConfig';
+
+function useCodeHighlighter(language: string = 'python') {
+ const monaco = useMonaco();
+ const preRef = React.useRef(null);
+
+ const monacoConfig: Record =
+ React.useMemo(() => {
+ return getMonacoConfig();
+ }, []);
+
+ React.useEffect(() => {
+ monacoConfig.theme.config.colors = {
+ ...monacoConfig.theme.config.colors,
+ 'editor.background': '#f2f3f4',
+ };
+ if (monaco && preRef.current) {
+ monaco.editor.colorizeElement(preRef.current, { theme: language });
+ monaco.editor.defineTheme(
+ monacoConfig.theme.name,
+ monacoConfig.theme.config,
+ );
+ monaco.editor.setTheme(monacoConfig.theme.name);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [monaco]);
+ return { elementRef: preRef };
+}
+
+export default useCodeHighlighter;
diff --git a/aim/web/ui/src/modules/core/api/dashboardsApi/index.ts b/aim/web/ui/src/modules/core/api/dashboardsApi/index.ts
new file mode 100644
index 0000000000..fa44d06848
--- /dev/null
+++ b/aim/web/ui/src/modules/core/api/dashboardsApi/index.ts
@@ -0,0 +1,77 @@
+import { getAPIHost } from 'config/config';
+
+import ENDPOINTS from 'services/api/endpoints';
+import NetworkService from 'services/NetworkService';
+
+import { IDashboardData, IDashboardRequestBody } from './types';
+
+const api = new NetworkService(`${getAPIHost()}${ENDPOINTS.DASHBOARDS.BASE}`);
+
+/**
+ * function fetchDashboardsList
+ * this call is used for fetching dashboards list.
+ * @returns {Promise}
+ */
+async function fetchDashboardsList(): Promise {
+ return (await api.makeAPIGetRequest(ENDPOINTS.DASHBOARDS.GET)).body;
+}
+
+/**
+ * function fetchDashboard
+ * this call is used for fetching a dashboard by id.
+ * @param id - id of dashboard
+ * @returns {Promise}
+ */
+async function fetchDashboard(id: string): Promise {
+ return (await api.makeAPIGetRequest(`${ENDPOINTS.DASHBOARDS.GET}/${id}`))
+ .body;
+}
+
+/**
+ * function createDashboard
+ * this call is used for creating new dashboard.
+ * @param reqBody - query body params
+ * @returns {Promise}
+ */
+async function createDashboard(
+ reqBody: IDashboardRequestBody,
+): Promise {
+ return (
+ await api.makeAPIPostRequest(ENDPOINTS.DASHBOARDS.CREATE, {
+ body: reqBody,
+ })
+ ).body;
+}
+
+/**
+ * function updateDashboard
+ * this call is used for updating a dashboard by id.
+ * @param id - id of dashboard
+ * @param reqBody - query body params
+ * @returns {Promise}
+ */
+async function updateDashboard(
+ id: string,
+ reqBody: IDashboardRequestBody,
+): Promise {
+ return (await api.makeAPIPutRequest(`/${id}`, { body: reqBody })).body;
+}
+
+/**
+ * function deleteDashboard
+ * this call is used for deleting a dashboard by id.
+ * @param id - id of dashboard
+ * @returns {Promise}
+ */
+async function deleteDashboard(id: string): Promise {
+ return api.makeAPIDeleteRequest(`/${id}`);
+}
+
+export {
+ fetchDashboardsList,
+ fetchDashboard,
+ createDashboard,
+ updateDashboard,
+ deleteDashboard,
+};
+export * from './types';
diff --git a/aim/web/ui/src/modules/core/api/dashboardsApi/types.ts b/aim/web/ui/src/modules/core/api/dashboardsApi/types.ts
new file mode 100644
index 0000000000..15cee13486
--- /dev/null
+++ b/aim/web/ui/src/modules/core/api/dashboardsApi/types.ts
@@ -0,0 +1,65 @@
+export interface IDashboardRequestBody {
+ /**
+ * The name of the dashboard
+ * @type {string}
+ * @example 'My Dashboard'
+ */
+ name: string;
+ /**
+ * The description of the dashboard
+ * @type string
+ * @example 'This is a description'
+ */
+ description: string;
+ /**
+ * The app_id which the dashboard belongs to
+ * @type {string}
+ * @optional
+ */
+ app_id?: string;
+}
+
+export interface IDashboardData {
+ /**
+ * The app id which the dashboard belongs to
+ * @type {string}
+ * @example '5e9f1b9b-7c1a-4b5a-8f0c-8c1c1b9b7c1a'
+ */
+ app_id: string;
+ /**
+ * The timestamp of the dashboard creation
+ * @type {string}
+ * @example '2020-01-01T00:00:00.000Z'
+ */
+ created_at: string;
+ /**
+ * The id of the dashboard
+ * @type {string}
+ * @example '5e9f1b9b-7c1a-4b5a-8f0c-8c1c1b9b7c1a'
+ */
+ id: string;
+ /**
+ * The name of the dashboard
+ * @type {string}
+ * @example 'My Dashboard'
+ */
+ name: string;
+ /**
+ * The description of the dashboard
+ * @type string
+ * @example 'This is a description'
+ */
+ description: string;
+ /**
+ * The timestamp of the dashboard update
+ * @type {string}
+ * @example '2020-01-01T00:00:00.000Z'
+ */
+ updated_at: string;
+ /**
+ * the app name which the dashboard belongs to
+ * @type {string}
+ * @example 'metrics'
+ */
+ app_type: 'metrics' | 'params' | 'images' | 'scatters';
+}
diff --git a/aim/web/ui/src/modules/core/api/experimentsApi/index.ts b/aim/web/ui/src/modules/core/api/experimentsApi/index.ts
new file mode 100644
index 0000000000..70857b9c76
--- /dev/null
+++ b/aim/web/ui/src/modules/core/api/experimentsApi/index.ts
@@ -0,0 +1,102 @@
+import { getAPIHost } from 'config/config';
+
+import ENDPOINTS from 'services/api/endpoints';
+import NetworkService from 'services/NetworkService';
+
+import { IExperimentData } from './types';
+
+const api = new NetworkService(`${getAPIHost()}${ENDPOINTS.EXPERIMENTS.BASE}`);
+
+/**
+ * function getExperiments
+ * this call is used for fetching experiments data.
+ * @returns {Promise}
+ */
+async function getExperiments(): Promise {
+ return (await api.makeAPIGetRequest(ENDPOINTS.EXPERIMENTS.GET)).body;
+}
+
+/**
+ * function searchExperiments
+ * this call is used for searching experiment data.
+ * @param query - query string
+ * @returns {Promise}
+ */
+async function searchExperiment(query: string): Promise {
+ return (
+ await api.makeAPIGetRequest(`${ENDPOINTS.EXPERIMENTS.SEARCH}=${query}`)
+ ).body;
+}
+
+/**
+ * function getExperimentById
+ * this call is used for fetching experiment data by id.
+ * @param id - experiment id
+ * @returns {Promise}
+ */
+async function getExperimentById(id: string): Promise {
+ return (await api.makeAPIGetRequest(`${ENDPOINTS.EXPERIMENTS.GET}/${id}`))
+ .body;
+}
+
+/**
+ * function getExperimentById
+ * this call is used for updating experiment data by id.
+ * @param id - experiment id
+ * @param reqBody - query body params
+ * @returns {Promise}
+ */
+async function updateExperimentById(
+ reqBody: { name?: string; archived?: boolean },
+ id: string,
+): Promise<{ status: string; id: string }> {
+ return (
+ await api.makeAPIPutRequest(`${ENDPOINTS.EXPERIMENTS.GET}/${id}`, {
+ body: reqBody,
+ })
+ ).body;
+}
+
+/**
+ * function getExperimentById
+ * this call is used for updating experiment data by id.
+ * @param reqBody - query body params
+ * @returns {Promise}
+ */
+async function createExperiment(reqBody: {
+ name: string;
+}): Promise<{ id: string; status: string }> {
+ return (
+ await api.makeAPIPostRequest(ENDPOINTS.EXPERIMENTS.CREATE, {
+ body: reqBody,
+ })
+ ).body;
+}
+
+/**
+ * function getExperimentById
+ * this call is used for updating experiment data by id.
+ * @param { id, params } - query params
+ * @returns {Promise}
+ */
+async function getRunsOfExperiment(
+ id: string,
+ params: { limit: number; offset?: string } = { limit: 10 },
+) {
+ return await api.makeAPIGetRequest(
+ `${ENDPOINTS.EXPERIMENTS.GET}/${id}/runs`,
+ {
+ query_params: params,
+ },
+ );
+}
+
+export {
+ getExperiments,
+ searchExperiment,
+ getExperimentById,
+ updateExperimentById,
+ createExperiment,
+ getRunsOfExperiment,
+};
+export * from './types';
diff --git a/aim/web/ui/src/modules/core/api/experimentsApi/types.ts b/aim/web/ui/src/modules/core/api/experimentsApi/types.ts
new file mode 100644
index 0000000000..d2075f4c3b
--- /dev/null
+++ b/aim/web/ui/src/modules/core/api/experimentsApi/types.ts
@@ -0,0 +1,22 @@
+/**
+ * interface IExperimentData
+ * the experiment data interface
+ */
+export interface IExperimentData {
+ /**
+ * The id of the experiment
+ */
+ id: string;
+ /**
+ * The name of the experiment
+ */
+ name: string;
+ /**
+ * is the experiment archived
+ */
+ archived: boolean;
+ /**
+ * The attached runs of the experiment
+ */
+ run_count: number;
+}
diff --git a/aim/web/ui/src/modules/core/api/projectApi/index.ts b/aim/web/ui/src/modules/core/api/projectApi/index.ts
index 64d9830c01..1f820e1f0c 100644
--- a/aim/web/ui/src/modules/core/api/projectApi/index.ts
+++ b/aim/web/ui/src/modules/core/api/projectApi/index.ts
@@ -6,7 +6,7 @@ import NetworkService from 'services/NetworkService';
import {
GetParamsQueryOptions,
GetParamsResult,
- GetActivityResult,
+ GetProjectContributionsResult,
} from './types';
const api = new NetworkService(`${getAPIHost()}${ENDPOINTS.PROJECTS.BASE}`);
@@ -27,12 +27,12 @@ async function getParams(
}
/**
- * function getActivity
- * this call is used from home page to show activity data
+ * function getProjectContributions
+ * this call is used from DashboardPage page to show project contributions data
*/
-async function getActivity(): Promise {
+async function getProjectContributions(): Promise {
return (await api.makeAPIGetRequest(ENDPOINTS.PROJECTS.GET_ACTIVITY)).body;
}
-export { getParams, getActivity };
+export { getParams, getProjectContributions };
export * from './types';
diff --git a/aim/web/ui/src/modules/core/api/projectApi/types.ts b/aim/web/ui/src/modules/core/api/projectApi/types.ts
index 72512e41f2..75add89ce1 100644
--- a/aim/web/ui/src/modules/core/api/projectApi/types.ts
+++ b/aim/web/ui/src/modules/core/api/projectApi/types.ts
@@ -1,4 +1,4 @@
-import { SequenceTypesEnum } from 'types/core/enums';
+import { SequenceTypesUnion } from 'types/core/enums';
import { Context } from 'types/core/shared';
/**
@@ -7,9 +7,13 @@ import { Context } from 'types/core/shared';
*/
export type GetParamsQueryOptions = {
/**
- * Sequence name one of 'metric' | 'distributions' | 'images' | 'figures' | 'audio' etc.
+ * Sequence: array of sequence names or one of 'metric' | 'distributions' | 'images' | 'figures' | 'audio' etc. .
*/
- sequence: SequenceTypesEnum;
+ sequence: SequenceTypesUnion | SequenceTypesUnion[];
+ /**
+ * Exclude 'params' from the response
+ */
+ exclude_params?: boolean;
};
/**
@@ -51,7 +55,7 @@ export type GetParamsResult = {
* ```
* This record includes high level params of run, system defined params like __system_params, environment variables etc.
*/
- params: Record;
+ params?: Record;
/**
* Context of tracked metrics sequences by passing name of sequence as`metric`
* This generates by calling
@@ -106,19 +110,27 @@ export type GetParamsResult = {
};
/**
- * type GetActivityResult
+ * type GetProjectContributionsResult
* The response type of GET /projects/activity
* This data is used by autosuggestions etc.
*/
-export type GetActivityResult = {
+export type GetProjectContributionsResult = {
/**
* Total number of experiments in a single repo/storage/project (.aim directory)
*/
num_experiments: number;
+ /**
+ * Total number of archived runs in a single repo/storage/project (.aim directory)
+ */
+ num_archived_runs: number;
/**
* Total number of runs in a single repo/storage/project (.aim directory)
*/
num_runs: number;
+ /**
+ * Number of active runs in a single repo/storage/project (.aim directory)
+ */
+ num_active_runs: number;
/**
* Activity distribution by datetime (creating run, tracking etc.)
* This data is used by the activity heatmap of main dashboard of UI
diff --git a/aim/web/ui/src/modules/core/api/releaseNotesApi/index.ts b/aim/web/ui/src/modules/core/api/releaseNotesApi/index.ts
new file mode 100644
index 0000000000..e7ce822413
--- /dev/null
+++ b/aim/web/ui/src/modules/core/api/releaseNotesApi/index.ts
@@ -0,0 +1,68 @@
+import ENDPOINTS from 'services/api/endpoints';
+import NetworkService from 'services/NetworkService';
+
+import { IReleaseNote } from './types';
+
+const api = new NetworkService(`${ENDPOINTS.RELEASE_NOTES.BASE}`);
+
+/**
+ * function fetchReleaseNotes
+ * this call is used for fetching release notes list.
+ * @returns {Promise}
+ */
+async function fetchReleaseNotes(): Promise {
+ return (
+ await api.makeAPIGetRequest(ENDPOINTS.RELEASE_NOTES.GET, {
+ query_params: { per_page: 10 },
+ headers: {},
+ })
+ ).body;
+}
+
+/**
+ * function fetchLatestRelease
+ * this call is used for fetching latest release note.
+ * @returns {Promise}
+ */
+async function fetchLatestRelease(): Promise {
+ return (
+ await api.makeAPIGetRequest(`${ENDPOINTS.RELEASE_NOTES.GET}/latest}`, {
+ headers: {},
+ })
+ ).body;
+}
+
+/**
+ * function fetchLatestReleaseById
+ * this call is used for fetching release note by id.
+ * @returns {Promise}
+ */
+async function fetchReleaseById(id: string): Promise {
+ return (
+ await api.makeAPIGetRequest(`${ENDPOINTS.RELEASE_NOTES.GET}/${id}}`, {
+ headers: {},
+ })
+ ).body;
+}
+
+/**
+ * function fetchLatestReleaseByTagName
+ * this call is used for fetching release note by tag name.
+ * @returns {Promise}
+ */
+
+async function fetchReleaseByTagName(tagName: string): Promise {
+ return (
+ await api.makeAPIGetRequest(
+ `${ENDPOINTS.RELEASE_NOTES.GET}${ENDPOINTS.RELEASE_NOTES.GET_BY_TAG_NAME}/${tagName}`,
+ { headers: {} },
+ )
+ ).body;
+}
+
+export {
+ fetchReleaseNotes,
+ fetchLatestRelease,
+ fetchReleaseById,
+ fetchReleaseByTagName,
+};
diff --git a/aim/web/ui/src/modules/core/api/releaseNotesApi/types.ts b/aim/web/ui/src/modules/core/api/releaseNotesApi/types.ts
new file mode 100644
index 0000000000..dfc1dc861b
--- /dev/null
+++ b/aim/web/ui/src/modules/core/api/releaseNotesApi/types.ts
@@ -0,0 +1,135 @@
+/**
+ * @description This interface is used for the release notes data. It is used for the release notes list and the release note details.
+ */
+export interface IReleaseNote {
+ /**
+ * @description The url of the release note with the release note id.
+ * @type {string}
+ * @example 'https://api.github.com/repos/aimhubio/aim/releases/76800801'
+ */
+ url: string;
+ /**
+ * @description The asset url of the release note.
+ * @type {string}
+ */
+ assets_url: string;
+ /**
+ * @description The upload url of the release note.
+ * @type {string}
+ */
+ upload_url: string;
+ /**
+ * @description The page/html url of the release note.
+ * @type {string}
+ */
+ html_url: string;
+ /**
+ * @description The id of the release note.
+ * @type {number}
+ * @example 76800801
+ */
+ id: number;
+ /**
+ * @description The author data of the release note.
+ * @type {IReleaseNoteAuthor}
+ */
+ author: ReleaseNoteAuthorType;
+ /**
+ * @description The node_id of the release note.
+ * @type {string}
+ * @example 'RE_kwDOC02th84Ek-Mh'
+ */
+ node_id: string;
+ /**
+ * @description The tag name of the release note.
+ * @type {string}
+ * @example 'v3.13.0'
+ */
+ tag_name: string;
+ /**
+ * @description The target of the commit of the release.
+ * @type string
+ * @example 'main'
+ */
+ target_commitish: string;
+ /**
+ * @description The name of the release.
+ * @type string
+ * @example 'v3.13.0 🚀'
+ */
+ name: string;
+ /**
+ * @description The draft status of the release.
+ * @type boolean
+ * @example false
+ */
+ draft: boolean;
+ /**
+ * @description The prerelease status of the release.
+ * @type boolean
+ * @example false
+ */
+ prerelease: boolean;
+ /**
+ * @description The created at timestamp of the release.
+ * @type string
+ * @example '2022-09-10T15:25:48Z'
+ */
+ created_at: string;
+ /**
+ * @description The published at timestamp of the release.
+ * @type string
+ * @example '2022-09-10T15:57:10Z'
+ */
+ published_at: string;
+ /**
+ * @description The assets of the release.
+ */
+ assets: [];
+ /**
+ * @description The gzip file download url of that version of aim.
+ * @type string
+ */
+ tarball_url: string;
+ /**
+ * @description The zip file download url of that version of aim.
+ * @type string
+ */
+ zipball_url: string;
+ /**
+ * @description The markdown type body of the release.
+ * @type string
+ * @example '## Features'
+ */
+ body: string;
+ /**
+ * @description The contributors of the release.
+ * @type number
+ * @example 3
+ */
+ mentions_count: number;
+}
+
+/**
+ * @description The type for the release notes author data
+ */
+export type ReleaseNoteAuthorType = {
+ login: string;
+ id: number;
+ node_id: string;
+ avatar_url: string;
+ gravatar_id: string;
+ url: string;
+ html_url: string;
+ followers_url: string;
+ following_url: string;
+ gists_url: string;
+ starred_url: string;
+ subscriptions_url: string;
+ organizations_url: string;
+ repos_url: string;
+ events_url: string;
+ received_events_url: string;
+ type: string;
+ site_admin: boolean;
+};
diff --git a/aim/web/ui/src/modules/core/api/runsApi/index.ts b/aim/web/ui/src/modules/core/api/runsApi/index.ts
index a73fc2b952..3d47368506 100644
--- a/aim/web/ui/src/modules/core/api/runsApi/index.ts
+++ b/aim/web/ui/src/modules/core/api/runsApi/index.ts
@@ -52,6 +52,59 @@ function createSearchRunsRequest(
cancel,
};
}
+function createSearchRunRequest(): RequestInstance {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ async function call(
+ queryParams: RunsSearchQueryParams,
+ ): Promise {
+ return (
+ await api.makeAPIGetRequest(`${ENDPOINTS.RUNS.SEARCH}/run`, {
+ query_params: queryParams,
+ signal,
+ })
+ ).body;
+ }
+
+ function cancel(): void {
+ controller.abort();
+ }
+
+ return {
+ call,
+ cancel,
+ };
+}
+
+function createActiveRunsRequest(): RequestInstance {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ async function call(
+ queryParams: RunsSearchQueryParams,
+ ): Promise {
+ return (
+ await api.makeAPIGetRequest(`${ENDPOINTS.RUNS.ACTIVE}`, {
+ signal,
+ })
+ ).body;
+ }
+
+ function cancel(): void {
+ controller.abort();
+ }
+
+ return {
+ call,
+ cancel,
+ };
+}
-export { searchRuns, createSearchRunsRequest };
+export {
+ searchRuns,
+ createSearchRunsRequest,
+ createActiveRunsRequest,
+ createSearchRunRequest,
+};
export * from './types';
diff --git a/aim/web/ui/src/modules/core/api/runsApi/types.ts b/aim/web/ui/src/modules/core/api/runsApi/types.ts
index aeb697d648..4032901129 100644
--- a/aim/web/ui/src/modules/core/api/runsApi/types.ts
+++ b/aim/web/ui/src/modules/core/api/runsApi/types.ts
@@ -42,6 +42,9 @@ export type RunsSearchQueryParams = {
* This parameter is used to for simple sampling, indicates how many objects want to load
*/
index_density?: number;
+
+ exclude_params?: boolean;
+ exclude_traces?: boolean;
};
/**
diff --git a/aim/web/ui/src/modules/core/api/tagsApi/index.ts b/aim/web/ui/src/modules/core/api/tagsApi/index.ts
new file mode 100644
index 0000000000..ce45b67e2e
--- /dev/null
+++ b/aim/web/ui/src/modules/core/api/tagsApi/index.ts
@@ -0,0 +1,114 @@
+import { getAPIHost } from 'config/config';
+
+import ENDPOINTS from 'services/api/endpoints';
+import NetworkService from 'services/NetworkService';
+
+import {
+ ICreateTagBody,
+ ICreateTagResult,
+ IGetTagRunsResult,
+ ITagData,
+ IUpdateTagBody,
+} from './types';
+
+const api = new NetworkService(`${getAPIHost()}${ENDPOINTS.TAGS.BASE}`);
+
+/**
+ * function fetchTagsList
+ * this call is used for fetching tags list.
+ * @returns {Promise}
+ */
+async function fetchTagsList(): Promise {
+ return (await api.makeAPIGetRequest(ENDPOINTS.TAGS.GET)).body;
+}
+
+/**
+ * function getTagById
+ * this call is used for getting a tag by id.
+ * @param id - id of tag
+ * @returns {Promise}
+ */
+async function getTagById(id: string): Promise {
+ return (await api.makeAPIGetRequest(`${ENDPOINTS.TAGS.GET}/${id}`)).body;
+}
+
+/**
+ * function getTagRuns
+ * this call is used for fetching a runs which have a tag by id.
+ * @param id - id of tag
+ * @returns {Promise}
+ */
+async function getTagRuns(id: string): Promise {
+ return (await api.makeAPIGetRequest(`${ENDPOINTS.TAGS.GET}/${id}/runs`)).body;
+}
+
+/**
+ * function createTag
+ * this call is used for creating new tag.
+ * @param reqBody - query body params
+ * @returns {Promise}
+ */
+async function createTag(reqBody: ICreateTagBody): Promise {
+ return (
+ await api.makeAPIPostRequest(ENDPOINTS.TAGS.CREATE, {
+ body: reqBody,
+ })
+ ).body;
+}
+
+/**
+ * function updateTag
+ * this call is used for updating a tag by id.
+ * @param reqBody - query body params
+ * @param id - id of tag
+ * @returns {Promise}
+ */
+async function updateTag(
+ reqBody: IUpdateTagBody,
+ id: string,
+): Promise {
+ return (
+ await api.makeAPIPutRequest(`${ENDPOINTS.TAGS.UPDATE}/${id}`, {
+ body: reqBody,
+ })
+ ).body;
+}
+
+/**
+ * function archiveTag
+ * this call is used for archiving a tag by id.
+ * @param id - id of tag
+ * @param archived - archived status
+ * @returns {Promise}
+ */
+async function archiveTag(
+ id: string,
+ archived: boolean,
+): Promise {
+ return (
+ await api.makeAPIPutRequest(`${ENDPOINTS.TAGS.UPDATE}/${id}`, {
+ body: { archived },
+ })
+ ).body;
+}
+
+/**
+ * function deleteTag
+ * this call is used for deleting a tag by id.
+ * @param id - id of tag
+ * @returns {Promise}
+ */
+async function deleteTag(id: string): Promise {
+ return (await api.makeAPIDeleteRequest(`${ENDPOINTS.TAGS.DELETE}/${id}`))
+ .body;
+}
+
+export {
+ fetchTagsList,
+ getTagRuns,
+ createTag,
+ updateTag,
+ getTagById,
+ archiveTag,
+ deleteTag,
+};
diff --git a/aim/web/ui/src/modules/core/api/tagsApi/types.ts b/aim/web/ui/src/modules/core/api/tagsApi/types.ts
new file mode 100644
index 0000000000..c41455ff29
--- /dev/null
+++ b/aim/web/ui/src/modules/core/api/tagsApi/types.ts
@@ -0,0 +1,132 @@
+// The response body of GET /tags
+export interface ITagData {
+ /**
+ * Tag name
+ * @example "test"
+ * @type string
+ */
+ name: string;
+ /**
+ * Tag color
+ * @example "#000000"
+ * @type string
+ */
+ color: string | null;
+ /**
+ * Tag id
+ * @example '3fa85f64-5717-4562-b3fc-2c963f66afa6'
+ * @type string
+ */
+ id: string;
+ /**
+ * Tag description
+ * @example "tag description"
+ * @type string
+ */
+ description: string;
+ /**
+ * Tag run count which is associated with this tag
+ * @example 1
+ * @type number
+ */
+ run_count: number;
+ /**
+ * Tag archived status
+ * @example false
+ * @type boolean
+ */
+ archived: boolean;
+}
+
+// The request body type of POST /tags
+export interface ICreateTagBody {
+ /**
+ * Tag name
+ * @example "test"
+ * @type string
+ */
+ name: string;
+ /**
+ * Tag color
+ * @example "#000000"
+ * @type string
+ */
+ color: string;
+ /**
+ * Tag description
+ * @example "tag description"
+ * @type string
+ */
+ description: string;
+}
+
+// The response body of the created tag
+export interface ICreateTagResult {
+ /**
+ * Tag id
+ * @example '3fa85f64-5717-4562-b3fc-2c963f66afa6'
+ * @type string
+ */
+ id: string;
+ /**
+ * Response status
+ * @example "OK"
+ * @type string
+ */
+ status: string;
+}
+
+export interface IUpdateTagBody extends Partial {
+ archived?: boolean;
+}
+
+// The response of getting runs by tag id
+export interface IGetTagRunsResult {
+ /**
+ * Tag id
+ * @example '3fa85f64-5717-4562-b3fc-2c963f66afa6'
+ * @type string
+ */
+ id: string;
+ /**
+ * Runs which are associated with this tag
+ * @example [{creation_time: 123, end_time: 123, experiment: 'test', name: 'test', run_id: '3fa85f64-5717-4562-b3fc-2c963f66afa6'}]
+ * @type ITagRun[]
+ * @see ITagRun
+ */
+ runs: ITagRun[];
+}
+
+// The run data which is associated with a tag
+type ITagRun = {
+ /**
+ * Run id
+ * @example '1af2657'
+ * @type string
+ */
+ run_id: string;
+ /**
+ * Run name
+ * @example 'run name'
+ * @type string
+ */
+ name: string;
+ /**
+ * Run experiment
+ * @example 'experiment name'
+ * @type string
+ */
+ experiment: string;
+ /**
+ * Run creation time
+ * @example 1632172882.096673
+ * @type number
+ */
+ creation_time: number;
+ /**
+ * Run end time
+ * @example 1632172882.350732
+ * @type number
+ */
+ end_time: number;
+};
diff --git a/aim/web/ui/src/modules/core/utils/createResource.ts b/aim/web/ui/src/modules/core/utils/createResource.ts
new file mode 100644
index 0000000000..4168a30d20
--- /dev/null
+++ b/aim/web/ui/src/modules/core/utils/createResource.ts
@@ -0,0 +1,31 @@
+import { RequestOptions } from 'https';
+import create from 'zustand';
+
+export interface IResourceState {
+ data: T | null;
+ loading: boolean;
+ error: any;
+}
+
+const defaultState = {
+ data: null,
+ loading: true,
+ error: null,
+};
+
+function createResource(getter: any) {
+ const state = create>(() => defaultState);
+
+ async function fetchData(args?: GetterArgs) {
+ state.setState({ loading: true });
+ const data = await getter(args);
+ state.setState({ data, loading: false });
+ }
+ function destroy() {
+ state.destroy();
+ state.setState(defaultState, true);
+ }
+ return { fetchData, state, destroy };
+}
+
+export default createResource;
diff --git a/aim/web/ui/src/pages/Bookmarks/components/BookmarkCard/BookmarkCard.tsx b/aim/web/ui/src/pages/Bookmarks/components/BookmarkCard/BookmarkCard.tsx
index 3fac0605c4..2ddfacc82b 100644
--- a/aim/web/ui/src/pages/Bookmarks/components/BookmarkCard/BookmarkCard.tsx
+++ b/aim/web/ui/src/pages/Bookmarks/components/BookmarkCard/BookmarkCard.tsx
@@ -17,9 +17,10 @@ import { IBookmarkCardProps } from 'types/pages/bookmarks/components/BookmarkCar
import './BookmarkCard.scss';
-const BookmarkIconType: {
+export const BookmarkIconType: {
[key: string]: { name: IconName; tooltipTitle: string };
} = {
+ runs: { name: 'runs', tooltipTitle: 'Runs Explorer' },
images: { name: 'images', tooltipTitle: 'Images Explorer' },
params: { name: 'params', tooltipTitle: 'Params Explorer' },
metrics: { name: 'metrics', tooltipTitle: 'Metrics Explorer' },
diff --git a/aim/web/ui/src/pages/Dashboard/Dashboard.scss b/aim/web/ui/src/pages/Dashboard/Dashboard.scss
new file mode 100644
index 0000000000..15f981f92e
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/Dashboard.scss
@@ -0,0 +1,39 @@
+@use 'src/styles/abstracts' as *;
+
+.Dashboard {
+ background-color: #ffffff;
+ overflow: hidden;
+ display: flex;
+ .DataList .BaseTable__row-cell {
+ p {
+ white-space: nowrap;
+ max-width: 100%;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ }
+ height: 100vh;
+
+ &__middle {
+ padding: $space-md $space-lg;
+ border: $border-dark-lighter;
+ border-top: none;
+ display: flex;
+ overflow: auto;
+ flex-direction: column;
+ flex: 1 1;
+
+ &--centered {
+ align-items: center;
+ justify-content: center;
+ }
+ }
+ &__Explore__container {
+ border-top: $border-dark-lighter;
+ border-bottom: $border-dark-lighter;
+ display: flex;
+ }
+ h2 {
+ margin: 0 0 1rem 0;
+ }
+}
diff --git a/aim/web/ui/src/pages/Dashboard/Dashboard.tsx b/aim/web/ui/src/pages/Dashboard/Dashboard.tsx
new file mode 100644
index 0000000000..b7b13dfe19
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/Dashboard.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import classnames from 'classnames';
+
+import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary';
+import { Spinner, Text } from 'components/kit';
+
+import ProjectContributions from './components/ProjectContributions/ProjectContributions';
+import ExploreSection from './components/ExploreSection/ExploreSection';
+import DashboardRight from './components/DashboardRight/DashboardRight';
+import ContributionsFeed from './components/ContributionsFeed/ContributionsFeed';
+import ProjectStatistics from './components/ProjectStatistics';
+import useProjectContributions from './components/ProjectContributions/useProjectContributions';
+import ActiveRunsTable from './components/ActiveRunsTable/ActiveRunsTable';
+import QuickStart from './components/QuickStart';
+import AimIntegrations from './components/AimIntegrations';
+
+import './Dashboard.scss';
+
+function Dashboard(): React.FunctionComponentElement {
+ const { projectContributionsStore } = useProjectContributions();
+
+ const totalRunsCount = projectContributionsStore.data?.num_runs ?? 0;
+ const activeRunsCount = projectContributionsStore.data?.num_active_runs ?? 0;
+ const isLoading = projectContributionsStore.loading;
+
+ return (
+
+
+
+
+ {isLoading ? (
+
+ ) : totalRunsCount === 0 ? (
+
+ ) : (
+ <>
+
+ Overview
+
+
+ {activeRunsCount ?
: null}
+
+
+ >
+ )}
+ {!isLoading && !totalRunsCount &&
}
+
+
+
+
+ );
+}
+export default Dashboard;
diff --git a/aim/web/ui/src/pages/Dashboard/components/ActiveRunsTable/ActiveRunsStore.ts b/aim/web/ui/src/pages/Dashboard/components/ActiveRunsTable/ActiveRunsStore.ts
new file mode 100644
index 0000000000..01ae5540ed
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ActiveRunsTable/ActiveRunsStore.ts
@@ -0,0 +1,17 @@
+import { createActiveRunsRequest } from 'modules/core/api/runsApi';
+import createResource from 'modules/core/utils/createResource';
+
+import { IRun } from 'types/services/models/metrics/runModel';
+
+import { parseStream } from 'utils/encoder/streamEncoding';
+
+function createActiveRunsEngine() {
+ let { call, cancel } = createActiveRunsRequest();
+
+ const { fetchData, state, destroy } = createResource[]>(
+ async () => parseStream(await call()),
+ );
+ return { fetchActiveRuns: fetchData, activeRunsState: state, destroy };
+}
+
+export default createActiveRunsEngine();
diff --git a/aim/web/ui/src/pages/Dashboard/components/ActiveRunsTable/ActiveRunsTable.scss b/aim/web/ui/src/pages/Dashboard/components/ActiveRunsTable/ActiveRunsTable.scss
new file mode 100644
index 0000000000..9f4613ed74
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ActiveRunsTable/ActiveRunsTable.scss
@@ -0,0 +1,33 @@
+@use 'src/styles/abstracts' as *;
+
+.ActiveRunsTable {
+ margin-top: $space-lg;
+}
+
+.ActiveRunsTable__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 28px;
+
+ .CompareSelectedRunsPopover__trigger {
+ margin-right: 0 !important;
+ }
+}
+
+.ActiveRunsTable__table {
+ height: 271px;
+ margin-top: $space-xxs;
+ border: 1px solid $cuddle-50;
+ border-top: none;
+}
+
+.ActiveRunsTable__table--loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.ActiveRunsTable__table--empty {
+ border: none;
+}
diff --git a/aim/web/ui/src/pages/Dashboard/components/ActiveRunsTable/ActiveRunsTable.tsx b/aim/web/ui/src/pages/Dashboard/components/ActiveRunsTable/ActiveRunsTable.tsx
new file mode 100644
index 0000000000..9f6090eb87
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ActiveRunsTable/ActiveRunsTable.tsx
@@ -0,0 +1,86 @@
+import * as React from 'react';
+import classNames from 'classnames';
+
+import { Spinner, Text } from 'components/kit';
+import Table from 'components/Table/Table';
+
+import { RowHeightSize } from 'config/table/tableConfigs';
+
+import CompareSelectedRunsPopover from 'pages/Metrics/components/Table/CompareSelectedRunsPopover';
+
+import { AppNameEnum } from 'services/models/explorer';
+
+import useActiveRunsTable from './useActiveRunsTable';
+
+import './ActiveRunsTable.scss';
+
+function ActiveRunsTable() {
+ const {
+ tableRef,
+ tableColumns,
+ tableData,
+ loading,
+ selectedRows,
+ comparisonQuery,
+ onRowSelect,
+ } = useActiveRunsTable();
+
+ return (
+
+
+
+ Active runs {tableData.length > 0 ? `(${tableData.length})` : ''}
+
+ {tableData.length > 0 && (
+
+
+
+ )}
+
+
+ {loading ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
+export default ActiveRunsTable;
diff --git a/aim/web/ui/src/pages/Dashboard/components/ActiveRunsTable/useActiveRunsTable.tsx b/aim/web/ui/src/pages/Dashboard/components/ActiveRunsTable/useActiveRunsTable.tsx
new file mode 100644
index 0000000000..1b719efcf0
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ActiveRunsTable/useActiveRunsTable.tsx
@@ -0,0 +1,256 @@
+import React from 'react';
+import _ from 'lodash-es';
+import moment from 'moment';
+
+import { IResourceState } from 'modules/core/utils/createResource';
+
+import RunNameColumn from 'components/Table/RunNameColumn';
+import { Badge } from 'components/kit';
+
+import { TABLE_DATE_FORMAT } from 'config/dates/dates';
+
+import { IRun } from 'types/services/models/metrics/runModel';
+
+import contextToString from 'utils/contextToString';
+import { processDurationTime } from 'utils/processDurationTime';
+import { getMetricHash } from 'utils/app/getMetricHash';
+import { formatValue } from 'utils/formatValue';
+import { isSystemMetric } from 'utils/isSystemMetric';
+import { formatSystemMetricName } from 'utils/formatSystemMetricName';
+import { decode, encode } from 'utils/encoder/encoder';
+
+import createActiveRunsEngine from './ActiveRunsStore';
+
+function useActiveRunsTable() {
+ const tableRef = React.useRef(null);
+ const { current: activeRunsEngine } = React.useRef(createActiveRunsEngine);
+ const activeRunsStore: IResourceState[]> =
+ activeRunsEngine.activeRunsState((state) => state);
+ const [selectedRows, setSelectedRows] = React.useState<
+ Record
+ >({});
+ const [comparisonQuery, setComparisonQuery] = React.useState('');
+
+ React.useEffect(() => {
+ activeRunsEngine.fetchActiveRuns();
+ return () => {
+ activeRunsEngine.destroy();
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const metricsColumns = React.useMemo(() => {
+ if (activeRunsStore.data) {
+ const metrics: any = [];
+ const systemMetrics: any = [];
+ const metricsValues: any = {};
+ activeRunsStore.data.forEach(({ hash, traces }: IRun) => {
+ traces.metric.forEach((trace: any) => {
+ const metricHash = getMetricHash(trace.name, trace.context as any);
+
+ if (metricsValues.hasOwnProperty(metricHash)) {
+ metricsValues[metricHash][hash] = [
+ trace.last_value.last_step,
+ trace.last_value.last,
+ ];
+ } else {
+ metricsValues[metricHash] = {
+ [hash]: [trace.last_value.last_step, trace.last_value.last],
+ };
+
+ const metricContext = contextToString(
+ trace.context as Record,
+ ) as string;
+
+ const isSystem = isSystemMetric(trace.name);
+ const col = {
+ key: metricHash,
+ content: (
+
+ ),
+ topHeader: isSystem
+ ? formatSystemMetricName(trace.name)
+ : trace.name,
+ name: trace.name,
+ context: metricContext,
+ isSystem,
+ };
+
+ if (isSystem) {
+ systemMetrics.push(col);
+ } else {
+ metrics.push(col);
+ }
+ }
+ });
+ });
+
+ return {
+ columns: _.orderBy(metrics, ['name', 'context'], ['asc', 'asc']).concat(
+ _.orderBy(systemMetrics, ['name', 'context'], ['asc', 'asc']),
+ ) as any,
+ values: metricsValues,
+ };
+ }
+
+ return {
+ columns: [],
+ values: [],
+ };
+ }, [activeRunsStore.data]);
+
+ // memoized table data
+ const tableData = React.useMemo(() => {
+ if (activeRunsStore.data) {
+ return activeRunsStore.data.map(
+ ({ props, hash }: IRun, index: number) => {
+ const key = encode({
+ hash,
+ });
+ let row: any = {
+ key,
+ selectKey: key,
+ index,
+ experiment: props.experiment?.name,
+ run: {
+ content: (
+
+ ),
+ },
+ date: moment(props.creation_time * 1000).format(TABLE_DATE_FORMAT),
+ duration: processDurationTime(
+ props.creation_time * 1000,
+ props.end_time ? props.end_time * 1000 : Date.now(),
+ ),
+ };
+
+ metricsColumns.columns.forEach((col: any) => {
+ const [step, value] = metricsColumns.values[col.key][hash] ?? [
+ null,
+ null,
+ ];
+ row[col.key] = {
+ content:
+ step === null
+ ? '--'
+ : col.isSystem
+ ? formatValue(value)
+ : `step: ${step} / value: ${formatValue(value)}`,
+ };
+ });
+ return row;
+ },
+ );
+ }
+ return [];
+ }, [activeRunsStore.data, metricsColumns]);
+
+ // memoized table columns
+ const tableColumns = React.useMemo(() => {
+ const columns = [
+ {
+ key: 'experiment',
+ content: Experiment ,
+ topHeader: 'Run',
+ pin: 'left',
+ },
+ {
+ key: 'run',
+ content: Name ,
+ topHeader: 'Run',
+ pin: 'left',
+ },
+ {
+ key: 'date',
+ content: Date ,
+ topHeader: 'Run',
+ },
+ {
+ key: 'duration',
+ content: Duration ,
+ topHeader: 'Run',
+ },
+ ];
+ return columns.concat(metricsColumns.columns);
+ }, [metricsColumns]);
+
+ // Update the table data and columns when the activity data changes
+ React.useEffect(() => {
+ if (tableRef.current?.updateData) {
+ tableRef.current.updateData({
+ newColumns: tableColumns,
+ newData: tableData,
+ });
+ }
+ }, [tableData, tableColumns]);
+
+ // Handler for row selection
+ const onRowSelect = React.useCallback(
+ ({ actionType, data }) => {
+ let selected: Record = { ...selectedRows };
+ switch (actionType) {
+ case 'single':
+ if (selectedRows[data.key]) {
+ selected = _.omit(selectedRows, data.key);
+ } else {
+ selected[data.key] = true;
+ }
+ break;
+ case 'selectAll':
+ if (Array.isArray(data)) {
+ data.forEach((item: any) => {
+ if (!selectedRows[item.key]) {
+ selected[item.key] = true;
+ }
+ });
+ } else {
+ Object.values(data)
+ .reduce((acc: any[], value: any) => {
+ return acc.concat(value.items);
+ }, [])
+ .forEach((item: any) => {
+ if (!selectedRows[item.selectKey]) {
+ selected[item.selectKey] = true;
+ }
+ });
+ }
+ break;
+ case 'removeAll':
+ if (Array.isArray(data)) {
+ selected = {};
+ }
+ break;
+ }
+
+ setSelectedRows(selected);
+
+ setComparisonQuery(
+ `run.hash in [${Object.keys(selected)
+ .map((key) => `"${JSON.parse(decode(key)).hash}"`)
+ .join(', ')}]`,
+ );
+ },
+ [selectedRows, tableData],
+ );
+
+ return {
+ data: activeRunsStore.data,
+ tableData,
+ tableColumns,
+ tableRef,
+ loading: activeRunsStore.loading,
+ selectedRows,
+ comparisonQuery,
+ onRowSelect,
+ };
+}
+
+export default useActiveRunsTable;
diff --git a/aim/web/ui/src/pages/Dashboard/components/AimIntegrations/AimIntegrations.scss b/aim/web/ui/src/pages/Dashboard/components/AimIntegrations/AimIntegrations.scss
new file mode 100644
index 0000000000..fb0e364287
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/AimIntegrations/AimIntegrations.scss
@@ -0,0 +1,38 @@
+@use 'src/styles/abstracts' as *;
+
+.AimIntegrations {
+ margin-top: $space-lg;
+}
+
+.AimIntegrations__section {
+ padding: $space-lg 0;
+}
+
+.AimIntegrations__section__title {
+ margin-bottom: $space-sm;
+}
+
+.AimIntegrations__section__accordion {
+ box-shadow: 0 0 0 1px $pico-20;
+ margin: 0 !important;
+
+ &::before {
+ content: none;
+ }
+}
+
+.AimIntegrations__section__accordion__summary {
+ padding: $space-xs $space-sm !important;
+ min-height: 42px !important;
+ max-height: 42px !important;
+}
+
+.AimIntegrations__section__accordion__details {
+ display: block;
+ padding: $space-sm $space-sm!important;
+}
+
+.AimIntegrations__section__text {
+ font-style: italic;
+ margin-top: $space-sm;
+}
diff --git a/aim/web/ui/src/pages/Dashboard/components/AimIntegrations/AimIntegrations.tsx b/aim/web/ui/src/pages/Dashboard/components/AimIntegrations/AimIntegrations.tsx
new file mode 100644
index 0000000000..67bb80d2cf
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/AimIntegrations/AimIntegrations.tsx
@@ -0,0 +1,202 @@
+import * as React from 'react';
+
+import {
+ Accordion,
+ AccordionDetails,
+ AccordionSummary,
+ Link,
+} from '@material-ui/core';
+
+import { Icon, Text } from 'components/kit';
+import CodeBlock from 'components/CodeBlock/CodeBlock';
+
+import { DOCUMENTATIONS } from 'config/references';
+
+import './AimIntegrations.scss';
+
+function AimIntegrations() {
+ const [expanded, setExpanded] = React.useState(0);
+
+ const handleChange =
+ (panel: number) => (event: React.ChangeEvent<{}>, newExpanded: boolean) => {
+ setExpanded(newExpanded ? panel : false);
+ };
+
+ const integrations = [
+ {
+ title: 'Integrate PyTorch Lightning',
+ docsLink: DOCUMENTATIONS.INTEGRATIONS.PYTORCH_LIGHTNING,
+ code: `from aim.pytorch_lightning import AimLogger
+
+# ...
+trainer = pl.Trainer(logger=AimLogger(experiment='experiment_name'))
+# ...`,
+ },
+ {
+ title: 'Integrate Hugging Face',
+ docsLink: DOCUMENTATIONS.INTEGRATIONS.HUGGING_FACE,
+ code: `from aim.hugging_face import AimCallback
+
+# ...
+aim_callback = AimCallback(repo='/path/to/logs/dir', experiment='mnli')
+trainer = Trainer(
+ model=model,
+ args=training_args,
+ train_dataset=train_dataset if training_args.do_train else None,
+ eval_dataset=eval_dataset if training_args.do_eval else None,
+ callbacks=[aim_callback],
+ # ...
+)
+# ...`,
+ },
+ {
+ title: 'Integrate Keras & tf.keras',
+ docsLink: DOCUMENTATIONS.INTEGRATIONS.KERAS,
+ code: `import aim
+
+# ...
+model.fit(x_train, y_train, epochs=epochs, callbacks=[
+ aim.keras.AimCallback(repo='/path/to/logs/dir', experiment='experiment_name')
+
+ # Use aim.tensorflow.AimCallback in case of tf.keras
+ aim.tensorflow.AimCallback(repo='/path/to/logs/dir', experiment='experiment_name')
+])
+# ...`,
+ },
+ {
+ title: 'Integrate KerasTuner',
+ docsLink: DOCUMENTATIONS.INTEGRATIONS.KERAS_TUNER,
+ code: `from aim.keras_tuner import AimCallback
+
+# ...
+tuner.search(
+ train_ds,
+ validation_data=test_ds,
+ callbacks=[AimCallback(tuner=tuner, repo='.', experiment='keras_tuner_test')],
+)
+# ...`,
+ },
+ {
+ title: 'Integrate XGBoost',
+ docsLink: DOCUMENTATIONS.INTEGRATIONS.XGBOOST,
+ code: `from aim.xgboost import AimCallback
+
+# ...
+aim_callback = AimCallback(repo='/path/to/logs/dir', experiment='experiment_name')
+bst = xgb.train(param, xg_train, num_round, watchlist, callbacks=[aim_callback])
+# ...`,
+ },
+ {
+ title: 'Integrate CatBoost',
+ docsLink: DOCUMENTATIONS.INTEGRATIONS.CATBOOST,
+ code: `from aim.catboost import AimLogger
+
+# ...
+model.fit(train_data, train_labels, log_cout=AimLogger(loss_function='Logloss'), logging_level="Info")
+# ...`,
+ },
+ {
+ title: 'Integrate fastai',
+ docsLink: DOCUMENTATIONS.INTEGRATIONS.FASTAI,
+ code: `from aim.fastai import AimCallback
+
+# ...
+learn = cnn_learner(dls, resnet18, pretrained=True,
+ loss_func=CrossEntropyLossFlat(),
+ metrics=accuracy, model_dir="/tmp/model/",
+ cbs=AimCallback(repo='.', experiment='fastai_test'))
+# ...`,
+ },
+ {
+ title: 'Integrate LightGBM',
+ docsLink: DOCUMENTATIONS.INTEGRATIONS.LIGHT_GBM,
+ code: `from aim.lightgbm import AimCallback
+
+# ...
+aim_callback = AimCallback(experiment='lgb_test')
+aim_callback.experiment['hparams'] = params
+
+gbm = lgb.train(params,
+ lgb_train,
+ num_boost_round=20,
+ valid_sets=lgb_eval,
+ callbacks=[aim_callback, lgb.early_stopping(stopping_rounds=5)])
+# ...`,
+ },
+
+ {
+ title: 'Integrate PyTorch Ignite',
+ docsLink: DOCUMENTATIONS.INTEGRATIONS.PYTORCH_IGNITE,
+ code: `from aim.pytorch_ignite import AimLogger
+
+# ...
+aim_logger = AimLogger()
+
+aim_logger.log_params({
+ "model": model.__class__.__name__,
+ "pytorch_version": str(torch.__version__),
+ "ignite_version": str(ignite.__version__),
+})
+
+aim_logger.attach_output_handler(
+ trainer,
+ event_name=Events.ITERATION_COMPLETED,
+ tag="train",
+ output_transform=lambda loss: {'loss': loss}
+)
+# ...`,
+ },
+ ];
+
+ return (
+
+
+ Integrate Aim with your favorite ML framework
+
+
+ {integrations.map((item, i) => (
+
+ }
+ className='AimIntegrations__section__accordion__summary'
+ >
+
+ {item.title}
+
+
+
+
+
+ See documentation{' '}
+
+ here
+
+ .
+
+
+
+ ))}
+
+
+ );
+}
+
+export default AimIntegrations;
diff --git a/aim/web/ui/src/pages/Dashboard/components/AimIntegrations/index.ts b/aim/web/ui/src/pages/Dashboard/components/AimIntegrations/index.ts
new file mode 100644
index 0000000000..abd212a2a0
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/AimIntegrations/index.ts
@@ -0,0 +1,3 @@
+import AimIntegrations from './AimIntegrations';
+
+export default AimIntegrations;
diff --git a/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/ContributionsFeed.scss b/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/ContributionsFeed.scss
new file mode 100644
index 0000000000..26c4d79ece
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/ContributionsFeed.scss
@@ -0,0 +1,16 @@
+@use 'src/styles/abstracts' as *;
+
+.ContributionsFeed {
+ border-top: none;
+ &__title {
+ margin-bottom: $space-sm;
+ padding: 0 $space-sm;
+ }
+ &__content {
+ margin-top: $space-sm;
+ padding-bottom: $space-xxxxs;
+ &-title {
+ margin-bottom: $space-sm;
+ }
+ }
+}
diff --git a/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/ContributionsFeed.tsx b/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/ContributionsFeed.tsx
new file mode 100644
index 0000000000..4207ed42f7
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/ContributionsFeed.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import _ from 'lodash-es';
+
+import { Button, Spinner, Text } from 'components/kit';
+
+import useContributionsFeed from './useContributionsFeed';
+import FeedItem from './FeedItem/FeedItem';
+
+import './ContributionsFeed.scss';
+
+function ContributionsFeed(): React.FunctionComponentElement | null {
+ let { data, loadMore, isLoading, totalRunsCount, fetchedCount } =
+ useContributionsFeed();
+
+ return totalRunsCount ? (
+
+
+ Activity
+
+ {isLoading && _.isEmpty(data) ? (
+
+
+
+ ) : (
+ <>
+ {Object.keys(data).map((key) => (
+
+
+ {key.split('_').join(' ')}
+
+ {Object.keys(data[key]).map((item: string) => {
+ return (
+
+ );
+ })}
+
+ ))}
+ {fetchedCount < totalRunsCount! ? (
+
+ {isLoading ? 'Loading...' : 'Show more activity'}
+
+ ) : null}
+ >
+ )}
+
+ ) : null;
+}
+
+export default React.memo(ContributionsFeed);
diff --git a/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/ContributionsFeedStore.ts b/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/ContributionsFeedStore.ts
new file mode 100644
index 0000000000..6c5e99dc78
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/ContributionsFeedStore.ts
@@ -0,0 +1,25 @@
+import { createSearchRunRequest } from 'modules/core/api/runsApi';
+import createResource from 'modules/core/utils/createResource';
+
+import { RequestOptions } from 'services/NetworkService';
+
+import { IRun } from 'types/services/models/metrics/runModel';
+
+import { parseStream } from 'utils/encoder/streamEncoding';
+
+function createContributionsFeedEngine() {
+ let { call, cancel } = createSearchRunRequest();
+
+ const { fetchData, state, destroy } = createResource[]>(
+ async (queryParams: RequestOptions['query_params']) =>
+ parseStream(await call(queryParams)),
+ );
+ return {
+ fetchContributionsFeed: (queryParams: RequestOptions['query_params']) =>
+ fetchData(queryParams),
+ contributionsFeedState: state,
+ destroy,
+ };
+}
+
+export default createContributionsFeedEngine();
diff --git a/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/FeedItem/FeedItem.d.ts b/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/FeedItem/FeedItem.d.ts
new file mode 100644
index 0000000000..60acf063ea
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/FeedItem/FeedItem.d.ts
@@ -0,0 +1,43 @@
+export interface IFeedItemProps {
+ date: string;
+ data: Array;
+}
+
+export type ContributionType = {
+ /**
+ * The run name of the contribution
+ * @type {string}
+ * @example 'My first run'
+ */
+ name: string;
+ /**
+ * The date of the contribution
+ * @type {string}
+ * @example '2021-01-01 12:00:00'
+ */
+ date: string;
+ /**
+ * The run hash of the contribution
+ * @type {string}
+ * @example '5e9f1b9b-7c1a-4b5a-8f0c-8c1c1b9b7c1a'
+ */
+ hash: string;
+ /**
+ * The run active state of the contribution
+ * @type {boolean}
+ * @example true
+ */
+ active: boolean;
+ /**
+ * The run creation time of the contribution
+ * @type {number}
+ * @example 1610000000
+ */
+ creation_time: number;
+ /**
+ * The run experiment name of the contribution
+ * @type {string}
+ * @example 'My first experiment'
+ */
+ experiment: string;
+};
diff --git a/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/FeedItem/FeedItem.scss b/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/FeedItem/FeedItem.scss
new file mode 100644
index 0000000000..c579705c38
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/FeedItem/FeedItem.scss
@@ -0,0 +1,39 @@
+@use 'src/styles/abstracts' as *;
+
+.FeedItem {
+ margin-bottom: $space-xs;
+ &__title {
+ align-items: center;
+ display: flex;
+ margin-bottom: $space-xxxs;
+ .Icon__container {
+ border-radius: $border-radius-circle;
+ background-color: $primary-color-10;
+ margin-right: $space-xxxs;
+ color: $text-color;
+ }
+ }
+ &__content {
+ position: relative;
+ padding-left: 38px;
+ &::before {
+ content: '';
+ position: absolute;
+ top: -2px;
+ left: 12px;
+ width: 1px;
+ height: calc(100% + 8px);
+ background-color: $primary-color-20;
+ }
+ &__item {
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+ margin-bottom: $space-xs;
+ .RunNameColumn {
+ margin: 0 $space-xs;
+ font-size: $text-sm;
+ }
+ }
+ }
+}
diff --git a/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/FeedItem/FeedItem.tsx b/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/FeedItem/FeedItem.tsx
new file mode 100644
index 0000000000..72868ce715
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/FeedItem/FeedItem.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+
+import { Icon, Text } from 'components/kit';
+import RunNameColumn from 'components/Table/RunNameColumn';
+
+import { IFeedItemProps } from './FeedItem.d';
+
+import './FeedItem.scss';
+
+function FeedItem(
+ props: IFeedItemProps,
+): React.FunctionComponentElement {
+ return (
+
+
+
+
+ {props.date.split('_').join(' ')}
+
+
+
+ {props.data.map((item) => (
+
+
+ Started a run:
+
+
+
+ {item.date}
+
+
+ ))}
+
+
+ );
+}
+
+export default React.memo(FeedItem);
diff --git a/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/useContributionsFeed.ts b/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/useContributionsFeed.ts
new file mode 100644
index 0000000000..60274ad72c
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ContributionsFeed/useContributionsFeed.ts
@@ -0,0 +1,123 @@
+import React from 'react';
+import moment from 'moment';
+
+import { IResourceState } from 'modules/core/utils/createResource';
+
+import {
+ CONTRIBUTION_DAY_FORMAT,
+ CONTRIBUTION_MONTH_FORMAT,
+ CONTRIBUTION_TIME_FORMAT,
+} from 'config/dates/dates';
+
+import { IRun } from 'types/services/models/metrics/runModel';
+
+import projectContributionsEngine from '../ProjectContributions/ProjectContributionsStore';
+
+import contributionsFeedEngine from './ContributionsFeedStore';
+
+function useContributionsFeed() {
+ const [data, setData] = React.useState([]);
+ const { current: engine } = React.useRef(contributionsFeedEngine);
+ const contributionsFeedStore: IResourceState =
+ engine.contributionsFeedState((state) => state);
+ const { current: contributionsEngine } = React.useRef(
+ projectContributionsEngine,
+ );
+ const projectContributionsStore =
+ contributionsEngine.projectContributionsState((state) => state);
+
+ React.useEffect(() => {
+ engine.fetchContributionsFeed({
+ limit: 25,
+ exclude_params: true,
+ exclude_traces: true,
+ });
+ return () => {
+ engine.destroy();
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ React.useEffect(() => {
+ if (contributionsFeedStore.data?.length) {
+ let newData = [...data, ...contributionsFeedStore.data];
+ setData(newData);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [contributionsFeedStore.data]);
+
+ const memoizedData = React.useMemo(() => {
+ // get existing month list from the contributionsFeedStore data
+ const feedData: { [key: string]: any } = {};
+ if (data.length) {
+ const monthList = data?.reduce((acc: any, run: IRun) => {
+ const { props } = run;
+ const month = moment(props.creation_time * 1000).format(
+ CONTRIBUTION_MONTH_FORMAT,
+ );
+ if (!acc.includes(month)) {
+ acc.push(month);
+ }
+ return acc;
+ }, []);
+ // create a list of objects with month and contributions
+
+ monthList.forEach((month: string) => {
+ feedData[month] = {};
+ });
+
+ // add contributions to the month list
+ data?.forEach((run: IRun) => {
+ const { props, hash } = run;
+
+ // get the month
+ const month = moment(props.creation_time * 1000).format(
+ CONTRIBUTION_MONTH_FORMAT,
+ );
+
+ // get the day of the month
+ const day = moment(props.creation_time * 1000).format(
+ CONTRIBUTION_DAY_FORMAT,
+ );
+
+ // create a contribution object
+ const contribution = {
+ name: props.name,
+ date: moment(props.creation_time * 1000).format(
+ CONTRIBUTION_TIME_FORMAT,
+ ),
+ hash,
+ active: props.active,
+ creation_time: props.creation_time,
+ experiment: props.experiment?.name,
+ };
+ if (feedData[month]?.[day]?.length) {
+ feedData[month][day].push(contribution);
+ } else {
+ feedData[month][day] = [contribution];
+ }
+ });
+ }
+ return feedData;
+ }, [data]);
+
+ function loadMore(): void {
+ if (contributionsFeedStore.data && !contributionsFeedStore.loading) {
+ engine.fetchContributionsFeed({
+ limit: 25,
+ exclude_params: true,
+ exclude_traces: true,
+ offset: data[data.length - 1].hash,
+ });
+ }
+ }
+ return {
+ isLoading: contributionsFeedStore.loading,
+ data: memoizedData,
+ totalRunsCount: projectContributionsStore.data?.num_runs,
+ fetchedCount: data.length,
+ loadMore,
+ };
+}
+
+export default useContributionsFeed;
diff --git a/aim/web/ui/src/pages/Dashboard/components/DashboardRight/DashboardRight.scss b/aim/web/ui/src/pages/Dashboard/components/DashboardRight/DashboardRight.scss
new file mode 100644
index 0000000000..c675f5d128
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/DashboardRight/DashboardRight.scss
@@ -0,0 +1,13 @@
+@use 'src/styles/abstracts' as *;
+
+.DashboardRight {
+ display: flex;
+ flex-direction: column;
+ width: 285px;
+ background-color: #fafafb;
+ overflow: auto;
+ &__title {
+ margin-top: $space-md;
+ padding: 0 $space-lg;
+ }
+}
diff --git a/aim/web/ui/src/pages/Dashboard/components/DashboardRight/DashboardRight.tsx b/aim/web/ui/src/pages/Dashboard/components/DashboardRight/DashboardRight.tsx
new file mode 100644
index 0000000000..01418b9d00
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/DashboardRight/DashboardRight.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+
+import { Text } from 'components/kit';
+
+import ReleaseNotes from './ReleaseNotes/ReleaseNotes';
+
+import './DashboardRight.scss';
+
+function DashboardRight(): React.FunctionComponentElement {
+ return (
+
+ );
+}
+
+export default React.memo(DashboardRight);
diff --git a/aim/web/ui/src/pages/Dashboard/components/DashboardRight/GuideDocs/GuideDocs.scss b/aim/web/ui/src/pages/Dashboard/components/DashboardRight/GuideDocs/GuideDocs.scss
new file mode 100644
index 0000000000..bc296d7eaf
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/DashboardRight/GuideDocs/GuideDocs.scss
@@ -0,0 +1,21 @@
+@use 'src/styles/abstracts' as *;
+
+.GuideLinks {
+ padding: $space-sm $space-sm $space-lg;
+ display: flex;
+ flex-direction: column;
+ border-bottom: $border-dark-lighter;
+ &__title {
+ margin-left: $space-sm;
+ }
+ &__content {
+ margin: $space-xs 0 $space-unit;
+ &--name {
+ flex: 1 100;
+ }
+ }
+ &--btn {
+ margin: 0 $space-sm;
+ text-decoration: none;
+ }
+}
diff --git a/aim/web/ui/src/pages/Dashboard/components/DashboardRight/GuideDocs/GuideDocs.tsx b/aim/web/ui/src/pages/Dashboard/components/DashboardRight/GuideDocs/GuideDocs.tsx
new file mode 100644
index 0000000000..e83c0fdcba
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/DashboardRight/GuideDocs/GuideDocs.tsx
@@ -0,0 +1,91 @@
+import React from 'react';
+
+import { Tooltip } from '@material-ui/core';
+
+import { Button, Icon, Text } from 'components/kit';
+import ListItem from 'components/kit/ListItem/ListItem';
+
+import { DOCUMENTATIONS } from 'config/references';
+
+import guideStore from './GuidesStore';
+
+import './GuideDocs.scss';
+
+function GuideDocs(): React.FunctionComponentElement {
+ const { shuffle, guideLinks, shuffled } = guideStore();
+
+ const onClick: (
+ e: React.MouseEvent,
+ path: string,
+ newTab?: boolean,
+ ) => void = React.useCallback(
+ (e: React.MouseEvent, path: string, newTab = false) => {
+ e.stopPropagation();
+ if (path) {
+ window.open(path, newTab ? '_blank' : '_self');
+ window.focus();
+ return;
+ }
+ },
+ [],
+ );
+
+ React.useEffect(() => {
+ if (!shuffled) {
+ shuffle();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+
+ Guides
+
+
+ {guideLinks.map(
+ (link: { name: string; url: string }, index: number) => (
+
+ onClick(e, link.url)}
+ size={12}
+ tint={100}
+ >
+ {link.name}
+
+
+
+ onClick(e, link.url, true)}
+ name='new-tab'
+ />
+
+
+
+ ),
+ )}
+
+
+
+ Docs
+
+
+
+ );
+}
+
+export default React.memo(GuideDocs);
diff --git a/aim/web/ui/src/pages/Dashboard/components/DashboardRight/GuideDocs/GuidesStore.ts b/aim/web/ui/src/pages/Dashboard/components/DashboardRight/GuideDocs/GuidesStore.ts
new file mode 100644
index 0000000000..b9542b2864
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/DashboardRight/GuideDocs/GuidesStore.ts
@@ -0,0 +1,20 @@
+import create from 'zustand';
+
+import { DASHBOARD_PAGE_GUIDES } from 'config/references';
+
+const guideStore = create<{
+ shuffled: boolean;
+ guideLinks: any;
+ shuffle: () => void;
+}>((set) => ({
+ shuffled: false,
+ guideLinks: [{}],
+ shuffle: () => {
+ const guideLinks = DASHBOARD_PAGE_GUIDES;
+ const shuffledLinks = guideLinks
+ .sort(() => 0.5 - Math.random())
+ .slice(0, 4);
+ set({ shuffled: true, guideLinks: shuffledLinks });
+ },
+}));
+export default guideStore;
diff --git a/aim/web/ui/src/pages/Dashboard/components/DashboardRight/ReleaseNotes/ReleaseNotes.scss b/aim/web/ui/src/pages/Dashboard/components/DashboardRight/ReleaseNotes/ReleaseNotes.scss
new file mode 100644
index 0000000000..809db7d6d9
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/DashboardRight/ReleaseNotes/ReleaseNotes.scss
@@ -0,0 +1,114 @@
+@use 'src/styles/abstracts' as *;
+
+.ReleaseNotes {
+ display: flex;
+ flex-direction: column;
+ &__Spinner {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: calc(100vh - 40px);
+ }
+ &__latest {
+ display: flex;
+ flex-direction: column;
+ margin-top: $space-lg;
+ padding: 0 $space-lg $space-unit;
+ border-bottom: $border-dark-lighter;
+ &__title {
+ display: flex;
+ margin-bottom: $space-sm;
+ span {
+ margin-left: $space-xs;
+ padding: $space-xxxxs $space-xxxs;
+ color: $white;
+ background-color: #cc231a;
+ border-radius: $border-radius-xss;
+ font-size: 10px;
+ font-weight: $font-700;
+ }
+ }
+ &__content {
+ display: flex;
+ flex-direction: column;
+ a {
+ text-decoration: none;
+ display: block;
+ }
+ &__item {
+ display: flex;
+ padding-left: 18px;
+ margin-bottom: $space-sm;
+ position: relative;
+ span {
+ word-break: break-word;
+ }
+ &::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 3px;
+ width: 7px;
+ height: 7px;
+ border-radius: $border-radius-circle;
+ background-color: $pico-30;
+ }
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+ }
+ &__changelog {
+ border-bottom: $border-dark-lighter;
+ &__title {
+ padding: $space-sm $space-lg 0;
+ }
+ &__content {
+ margin-top: $space-sm;
+ max-height: 290px;
+ padding: 0 $space-lg $space-unit;
+ overflow: auto;
+ .ReleaseNoteItem {
+ &::after {
+ height: calc(100% + #{$space-sm});
+ }
+ }
+ }
+ &__currentRelease {
+ position: relative;
+ padding: $space-sm $space-lg;
+ .ReleaseNoteItem {
+ &::after {
+ top: -13px;
+ height: calc(100% + #{$space-sm});
+ }
+ }
+ &::before {
+ content: '';
+ transition: all 0.18s ease-out;
+ opacity: 0;
+ position: absolute;
+ left: 0;
+ top: -12px;
+ height: 13px;
+ width: 100%;
+ background: linear-gradient(
+ 180deg,
+ rgba(234, 235, 239, 0) 0%,
+ #eaebef 100%
+ );
+ }
+ &__scroll {
+ &::before {
+ opacity: 1;
+ }
+ .ReleaseNoteItem {
+ &::after {
+ top: -11px !important;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/aim/web/ui/src/pages/Dashboard/components/DashboardRight/ReleaseNotes/ReleaseNotes.tsx b/aim/web/ui/src/pages/Dashboard/components/DashboardRight/ReleaseNotes/ReleaseNotes.tsx
new file mode 100644
index 0000000000..f2e091dff7
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/DashboardRight/ReleaseNotes/ReleaseNotes.tsx
@@ -0,0 +1,107 @@
+import React from 'react';
+
+import { Button, Spinner, Text } from 'components/kit';
+import ReleaseNoteItem from 'components/ReleaseNoteItem/ReleaseNoteItem';
+
+import { AIM_VERSION } from 'config/config';
+
+import GuideLinks from '../GuideDocs/GuideDocs';
+
+import useReleaseNotes from './useReleaseNotes';
+
+import './ReleaseNotes.scss';
+
+function ReleaseNotes(): React.FunctionComponentElement {
+ const {
+ changelogData,
+ LatestReleaseData,
+ currentReleaseData,
+ isLoading,
+ releaseNoteRef,
+ scrollShadow,
+ } = useReleaseNotes();
+
+ return (
+
+ {isLoading ? (
+
+
+
+ ) : (
+ <>
+
+
+
+ Aim {LatestReleaseData?.tagName}
+
+ {`v${AIM_VERSION}` === LatestReleaseData?.tagName ? null : (
+ New
+ )}
+
+
+ {LatestReleaseData?.info?.map((title: string, index: number) => (
+
+ {title.replace(/-/g, ' ')}
+
+ ))}
+
+
+ Full release notes
+
+
+
+
+ {`v${AIM_VERSION}` === LatestReleaseData?.tagName ? null : (
+
+
+ Changelog
+
+
+ {changelogData.map((item) => {
+ return (
+
+ );
+ })}
+
+ {currentReleaseData ? (
+
+
+
+ ) : null}
+
+ )}
+
+ >
+ )}
+
+ );
+}
+
+export default React.memo(ReleaseNotes);
diff --git a/aim/web/ui/src/pages/Dashboard/components/DashboardRight/ReleaseNotes/ReleasesStore.ts b/aim/web/ui/src/pages/Dashboard/components/DashboardRight/ReleaseNotes/ReleasesStore.ts
new file mode 100644
index 0000000000..e92c826733
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/DashboardRight/ReleaseNotes/ReleasesStore.ts
@@ -0,0 +1,11 @@
+import { fetchReleaseNotes } from 'modules/core/api/releaseNotesApi';
+import { IReleaseNote } from 'modules/core/api/releaseNotesApi/types';
+import createResource from 'modules/core/utils/createResource';
+
+function createReleasesEngine() {
+ const { fetchData, state, destroy } =
+ createResource(fetchReleaseNotes);
+ return { fetchReleases: fetchData, releasesState: state, destroy };
+}
+
+export default createReleasesEngine();
diff --git a/aim/web/ui/src/pages/Dashboard/components/DashboardRight/ReleaseNotes/useReleaseNotes.ts b/aim/web/ui/src/pages/Dashboard/components/DashboardRight/ReleaseNotes/useReleaseNotes.ts
new file mode 100644
index 0000000000..e0b81ca143
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/DashboardRight/ReleaseNotes/useReleaseNotes.ts
@@ -0,0 +1,159 @@
+import React from 'react';
+import _ from 'lodash-es';
+import { marked } from 'marked';
+
+import { IReleaseNote } from 'modules/core/api/releaseNotesApi/types';
+import { IResourceState } from 'modules/core/utils/createResource';
+import { fetchReleaseByTagName } from 'modules/core/api/releaseNotesApi';
+
+import { AIM_VERSION } from 'config/config';
+
+import createReleaseNotesEngine from './ReleasesStore';
+
+const CHANGELOG_CONTENT_MAX_HEIGHT = 296;
+function useReleaseNotes() {
+ const [loading, setLoading] = React.useState(true);
+ const [mounted, setMounted] = React.useState(false);
+ const [scrollShadow, setScrollShadow] = React.useState(false);
+ const [currentRelease, setCurrentRelease] = React.useState();
+ const { current: releaseNotesEngine } = React.useRef(
+ createReleaseNotesEngine,
+ );
+ const releaseNotesStore: IResourceState =
+ releaseNotesEngine.releasesState((state) => state);
+ const releaseNoteRef = React.useRef(null);
+
+ React.useEffect(() => {
+ if (releaseNotesStore.data?.length) {
+ // detect current release in fetched release notes
+ const release: IReleaseNote | undefined = releaseNotesStore.data?.find(
+ (release: IReleaseNote) => release.tag_name === `v${AIM_VERSION}`,
+ );
+ if (release) {
+ setCurrentRelease(release);
+ setLoading(false);
+ } else {
+ fetchCurrentRelease();
+ }
+ } else {
+ releaseNotesEngine.fetchReleases();
+ }
+ return () => {
+ releaseNoteRef?.current?.removeEventListener(
+ 'scroll',
+ onChangelogContentScroll,
+ );
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [releaseNotesStore.data]);
+
+ React.useEffect(() => {
+ if (!loading) {
+ setMounted(true);
+ }
+ if (mounted && releaseNoteRef.current) {
+ if (
+ releaseNoteRef?.current?.scrollHeight > CHANGELOG_CONTENT_MAX_HEIGHT
+ ) {
+ setScrollShadow(true);
+ }
+ releaseNoteRef?.current?.addEventListener(
+ 'scroll',
+ onChangelogContentScroll,
+ );
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [loading, mounted]);
+
+ const onChangelogContentScroll = _.throttle(() => {
+ const hasScrollShadow: boolean =
+ releaseNoteRef!.current!.scrollTop + CHANGELOG_CONTENT_MAX_HEIGHT <=
+ releaseNoteRef!.current!.scrollHeight;
+ setScrollShadow(hasScrollShadow);
+ }, 150);
+ async function fetchCurrentRelease(): Promise {
+ try {
+ const data = await fetchReleaseByTagName(`v${AIM_VERSION}`);
+ setCurrentRelease(data);
+ setLoading(false);
+ } catch (e) {
+ setLoading(false);
+ }
+ }
+
+ function getLatestReleaseInfo(releaseBody: string): RegExpMatchArray | null {
+ const wrapper = document.createElement('div');
+ wrapper.innerHTML = marked.parse(releaseBody);
+ const listElements: string[] = [];
+ wrapper.querySelectorAll('li').forEach((li, index) => {
+ if (index < 4) {
+ listElements.push(
+ li.innerText.replace(
+ /(\sby\s\@[A-z\d](?:[A-z\d]|-(?=[A-z\d])){0,38}\s\w+\shttps\:\/\/github\.com\/((\w+\/?){4}))/g,
+ '',
+ ),
+ );
+ } else {
+ return;
+ }
+ });
+ return listElements;
+ }
+
+ // function to modify release notes name
+ function modifyReleaseName(releaseTitle: string): string {
+ return releaseTitle.replace(/(^\🚀\s*v\d+\.\d+\.\d+\s*\-\s*)/, '');
+ }
+
+ const changelogData: { tagName: string; info: any; url: string }[] =
+ React.useMemo(() => {
+ const data: { tagName: string; info: any; url: string }[] = [];
+ releaseNotesStore?.data?.some((release: IReleaseNote) => {
+ data.push({
+ tagName: release.tag_name,
+ info: modifyReleaseName(release.name),
+ url: release.html_url,
+ });
+ if (release.tag_name === `v${AIM_VERSION}`) {
+ return true;
+ }
+ });
+ return data;
+ }, [releaseNotesStore.data]);
+
+ const currentReleaseData:
+ | { tagName: string; info: any; url: string }
+ | undefined = React.useMemo(() => {
+ if (currentRelease) {
+ return {
+ tagName: currentRelease?.tag_name,
+ info: modifyReleaseName(currentRelease?.name),
+ url: currentRelease.html_url,
+ };
+ }
+ }, [currentRelease]);
+
+ const LatestReleaseData:
+ | { tagName: string; info: any; url: string }
+ | undefined = React.useMemo(() => {
+ const latest = releaseNotesStore?.data?.[0];
+ if (latest) {
+ return {
+ tagName: latest?.tag_name,
+ info: getLatestReleaseInfo(latest.body),
+ url: latest.html_url,
+ };
+ }
+ }, [releaseNotesStore.data]);
+
+ return {
+ changelogData,
+ currentReleaseData,
+ LatestReleaseData,
+ isLoading: loading,
+ releaseNoteRef,
+ scrollShadow,
+ };
+}
+
+export default useReleaseNotes;
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/DashboardBookmarks/DashboardBookmarks.scss b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/DashboardBookmarks/DashboardBookmarks.scss
new file mode 100644
index 0000000000..248e482053
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/DashboardBookmarks/DashboardBookmarks.scss
@@ -0,0 +1,29 @@
+@use 'src/styles/abstracts' as *;
+
+$bookmark-height: 24px;
+.DashboardBookmarks {
+ padding: $space-sm;
+ border-top: $border-dark-lighter;
+
+ &__title {
+ padding: 0 $space-sm;
+ margin-bottom: $space-xs;
+ }
+ &__list {
+ max-height: 5 * $bookmark-height;
+ overflow: auto;
+ &__ListItem {
+ &__Text {
+ flex: 1;
+ margin-left: $space-xs;
+ text-transform: capitalize;
+ }
+ }
+ }
+ &__NavLink {
+ margin-top: $space-unit;
+ text-decoration: none;
+ display: block;
+ padding: 0 $space-sm;
+ }
+}
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/DashboardBookmarks/DashboardBookmarks.tsx b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/DashboardBookmarks/DashboardBookmarks.tsx
new file mode 100644
index 0000000000..97b9754197
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/DashboardBookmarks/DashboardBookmarks.tsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import { NavLink } from 'react-router-dom';
+
+import { IDashboardData } from 'modules/core/api/dashboardsApi';
+import { Tooltip } from '@material-ui/core';
+
+import { Button, Icon, Text } from 'components/kit';
+import ListItem from 'components/kit/ListItem/ListItem';
+
+import { BookmarkIconType } from 'pages/Bookmarks/components/BookmarkCard/BookmarkCard';
+
+import useDashboardBookmarks from './useDashboardBookmarks';
+
+import './DashboardBookmarks.scss';
+
+function DashboardBookmarks(): React.FunctionComponentElement | null {
+ const { dashboardBookmarksStore, handleClick } = useDashboardBookmarks();
+
+ return dashboardBookmarksStore.data?.length ? (
+
+
+ Bookmarks{' '}
+ {dashboardBookmarksStore.data.length
+ ? `(${dashboardBookmarksStore.data.length})`
+ : ''}
+
+
+ {dashboardBookmarksStore.data
+ .slice(0, 5)
+ ?.map((dashboard: IDashboardData) => (
+
+
+
+
+ handleClick(e, dashboard)}
+ size={12}
+ tint={100}
+ >
+ {dashboard.name}
+
+
+ handleClick(e, dashboard, true)}
+ name='new-tab'
+ />
+
+
+
+
+ ))}
+
+ {dashboardBookmarksStore.data.length > 5 ? (
+
+
+ See all bookmarks
+
+
+ ) : null}
+
+ ) : null;
+}
+
+export default React.memo(DashboardBookmarks);
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/DashboardBookmarks/DashboardBookmarksStore.ts b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/DashboardBookmarks/DashboardBookmarksStore.ts
new file mode 100644
index 0000000000..85c86fa6ba
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/DashboardBookmarks/DashboardBookmarksStore.ts
@@ -0,0 +1,17 @@
+import {
+ fetchDashboardsList,
+ IDashboardData,
+} from 'modules/core/api/dashboardsApi';
+import createResource from 'modules/core/utils/createResource';
+
+function createDashboardBookmarksEngine() {
+ const { fetchData, state, destroy } =
+ createResource(fetchDashboardsList);
+ return {
+ fetchDashboardBookmarks: fetchData,
+ dashboardBookmarksState: state,
+ destroy,
+ };
+}
+
+export default createDashboardBookmarksEngine();
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/DashboardBookmarks/useDashboardBookmarks.tsx b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/DashboardBookmarks/useDashboardBookmarks.tsx
new file mode 100644
index 0000000000..f7d0e4e508
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/DashboardBookmarks/useDashboardBookmarks.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import { useHistory } from 'react-router-dom';
+
+import { IResourceState } from 'modules/core/utils/createResource';
+import { IDashboardData } from 'modules/core/api/dashboardsApi';
+
+import createBookmarksEngine from './DashboardBookmarksStore';
+
+function useDashboardBookmarks() {
+ const history = useHistory();
+ const { current: dashboardBookmarksEngine } = React.useRef(
+ createBookmarksEngine,
+ );
+ const dashboardBookmarksStore: IResourceState =
+ dashboardBookmarksEngine.dashboardBookmarksState((state) => state);
+
+ React.useEffect(() => {
+ dashboardBookmarksEngine.fetchDashboardBookmarks();
+ return () => {
+ dashboardBookmarksEngine.destroy();
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const handleClick: (
+ e: React.MouseEvent,
+ dashboard: IDashboardData,
+ newTab?: boolean,
+ ) => void = React.useCallback(
+ (
+ e: React.MouseEvent,
+ dashboard: IDashboardData,
+ newTab = false,
+ ) => {
+ e.stopPropagation();
+ if (dashboard) {
+ const path = `${dashboard.app_type}/${dashboard.app_id}`;
+ if (newTab) {
+ window.open(path, '_blank');
+ window.focus();
+ return;
+ }
+ history.push(path);
+ }
+ },
+ [history],
+ );
+
+ return { handleClick, dashboardBookmarksStore };
+}
+
+export default useDashboardBookmarks;
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExperimentsCard/ExperimentsCard.scss b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExperimentsCard/ExperimentsCard.scss
new file mode 100644
index 0000000000..db26735228
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExperimentsCard/ExperimentsCard.scss
@@ -0,0 +1,10 @@
+@use 'src/styles/abstracts' as *;
+
+.ExperimentsCard {
+ border-top: $border-dark-lighter;
+ padding: $space-unit $space-sm;
+ &__title {
+ margin-bottom: $space-sm;
+ padding: 0 $space-sm;
+ }
+}
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExperimentsCard/ExperimentsCard.tsx b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExperimentsCard/ExperimentsCard.tsx
new file mode 100644
index 0000000000..6d1e3f8caf
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExperimentsCard/ExperimentsCard.tsx
@@ -0,0 +1,63 @@
+import React from 'react';
+
+import DataList from 'components/kit/DataList';
+import { Text } from 'components/kit';
+
+import CompareSelectedRunsPopover from 'pages/Metrics/components/Table/CompareSelectedRunsPopover';
+
+import { AppNameEnum } from 'services/models/explorer';
+
+import useExperimentsCard from './useExperimentsCard';
+
+import './ExperimentsCard.scss';
+
+function ExperimentsCard(): React.FunctionComponentElement | null {
+ const {
+ tableRef,
+ tableColumns,
+ tableData,
+ experimentsStore,
+ selectedRows,
+ experimentsQuery,
+ } = useExperimentsCard();
+ return experimentsStore.data?.length ? (
+
+
+ Experiments ({experimentsStore.data.length})
+
+ ,
+ ]}
+ disableMatchBar={true}
+ />
+
+ ) : null;
+}
+
+ExperimentsCard.displayName = 'ExperimentsCard';
+
+export default React.memo(ExperimentsCard);
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExperimentsCard/ExperimentsStore.ts b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExperimentsCard/ExperimentsStore.ts
new file mode 100644
index 0000000000..93b9d930cc
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExperimentsCard/ExperimentsStore.ts
@@ -0,0 +1,13 @@
+import {
+ getExperiments,
+ IExperimentData,
+} from 'modules/core/api/experimentsApi';
+import createResource from 'modules/core/utils/createResource';
+
+function createExperimentsEngine() {
+ const { fetchData, state, destroy } =
+ createResource(getExperiments);
+ return { fetchExperiments: fetchData, experimentsState: state, destroy };
+}
+
+export default createExperimentsEngine();
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExperimentsCard/index.ts b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExperimentsCard/index.ts
new file mode 100644
index 0000000000..8a21d7de4d
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExperimentsCard/index.ts
@@ -0,0 +1,3 @@
+import ExperimentsCard from './ExperimentsCard';
+
+export default ExperimentsCard;
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExperimentsCard/useExperimentsCard.tsx b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExperimentsCard/useExperimentsCard.tsx
new file mode 100644
index 0000000000..be96711e33
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExperimentsCard/useExperimentsCard.tsx
@@ -0,0 +1,183 @@
+import React from 'react';
+import _ from 'lodash-es';
+
+import { IExperimentData } from 'modules/core/api/experimentsApi';
+import { IResourceState } from 'modules/core/utils/createResource';
+import { Checkbox } from '@material-ui/core';
+
+import { Icon, Text } from 'components/kit';
+
+import createExperimentEngine from './ExperimentsStore';
+
+function useExperimentsCard() {
+ const tableRef = React.useRef(null);
+ const [selectedRows, setSelectedRows] = React.useState([]);
+ const { current: experimentsEngine } = React.useRef(createExperimentEngine);
+ const experimentsStore: IResourceState =
+ experimentsEngine.experimentsState((state) => state);
+
+ React.useEffect(() => {
+ experimentsEngine.fetchExperiments();
+ return () => {
+ experimentsEngine.destroy();
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ // memoized table data
+ const tableData: {
+ key: number;
+ name: string;
+ archived: boolean;
+ run_count: number;
+ id: string;
+ }[] = React.useMemo(() => {
+ if (experimentsStore.data) {
+ return experimentsStore.data.map(
+ ({ name, archived, run_count }: any, index: number) => {
+ return {
+ key: index,
+ name: name,
+ archived,
+ run_count,
+ id: name,
+ };
+ },
+ );
+ }
+ return [];
+ }, [experimentsStore.data]);
+
+ // on row selection
+ const onRowSelect = React.useCallback(
+ (rowKey?: string) => {
+ if (rowKey) {
+ const newSelectedRows = selectedRows.includes(rowKey)
+ ? selectedRows.filter((row: string) => row !== rowKey)
+ : [...selectedRows, rowKey];
+ setSelectedRows(newSelectedRows);
+ } else if (selectedRows.length) {
+ setSelectedRows([]);
+ } else {
+ setSelectedRows(tableData.map(({ name }: any) => name));
+ }
+ },
+ [selectedRows, tableData],
+ );
+
+ // memoized table columns
+ const tableColumns = React.useMemo(
+ () => [
+ {
+ dataKey: 'id',
+ key: 'id',
+ title: (
+ }
+ className='selectCheckbox'
+ checkedIcon={
+ tableData.length === Object.keys(selectedRows)?.length ? (
+
+
+
+ ) : (
+
+
+
+ )
+ }
+ onClick={() => onRowSelect()}
+ checked={!!selectedRows.length}
+ />
+ ),
+ width: '20px',
+ cellRenderer: ({ cellData }: any) => {
+ return (
+ }
+ className='selectCheckbox'
+ checked={selectedRows.includes(cellData)}
+ checkedIcon={
+
+
+
+ }
+ onClick={() => onRowSelect(cellData)}
+ />
+ );
+ },
+ },
+ {
+ dataKey: 'name',
+ key: 'name',
+ title: (
+
+ Name
+
+ ),
+ width: 'calc(100% - 50px)',
+ style: { paddingLeft: 10, paddingRight: 12 },
+ cellRenderer: ({ cellData }: any) => (
+
+ {cellData}
+
+ ),
+ },
+ {
+ dataKey: 'run_count',
+ key: 'run_count',
+ title: (
+
+ Runs
+
+ ),
+ flexGrow: 1,
+ width: '46px',
+ textAlign: 'right',
+ style: { textAlign: 'right' },
+ cellRenderer: ({ cellData }: any) => (
+
+ {cellData}
+
+ ),
+ },
+ ],
+ [tableData?.length, onRowSelect, selectedRows],
+ );
+
+ // Update the table data and columns when the experiments data changes
+ React.useEffect(() => {
+ if (tableRef.current?.updateData) {
+ tableRef.current.updateData({
+ newColumns: tableColumns,
+ newData: tableData,
+ });
+ }
+ }, [tableData, tableColumns]);
+
+ const experimentsQuery = React.useMemo(() => {
+ return `run.experiment in [${_.uniq(selectedRows)
+ .map((val: string) => `"${val}"`)
+ .join(',')}]`;
+ }, [selectedRows]);
+
+ return {
+ tableRef,
+ tableColumns,
+ tableData,
+ experimentsStore,
+ selectedRows,
+ experimentsQuery,
+ };
+}
+export default useExperimentsCard;
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExploreSection.scss b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExploreSection.scss
new file mode 100644
index 0000000000..d074275047
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExploreSection.scss
@@ -0,0 +1,135 @@
+@use 'src/styles/abstracts' as *;
+
+.ExploreSection {
+ display: flex;
+ flex-direction: column;
+ min-width: 310px;
+ width: 310px;
+ background-color: $pico-2;
+ overflow: auto;
+ height: 100vh;
+ &__Spinner {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ &__loading {
+ display: none;
+ }
+ &__title {
+ margin-top: 1.25rem;
+ padding: 0 1.5rem;
+ }
+ .DataList {
+ .CompareSelectedRunsPopover__trigger {
+ margin-right: 0 !important;
+ .Icon__container {
+ margin-right: $space-xxxs;
+ }
+ .Text {
+ font-size: $text-sm;
+ }
+ }
+
+ .IllustrationBlock {
+ &__container {
+ padding-bottom: 0;
+ }
+ &__large__img {
+ height: 100%;
+ img {
+ height: 100%;
+ }
+ }
+ }
+ .defaultSelectIcon {
+ border: 1.5px solid $pico-50;
+ border-radius: $border-radius-xs;
+ width: 12px;
+ height: 12px;
+ }
+ &__toolbarItems {
+ height: unset;
+ margin-left: $space-xs;
+ }
+ &__textsTable {
+ border: none;
+ padding-left: $space-xs;
+ }
+ .SearchBar {
+ width: auto;
+ min-width: 250px;
+ padding: 0 $space-sm;
+ margin-bottom: $space-xxxs;
+ .SearchInput {
+ margin-right: 0;
+ }
+ .MuiOutlinedInput-root {
+ background-color: $white;
+ }
+ label {
+ font-size: $text-sm;
+ }
+ }
+ .selectedSelectIcon {
+ border-radius: $border-radius-xs;
+ width: 12px;
+ height: 12px;
+ background: $primary-color;
+ color: $white;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ .partiallySelectedSelectIcon {
+ border-radius: $border-radius-xs;
+ width: 12px;
+ height: 12px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ .MuiCheckbox-root {
+ padding: $space-xxxs !important;
+ }
+ .BaseTable {
+ background-color: transparent;
+ &__table {
+ background-color: transparent;
+ }
+ &__header {
+ background: transparent;
+ &-row {
+ background-color: transparent;
+ }
+ &-cell {
+ border: none;
+ background-color: transparent;
+ padding: 0;
+ &--text {
+ font-size: 12px;
+ }
+ }
+ }
+ &__row {
+ background-color: transparent;
+ box-shadow: unset;
+ &-cell {
+ background-color: transparent;
+ border: none;
+ padding: 0;
+ &:first-child {
+ padding: 0;
+ }
+ p {
+ max-width: 100%;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExploreSection.tsx b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExploreSection.tsx
new file mode 100644
index 0000000000..6f8b0fe952
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/ExploreSection.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+
+import { Spinner, Text } from 'components/kit';
+
+import ExperimentsCard from './ExperimentsCard';
+import DashboardBookmarks from './DashboardBookmarks/DashboardBookmarks';
+import QuickLinks from './QuickLinks/QuickLinks';
+import TagsCard from './TagsCard/TagsCard';
+import RecentSearches from './RecentSearches/RecentSearches';
+import createTagsEngine from './TagsCard/TagsStore';
+import createExperimentsEngine from './ExperimentsCard/ExperimentsStore';
+import bookmarksEngine from './DashboardBookmarks/DashboardBookmarksStore';
+
+import './ExploreSection.scss';
+
+function ExploreSection(): React.FunctionComponentElement {
+ const [loading, setLoading] = React.useState(true);
+ const { loading: tagsLoading } = React.useRef(
+ createTagsEngine,
+ ).current.tagsState((state) => state);
+ const { loading: experimentsLoading } = React.useRef(
+ createExperimentsEngine,
+ ).current.experimentsState((state) => state);
+ const { loading: bookmarksLoading } = React.useRef(
+ bookmarksEngine,
+ ).current.dashboardBookmarksState((state) => state);
+
+ React.useEffect(() => {
+ // if all resources are loaded
+ if (!tagsLoading && !experimentsLoading && !bookmarksLoading) {
+ setLoading(false);
+ }
+ }, [bookmarksLoading, experimentsLoading, tagsLoading]);
+
+ return (
+
+
+ Explore
+
+
+
+ {loading && (
+
+
+
+ )}
+
+
+
+
+
+
+ );
+}
+
+export default React.memo(ExploreSection);
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/QuickLinks/QuickLinks.scss b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/QuickLinks/QuickLinks.scss
new file mode 100644
index 0000000000..bf3b38c7a3
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/QuickLinks/QuickLinks.scss
@@ -0,0 +1,17 @@
+@use 'src/styles/abstracts' as *;
+
+.QuickLinks {
+ padding: $space-lg $space-sm $space-unit;
+ &__title {
+ margin-bottom: $space-xs;
+ padding: 0 $space-sm;
+ }
+ &__list {
+ margin-top: $space-md $space-lg;
+ &__ListItem {
+ &__Text {
+ flex: 1;
+ }
+ }
+ }
+}
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/QuickLinks/QuickLinks.tsx b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/QuickLinks/QuickLinks.tsx
new file mode 100644
index 0000000000..edbe8aa1ba
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/QuickLinks/QuickLinks.tsx
@@ -0,0 +1,112 @@
+import React from 'react';
+import { useHistory } from 'react-router-dom';
+import moment from 'moment';
+
+import { Tooltip } from '@material-ui/core';
+
+import { Icon, Text } from 'components/kit';
+import ListItem from 'components/kit/ListItem/ListItem';
+
+import { DATE_QUERY_FORMAT } from 'config/dates/dates';
+
+import { encode } from 'utils/encoder/encoder';
+
+import './QuickLinks.scss';
+
+const linkItems: { path: string; label: string }[] = [
+ {
+ path: 'active',
+ label: 'Active runs',
+ },
+ {
+ path: 'archived',
+ label: 'Archived runs',
+ },
+ {
+ path: 'latest',
+ label: "Last week's runs",
+ },
+];
+
+function QuickLinks(): React.FunctionComponentElement {
+ const history = useHistory();
+
+ const onClick: (
+ e: React.MouseEvent,
+ value: string,
+ newTab?: boolean,
+ ) => void = React.useCallback(
+ (e: React.MouseEvent, value: string, newTab = false) => {
+ e.stopPropagation();
+ if (value) {
+ let search = {};
+ if (value === 'latest') {
+ search = encode({
+ query: `datetime(${moment()
+ .subtract(7, 'day')
+ .format(
+ DATE_QUERY_FORMAT,
+ )}) <= run.created_at < datetime(${moment().format(
+ DATE_QUERY_FORMAT,
+ )})`,
+ });
+ } else {
+ search = encode({
+ query: `run.${value.toLowerCase()} == True`,
+ });
+ }
+ const path = `/runs?select=${search}`;
+ if (newTab) {
+ window.open(path, '_blank');
+ window.focus();
+ return;
+ }
+ history.push(path);
+ }
+ },
+ [history],
+ );
+
+ return (
+
+
+ Quick Navigation
+
+
+ {linkItems.map(({ label, path }) => (
+
+ onClick(e, path)}
+ size={12}
+ tint={100}
+ >
+ {label}
+
+
+
+ onClick(e, path, true)}
+ name='new-tab'
+ />
+
+
+
+ ))}
+
+
+ );
+}
+
+export default QuickLinks;
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/RecentSearches/RecentSearchItem.tsx b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/RecentSearches/RecentSearchItem.tsx
new file mode 100644
index 0000000000..9a237db849
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/RecentSearches/RecentSearchItem.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { useHistory } from 'react-router-dom';
+
+import { Tooltip } from '@material-ui/core';
+
+import ListItem from 'components/kit/ListItem/ListItem';
+import { Icon } from 'components/kit';
+
+import useCodeHighlighter from 'hooks/useCodeHighlighter';
+
+import { BookmarkIconType } from 'pages/Bookmarks/components/BookmarkCard/BookmarkCard';
+
+import { encode } from 'utils/encoder/encoder';
+
+function RecentSearchItem({
+ query,
+ explorer,
+}: {
+ query: string;
+ explorer: string;
+}) {
+ const { elementRef } = useCodeHighlighter();
+ const history = useHistory();
+
+ const onClick: (e: React.MouseEvent, newTab?: boolean) => void =
+ React.useCallback(
+ (e: React.MouseEvent, newTab?: boolean) => {
+ e.stopPropagation();
+ const search = encode({
+ query,
+ advancedMode: true,
+ advancedQuery: query,
+ });
+ const path = `/${explorer}?select=${search}`;
+ if (newTab) {
+ window.open(path, '_blank');
+ window.focus();
+ return;
+ }
+ history.push(path);
+ },
+ [explorer, history, query],
+ );
+ return (
+
+
+
onClick(e)}>
+
+
+ {query}
+
+ onClick(e, true)}
+ name='new-tab'
+ />
+
+
+
+ );
+}
+export default React.memo(RecentSearchItem);
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/RecentSearches/RecentSearches.scss b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/RecentSearches/RecentSearches.scss
new file mode 100644
index 0000000000..b7e6818470
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/RecentSearches/RecentSearches.scss
@@ -0,0 +1,21 @@
+@use 'src/styles/abstracts' as *;
+
+.RecentSearches {
+ padding: 0 $space-sm $space-unit;
+ &__title {
+ margin-bottom: $space-xs;
+ padding: 0 $space-sm;
+ }
+
+ pre {
+ max-width: 100%;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ font-size: 15px;
+ font-weight: $font-600;
+ @include monospaceFontFamily(15);
+ flex: 1 1;
+ margin: 0 $space-xs;
+ }
+}
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/RecentSearches/RecentSearches.tsx b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/RecentSearches/RecentSearches.tsx
new file mode 100644
index 0000000000..af23102f32
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/RecentSearches/RecentSearches.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+
+import { Text } from 'components/kit';
+
+import { getItem } from 'utils/storage';
+
+import RecentSearchItem from './RecentSearchItem';
+
+import './RecentSearches.scss';
+
+function RecentSearches(): React.FunctionComponentElement | null {
+ const [recentSearches, setRecentSearches] = React.useState<
+ { explorer: string; query: string }[]
+ >([]);
+
+ React.useEffect(() => {
+ const recent = getItem('recentSearches');
+ if (recent) {
+ setRecentSearches(JSON.parse(recent));
+ }
+ }, []);
+ return recentSearches.length ? (
+
+
+ Recent Searches
+
+
+ {recentSearches.map((item, index) => (
+
+ ))}
+
+
+ ) : null;
+}
+
+export default React.memo(RecentSearches);
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/RecentSearches/useRecentSearches.ts b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/RecentSearches/useRecentSearches.ts
new file mode 100644
index 0000000000..56cfc9fe3e
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/RecentSearches/useRecentSearches.ts
@@ -0,0 +1,5 @@
+function useRecentSearches() {
+ return null;
+}
+
+export default useRecentSearches;
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/TagsCard/TagsCard.d.ts b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/TagsCard/TagsCard.d.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/TagsCard/TagsCard.scss b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/TagsCard/TagsCard.scss
new file mode 100644
index 0000000000..1f3a57025b
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/TagsCard/TagsCard.scss
@@ -0,0 +1,19 @@
+@use 'src/styles/abstracts' as *;
+
+.TagsCard {
+ border-top: $border-dark-lighter;
+ padding: 1rem;
+ padding: $space-unit $space-sm;
+ &__title {
+ margin-bottom: $space-sm;
+ padding: 0 $space-sm;
+ }
+ .Badge {
+ border-radius: $border-radius-sm;
+ }
+ &__NavLink {
+ text-decoration: none;
+ display: block;
+ margin-top: $space-unit;
+ }
+}
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/TagsCard/TagsCard.tsx b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/TagsCard/TagsCard.tsx
new file mode 100644
index 0000000000..dc00ebee0c
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/TagsCard/TagsCard.tsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import { NavLink } from 'react-router-dom';
+
+import DataList from 'components/kit/DataList';
+import { Button, Text } from 'components/kit';
+
+import CompareSelectedRunsPopover from 'pages/Metrics/components/Table/CompareSelectedRunsPopover';
+
+import { AppNameEnum } from 'services/models/explorer';
+
+import useTagsCard from './useTagsCard';
+
+import './TagsCard.scss';
+
+function TagsCard(): React.FunctionComponentElement | null {
+ const {
+ tableRef,
+ tableColumns,
+ tableData,
+ tagsStore,
+ selectedRows,
+ tagsQuery,
+ } = useTagsCard();
+
+ return (
+
+
+ Tags {tagsStore?.data?.length ? `(${tagsStore?.data?.length})` : ''}
+
+ {tagsStore?.data?.length ? (
+ ,
+ ]}
+ />
+ ) : null}
+
+
+ {tagsStore?.data?.length ? 'See all tags' : 'Create a new tag'}
+
+
+
+ );
+}
+
+TagsCard.displayName = 'TagsCard';
+
+export default React.memo(TagsCard);
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/TagsCard/TagsStore.ts b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/TagsCard/TagsStore.ts
new file mode 100644
index 0000000000..57e39ee43c
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/TagsCard/TagsStore.ts
@@ -0,0 +1,11 @@
+import createResource from 'modules/core/utils/createResource';
+import { fetchTagsList } from 'modules/core/api/tagsApi';
+import { ITagData } from 'modules/core/api/tagsApi/types';
+
+function createTagsEngine() {
+ const { fetchData, state, destroy } =
+ createResource(fetchTagsList);
+ return { fetchTags: fetchData, tagsState: state, destroy };
+}
+
+export default createTagsEngine();
diff --git a/aim/web/ui/src/pages/Dashboard/components/ExploreSection/TagsCard/useTagsCard.tsx b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/TagsCard/useTagsCard.tsx
new file mode 100644
index 0000000000..c10cb48a25
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ExploreSection/TagsCard/useTagsCard.tsx
@@ -0,0 +1,176 @@
+import React from 'react';
+import _ from 'lodash-es';
+
+import { IResourceState } from 'modules/core/utils/createResource';
+import { Checkbox } from '@material-ui/core';
+import { ITagData } from 'modules/core/api/tagsApi/types';
+
+import { Badge, Icon, Text } from 'components/kit';
+
+import createTagsEngine from './TagsStore';
+
+function useTagsCard() {
+ const tableRef = React.useRef(null);
+ const [selectedRows, setSelectedRows] = React.useState([]);
+ const { current: tagsEngine } = React.useRef(createTagsEngine);
+ const tagsStore: IResourceState = tagsEngine.tagsState(
+ (state) => state,
+ );
+
+ React.useEffect(() => {
+ tagsEngine.fetchTags();
+ return () => {
+ tagsEngine.destroy();
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ // memoized table data
+ const tableData = React.useMemo(() => {
+ if (tagsStore.data) {
+ return tagsStore.data.map(
+ ({ name, archived, run_count, color }: ITagData) => {
+ return {
+ key: name,
+ name,
+ color,
+ archived,
+ run_count,
+ id: name,
+ };
+ },
+ );
+ }
+ return [];
+ }, [tagsStore.data]);
+
+ // on row selection
+ const onRowSelect = React.useCallback(
+ (rowKey?: string) => {
+ if (rowKey) {
+ const newSelectedRows = selectedRows.includes(rowKey)
+ ? selectedRows.filter((row: string) => row !== rowKey)
+ : [...selectedRows, rowKey];
+ setSelectedRows(newSelectedRows);
+ } else if (selectedRows.length) {
+ setSelectedRows([]);
+ } else {
+ setSelectedRows(tableData.map(({ name }: any) => name.label));
+ }
+ },
+ [selectedRows, tableData],
+ );
+
+ // memoized table columns
+ const tableColumns = React.useMemo(
+ () => [
+ {
+ dataKey: 'id',
+ key: 'id',
+ title: (
+ }
+ className='selectCheckbox'
+ checkedIcon={
+ tableData.length === Object.keys(selectedRows)?.length ? (
+
+
+
+ ) : (
+
+
+
+ )
+ }
+ onClick={() => onRowSelect()}
+ checked={!!selectedRows.length}
+ />
+ ),
+ width: '20px',
+ cellRenderer: ({ cellData }: any, index: number) => {
+ return (
+ }
+ className='selectCheckbox'
+ checked={selectedRows.includes(cellData)}
+ checkedIcon={
+
+
+
+ }
+ onClick={() => onRowSelect(cellData)}
+ />
+ );
+ },
+ },
+ {
+ dataKey: 'name',
+ key: 'name',
+ title: (
+
+ Name
+
+ ),
+ width: 'calc(100% - 50px)',
+ style: { paddingLeft: 10, paddingRight: 12 },
+ cellRenderer: ({ cellData, rowData: { color } }: any) => (
+
+ ),
+ },
+ {
+ dataKey: 'run_count',
+ key: 'run_count',
+ title: (
+
+ Runs
+
+ ),
+ flexGrow: 1,
+ style: { textAlign: 'right' },
+ width: '46px',
+ cellRenderer: ({ cellData }: any) => (
+
+ {cellData}
+
+ ),
+ },
+ ],
+ [tableData?.length, onRowSelect, selectedRows],
+ );
+
+ // Update the table data and columns when the tags data changes
+ React.useEffect(() => {
+ if (tableRef.current?.updateData) {
+ tableRef.current.updateData({
+ newColumns: tableColumns,
+ newData: tableData,
+ });
+ }
+ }, [tableData, tableColumns]);
+
+ const tagsQuery = React.useMemo(() => {
+ return `any([t in [${_.uniq(selectedRows)
+ .map((val: string) => `"${val}"`)
+ .join(',')}] for t in run.tags])`;
+ }, [selectedRows]);
+
+ return {
+ tableRef,
+ tableColumns,
+ tableData,
+ tagsStore,
+ selectedRows,
+ tagsQuery,
+ };
+}
+export default useTagsCard;
diff --git a/aim/web/ui/src/pages/Home/components/Activity/Activity.scss b/aim/web/ui/src/pages/Dashboard/components/ProjectContributions/ProjectContributions.scss
similarity index 68%
rename from aim/web/ui/src/pages/Home/components/Activity/Activity.scss
rename to aim/web/ui/src/pages/Dashboard/components/ProjectContributions/ProjectContributions.scss
index 23b4e36079..e5e9c7cc93 100644
--- a/aim/web/ui/src/pages/Home/components/Activity/Activity.scss
+++ b/aim/web/ui/src/pages/Dashboard/components/ProjectContributions/ProjectContributions.scss
@@ -1,12 +1,21 @@
-.Activity {
+@use 'src/styles/abstracts' as *;
+
+.ProjectContributions {
+ margin: 1.75rem 0;
+ &__HeatMap {
+ display: flex;
+ overflow: hidden;
+ @media only screen and (max-width: 1452px) {
+ justify-content: flex-end;
+ }
+ }
&__Statistics__card {
background: linear-gradient(97.73deg, #8c32af 0%, #6bace5 100%);
opacity: 0.8;
- border-radius: 6px;
+ border-radius: $border-radius-main;
width: 13.125em;
height: 80px;
- color: #ffffff;
- padding: 0.5em 1em;
+ color: $white;
position: relative;
span {
display: block;
diff --git a/aim/web/ui/src/pages/Dashboard/components/ProjectContributions/ProjectContributions.tsx b/aim/web/ui/src/pages/Dashboard/components/ProjectContributions/ProjectContributions.tsx
new file mode 100644
index 0000000000..7bede8889f
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ProjectContributions/ProjectContributions.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+
+import HeatMap from 'components/HeatMap/HeatMap';
+import { Text } from 'components/kit';
+import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary';
+
+import { ANALYTICS_EVENT_KEYS } from 'config/analytics/analyticsKeysMap';
+
+import { trackEvent } from 'services/analytics';
+
+import useProjectContributions from './useProjectContributions';
+
+import './ProjectContributions.scss';
+
+function ProjectContributions(): React.FunctionComponentElement {
+ const { projectContributionsStore } = useProjectContributions();
+ function shiftDate(date: any, numDays: any) {
+ const newDate = new Date(date);
+ newDate.setDate(newDate.getDate() + numDays);
+ return newDate;
+ }
+ let today = new Date();
+ return (
+
+
+
+ Contributions
+
+
+ {
+ trackEvent(ANALYTICS_EVENT_KEYS.dashboard.activityCellClick);
+ }}
+ data={Object.keys(
+ projectContributionsStore.data?.activity_map ?? {},
+ ).map((k) => [
+ new Date(k),
+ projectContributionsStore.data?.activity_map[k],
+ ])}
+ />
+
+
+
+ );
+}
+export default React.memo(ProjectContributions);
diff --git a/aim/web/ui/src/pages/Dashboard/components/ProjectContributions/ProjectContributionsStore.ts b/aim/web/ui/src/pages/Dashboard/components/ProjectContributions/ProjectContributionsStore.ts
new file mode 100644
index 0000000000..2a133c6593
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ProjectContributions/ProjectContributionsStore.ts
@@ -0,0 +1,17 @@
+import {
+ getProjectContributions,
+ GetProjectContributionsResult,
+} from 'modules/core/api/projectApi';
+import createResource from 'modules/core/utils/createResource';
+
+function projectContributionsEngine() {
+ const { fetchData, state, destroy } =
+ createResource(getProjectContributions);
+ return {
+ fetchProjectContributions: fetchData,
+ projectContributionsState: state,
+ destroy,
+ };
+}
+
+export default projectContributionsEngine();
diff --git a/aim/web/ui/src/pages/Dashboard/components/ProjectContributions/useProjectContributions.tsx b/aim/web/ui/src/pages/Dashboard/components/ProjectContributions/useProjectContributions.tsx
new file mode 100644
index 0000000000..c13a19fb36
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ProjectContributions/useProjectContributions.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+
+import { GetProjectContributionsResult } from 'modules/core/api/projectApi';
+import { IResourceState } from 'modules/core/utils/createResource';
+
+import projectContributionsEngine from './ProjectContributionsStore';
+
+function useProjectContributions() {
+ const { current: engine } = React.useRef(projectContributionsEngine);
+ const projectContributionsStore: IResourceState =
+ engine.projectContributionsState((state) => state);
+
+ React.useEffect(() => {
+ if (!projectContributionsStore.data) {
+ engine.fetchProjectContributions();
+ }
+ return () => {
+ engine.destroy();
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return {
+ projectContributionsStore,
+ };
+}
+
+export default useProjectContributions;
diff --git a/aim/web/ui/src/pages/Dashboard/components/ProjectStatistics/ProjectStatistics.d.ts b/aim/web/ui/src/pages/Dashboard/components/ProjectStatistics/ProjectStatistics.d.ts
new file mode 100644
index 0000000000..1535cf12fc
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ProjectStatistics/ProjectStatistics.d.ts
@@ -0,0 +1,10 @@
+import { IconName } from 'components/kit/Icon';
+
+export interface IProjectStatistic {
+ label: string;
+ count: number;
+ iconBgColor?: string;
+ icon?: IconName;
+ navLink?: string;
+ title?: string;
+}
diff --git a/aim/web/ui/src/pages/Dashboard/components/ProjectStatistics/ProjectStatistics.scss b/aim/web/ui/src/pages/Dashboard/components/ProjectStatistics/ProjectStatistics.scss
new file mode 100644
index 0000000000..5e98dce169
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ProjectStatistics/ProjectStatistics.scss
@@ -0,0 +1,20 @@
+@use 'src/styles/abstracts' as *;
+
+.ProjectStatistics {
+ &__totalRuns {
+ margin-top: $space-lg;
+ }
+ &__trackedSequences {
+ margin-top: $space-lg;
+ }
+ &__cards {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: $space-unit;
+ margin-top: $space-sm;
+ }
+ &__bar {
+ margin-top: $space-sm;
+ }
+}
diff --git a/aim/web/ui/src/pages/Dashboard/components/ProjectStatistics/ProjectStatistics.tsx b/aim/web/ui/src/pages/Dashboard/components/ProjectStatistics/ProjectStatistics.tsx
new file mode 100644
index 0000000000..b500cbbdd4
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ProjectStatistics/ProjectStatistics.tsx
@@ -0,0 +1,246 @@
+import * as React from 'react';
+
+import { Text } from 'components/kit';
+import StatisticsCard from 'components/StatisticsCard';
+import StatisticsBar from 'components/StatisticsBar';
+
+import routes from 'routes/routes';
+
+import { SequenceTypesEnum } from 'types/core/enums';
+
+import { encode } from 'utils/encoder/encoder';
+
+import { IProjectStatistic, useProjectStatistics } from '.';
+
+import './ProjectStatistics.scss';
+
+const statisticsInitialMap: Record = {
+ [SequenceTypesEnum.Metric]: {
+ label: 'Metrics',
+ count: 0,
+ icon: 'metrics',
+ iconBgColor: '#7A4CE0',
+ navLink: routes.METRICS.path,
+ title: 'Metrics Explorer',
+ },
+ systemMetrics: {
+ label: 'Sys. metrics',
+ count: 0,
+ icon: 'metrics',
+ iconBgColor: '#AF4EAB',
+ navLink: `${routes.METRICS.path}?select=${encode({
+ advancedQuery: "metric.name.startswith('__system__') == True",
+ advancedMode: true,
+ })}`,
+ title: 'Metrics Explorer',
+ },
+ [SequenceTypesEnum.Figures]: {
+ label: 'Figures',
+ icon: 'figures-explorer',
+ count: 0,
+ iconBgColor: '#18AB6D',
+ navLink: routes.FIGURES_EXPLORER.path,
+ title: 'Figures Explorer',
+ },
+ [SequenceTypesEnum.Images]: {
+ label: 'Images',
+ icon: 'images',
+ count: 0,
+ iconBgColor: '#F17922',
+ navLink: routes.IMAGE_EXPLORE.path,
+ title: 'Images Explorer',
+ },
+ [SequenceTypesEnum.Audios]: {
+ label: 'Audios',
+ icon: 'audio',
+ count: 0,
+ iconBgColor: '#FCB500',
+ navLink: '',
+ title: 'Explorer coming soon',
+ },
+ [SequenceTypesEnum.Texts]: {
+ label: 'Texts',
+ icon: 'text',
+ count: 0,
+ iconBgColor: '#E149A0',
+ navLink: '',
+ title: 'Explorer coming soon',
+ },
+ [SequenceTypesEnum.Distributions]: {
+ label: 'Distributions',
+ icon: 'distributions',
+ count: 0,
+ iconBgColor: '#0394B4',
+ navLink: '',
+ title: 'Explorer coming soon',
+ },
+};
+
+const runsCountingInitialMap: Record<'archived' | 'runs', IProjectStatistic> = {
+ runs: {
+ label: 'runs',
+ icon: 'runs',
+ count: 0,
+ iconBgColor: '#1473E6',
+ navLink: routes.RUNS.path,
+ },
+ archived: {
+ label: 'archived',
+ icon: 'archive',
+ count: 0,
+ iconBgColor: '#606986',
+ navLink: `/runs?select=${encode({ query: 'run.archived == True' })}`,
+ },
+};
+
+function ProjectStatistics() {
+ const [hoveredState, setHoveredState] = React.useState({
+ source: '',
+ id: '',
+ });
+ const { projectParamsStore, projectContributionsStore } =
+ useProjectStatistics();
+
+ const { statisticsMap, totalTrackedSequencesCount } = React.useMemo(() => {
+ const statistics = { ...statisticsInitialMap };
+ let totalTrackedSequencesCount = 0;
+
+ for (let [seqName, seqData] of Object.entries(
+ projectParamsStore.data || {},
+ )) {
+ let systemMetricsCount = 0;
+ let sequenceItemsCount = 0;
+ for (let [itemKey, itemData] of Object.entries(seqData)) {
+ if (itemKey.startsWith('__system__')) {
+ systemMetricsCount += itemData.length;
+ } else {
+ sequenceItemsCount += itemData.length;
+ }
+ }
+ totalTrackedSequencesCount += sequenceItemsCount;
+ statistics[seqName].count = sequenceItemsCount;
+ if (systemMetricsCount) {
+ totalTrackedSequencesCount += systemMetricsCount;
+ statistics.systemMetrics.count = systemMetricsCount;
+ }
+ }
+ return { statisticsMap: statistics, totalTrackedSequencesCount };
+ }, [projectParamsStore]);
+
+ const { totalRunsCount, archivedRuns } = React.useMemo(
+ () => ({
+ totalRunsCount: projectContributionsStore.data?.num_runs || 0,
+ archivedRuns: projectContributionsStore.data?.num_archived_runs || 0,
+ }),
+ [projectContributionsStore],
+ );
+ const statisticsBarData = React.useMemo(
+ () =>
+ Object.values(statisticsMap).map(
+ ({ label, iconBgColor = '#000', count }) => ({
+ highlighted: hoveredState.id === label,
+ label,
+ color: iconBgColor,
+ percent:
+ totalTrackedSequencesCount === 0
+ ? 0
+ : (count / totalTrackedSequencesCount) * 100,
+ }),
+ ),
+ [statisticsMap, totalTrackedSequencesCount, hoveredState],
+ );
+ const runsCountingMap = React.useMemo(
+ () => ({
+ runs: {
+ ...runsCountingInitialMap.runs,
+ count: totalRunsCount - archivedRuns,
+ },
+ archived: {
+ ...runsCountingInitialMap.archived,
+ count: archivedRuns,
+ },
+ }),
+ [archivedRuns, totalRunsCount],
+ );
+ const onMouseOver = React.useCallback((id = '', source = '') => {
+ setHoveredState({ source, id });
+ }, []);
+ const onMouseLeave = React.useCallback(() => {
+ setHoveredState({ source: '', id: '' });
+ }, []);
+ return (
+
+
+ Total runs: {totalRunsCount}
+
+
+ {Object.values(runsCountingMap).map(
+ ({ label, icon, count, iconBgColor, navLink }) => (
+
+ ),
+ )}
+
+
+ Tracked sequences
+
+
+ {Object.values(statisticsMap).map(
+ ({ label, title, icon, count, iconBgColor, navLink }) => (
+
+ ),
+ )}
+
+
+
+
+
+ );
+}
+
+ProjectStatistics.displayName = 'ProjectStatistics';
+
+export default React.memo(ProjectStatistics);
diff --git a/aim/web/ui/src/pages/Dashboard/components/ProjectStatistics/ProjectStatisticsStore.ts b/aim/web/ui/src/pages/Dashboard/components/ProjectStatistics/ProjectStatisticsStore.ts
new file mode 100644
index 0000000000..a9cca3a159
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ProjectStatistics/ProjectStatisticsStore.ts
@@ -0,0 +1,23 @@
+import createResource from 'modules/core/utils/createResource';
+import { getParams, GetParamsResult } from 'modules/core/api/projectApi';
+
+import { SequenceTypesEnum } from 'types/core/enums';
+
+function projectStatisticsEngine() {
+ const { fetchData, state, destroy } = createResource(() =>
+ getParams({
+ sequence: [
+ SequenceTypesEnum.Metric,
+ SequenceTypesEnum.Images,
+ SequenceTypesEnum.Figures,
+ SequenceTypesEnum.Texts,
+ SequenceTypesEnum.Audios,
+ SequenceTypesEnum.Distributions,
+ ],
+ exclude_params: true,
+ }),
+ );
+ return { fetchProjectParams: fetchData, projectParamsState: state, destroy };
+}
+
+export default projectStatisticsEngine();
diff --git a/aim/web/ui/src/pages/Dashboard/components/ProjectStatistics/index.tsx b/aim/web/ui/src/pages/Dashboard/components/ProjectStatistics/index.tsx
new file mode 100644
index 0000000000..8b236988ca
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ProjectStatistics/index.tsx
@@ -0,0 +1,7 @@
+import ProjectStatistics from './ProjectStatistics';
+import useProjectStatistics from './useProjectStatistics';
+
+export * from './ProjectStatistics.d';
+export { useProjectStatistics };
+
+export default ProjectStatistics;
diff --git a/aim/web/ui/src/pages/Dashboard/components/ProjectStatistics/useProjectStatistics.tsx b/aim/web/ui/src/pages/Dashboard/components/ProjectStatistics/useProjectStatistics.tsx
new file mode 100644
index 0000000000..fb667bbacf
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/ProjectStatistics/useProjectStatistics.tsx
@@ -0,0 +1,30 @@
+import * as React from 'react';
+
+import projectContributionsEngine from '../ProjectContributions/ProjectContributionsStore';
+
+import projectStatisticsEngine from './ProjectStatisticsStore';
+
+function useProjectStatistics() {
+ const { current: projectStatsEngine } = React.useRef(projectStatisticsEngine);
+ const projectParamsStore = projectStatsEngine.projectParamsState(
+ (state) => state,
+ );
+ const { current: contributionsEngine } = React.useRef(
+ projectContributionsEngine,
+ );
+ const projectContributionsStore =
+ contributionsEngine.projectContributionsState((state) => state);
+
+ React.useEffect(() => {
+ if (!projectParamsStore.data) {
+ projectStatsEngine.fetchProjectParams();
+ }
+ return () => {
+ projectStatsEngine.destroy();
+ };
+ }, [projectStatsEngine]);
+
+ return { projectParamsStore, projectContributionsStore };
+}
+
+export default useProjectStatistics;
diff --git a/aim/web/ui/src/pages/Dashboard/components/QuickStart/QuickStart.scss b/aim/web/ui/src/pages/Dashboard/components/QuickStart/QuickStart.scss
new file mode 100644
index 0000000000..65dec975ea
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/QuickStart/QuickStart.scss
@@ -0,0 +1,14 @@
+@use 'src/styles/abstracts' as *;
+
+.QuickStart__section {
+ padding: $space-lg 0;
+}
+
+.QuickStart__section__title {
+ margin-bottom: $space-sm;
+}
+
+.QuickStart__section__text {
+ font-style: italic;
+ margin-top: $space-sm;
+}
diff --git a/aim/web/ui/src/pages/Dashboard/components/QuickStart/QuickStart.tsx b/aim/web/ui/src/pages/Dashboard/components/QuickStart/QuickStart.tsx
new file mode 100644
index 0000000000..4608789dbf
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/QuickStart/QuickStart.tsx
@@ -0,0 +1,74 @@
+import * as React from 'react';
+
+import { Link } from '@material-ui/core';
+
+import { Text } from 'components/kit';
+import CodeBlock from 'components/CodeBlock/CodeBlock';
+
+import { DOCUMENTATIONS } from 'config/references';
+
+import './QuickStart.scss';
+
+function QuickStart() {
+ return (
+
+
+ Quick Start
+
+
+
+ Integrate Aim with your code
+
+
+
+ See the full list of supported trackable objects(e.g. images, text,
+ etc){' '}
+
+ here
+
+ .
+
+
+
+ );
+}
+
+export default QuickStart;
diff --git a/aim/web/ui/src/pages/Dashboard/components/QuickStart/index.ts b/aim/web/ui/src/pages/Dashboard/components/QuickStart/index.ts
new file mode 100644
index 0000000000..a9701cfe6a
--- /dev/null
+++ b/aim/web/ui/src/pages/Dashboard/components/QuickStart/index.ts
@@ -0,0 +1,3 @@
+import QuickStart from './QuickStart';
+
+export default QuickStart;
diff --git a/aim/web/ui/src/pages/Home/Home.scss b/aim/web/ui/src/pages/Home/Home.scss
deleted file mode 100644
index 91bcee95c7..0000000000
--- a/aim/web/ui/src/pages/Home/Home.scss
+++ /dev/null
@@ -1,21 +0,0 @@
-@use 'src/styles/abstracts' as *;
-
-.Home {
- &__container {
- background-color: #ffffff;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- h2 {
- margin: 0 0 1em 0;
- }
- }
- &__Explore__container {
- border-top: 1px solid #e8f1fc;
- border-bottom: 1px solid #e8f1fc;
- display: flex;
- }
- &__Activity__container {
- padding: 1.5em;
- }
-}
diff --git a/aim/web/ui/src/pages/Home/Home.tsx b/aim/web/ui/src/pages/Home/Home.tsx
deleted file mode 100644
index 5b719d3a5c..0000000000
--- a/aim/web/ui/src/pages/Home/Home.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import React from 'react';
-
-import NotificationContainer from 'components/NotificationContainer/NotificationContainer';
-import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary';
-
-import { IHomeProps } from 'types/pages/home/Home';
-
-import ExploreAim from './components/ExploreAim/ExploreAim';
-import SetupGuide from './components/SetupGuide/SetupGuide';
-import Activity from './components/Activity/Activity';
-
-import './Home.scss';
-
-function Home({
- activityData,
- onSendEmail,
- notifyData,
- onNotificationDelete,
- askEmailSent,
-}: IHomeProps): React.FunctionComponentElement {
- return (
-
-
-
-
-
-
-
- {notifyData?.length > 0 && (
-
- )}
-
-
- );
-}
-export default Home;
diff --git a/aim/web/ui/src/pages/Home/HomeContainer.tsx b/aim/web/ui/src/pages/Home/HomeContainer.tsx
deleted file mode 100644
index 933c52b20e..0000000000
--- a/aim/web/ui/src/pages/Home/HomeContainer.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from 'react';
-import { useModel } from 'hooks';
-
-import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary';
-
-import { ANALYTICS_EVENT_KEYS } from 'config/analytics/analyticsKeysMap';
-
-import homeAppModel from 'services/models/home/homeAppModel';
-import * as analytics from 'services/analytics';
-
-import Home from './Home';
-
-function HomeContainer(): React.FunctionComponentElement {
- const homeData = useModel(homeAppModel);
-
- React.useEffect(() => {
- homeAppModel.initialize();
- analytics.trackEvent(ANALYTICS_EVENT_KEYS.home.pageView);
- return () => {
- homeAppModel.destroy();
- };
- }, []);
-
- return (
-
-
-
- );
-}
-export default HomeContainer;
diff --git a/aim/web/ui/src/pages/Home/components/Activity/Activity.tsx b/aim/web/ui/src/pages/Home/components/Activity/Activity.tsx
deleted file mode 100644
index f77cd91b94..0000000000
--- a/aim/web/ui/src/pages/Home/components/Activity/Activity.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import React from 'react';
-
-import { Grid } from '@material-ui/core';
-
-import HeatMap from 'components/HeatMap/HeatMap';
-import { Spinner, Text } from 'components/kit';
-import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary';
-
-import { ANALYTICS_EVENT_KEYS } from 'config/analytics/analyticsKeysMap';
-
-import { trackEvent } from 'services/analytics';
-
-import { IActivityProps } from 'types/pages/home/components/Activity/Activity';
-
-import './Activity.scss';
-
-function Activity({
- activityData,
-}: IActivityProps): React.FunctionComponentElement {
- function shiftDate(date: any, numDays: any) {
- const newDate = new Date(date);
- newDate.setDate(newDate.getDate() + numDays);
- return newDate;
- }
- let today = new Date();
- return (
-
-
-
-
- Statistics
-
-
-
- Experiments
-
-
- {activityData?.num_experiments ?? (
-
- )}
-
-
-
-
- Runs
-
-
- {activityData?.num_runs ?? (
-
- )}
-
-
-
-
-
- Activity
-
-
- {
- trackEvent(ANALYTICS_EVENT_KEYS.home.activityCellClick);
- }}
- data={Object.keys(activityData?.activity_map ?? {}).map((k) => [
- new Date(k),
- activityData.activity_map[k],
- ])}
- />
-
-
-
-
- );
-}
-export default React.memo(Activity);
diff --git a/aim/web/ui/src/pages/Home/components/AskForm/AskForm.scss b/aim/web/ui/src/pages/Home/components/AskForm/AskForm.scss
deleted file mode 100644
index 14988b44fe..0000000000
--- a/aim/web/ui/src/pages/Home/components/AskForm/AskForm.scss
+++ /dev/null
@@ -1,36 +0,0 @@
-@use 'src/styles/abstracts' as *;
-
-.AskForm {
- margin-top: 2rem;
- padding: 2rem 1rem;
- text-align: center;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-direction: column;
- border: $border-main;
- border-radius: $border-radius-main;
- &__avatar {
- display: inline-flex;
- img {
- margin: 0 auto;
- width: 4rem;
- height: 4rem;
- border-radius: 2rem;
- }
- }
- &__title {
- margin: 0.5rem 0 !important;
- }
-
- &__p {
- max-width: 36rem;
- margin-bottom: 1rem;
- }
- &__submit {
- margin-top: 0.875rem;
- }
- .TextField__OutLined__Large {
- width: 21.125rem;
- }
-}
diff --git a/aim/web/ui/src/pages/Home/components/AskForm/AskForm.tsx b/aim/web/ui/src/pages/Home/components/AskForm/AskForm.tsx
deleted file mode 100644
index 1ef6e49eb0..0000000000
--- a/aim/web/ui/src/pages/Home/components/AskForm/AskForm.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import React from 'react';
-
-import { TextField } from '@material-ui/core';
-
-import { Button, Text } from 'components/kit';
-
-import { IAskFormProps } from 'types/pages/home/components/AskForm/AskForm';
-
-import './AskForm.scss';
-
-function AskForm({
- onSendEmail,
-}: IAskFormProps): React.FunctionComponentElement {
- const [email, setEmail] = React.useState('');
-
- async function handleSubmit() {
- const data = await onSendEmail({ email });
- if (data.ok) {
- setEmail('');
- }
- }
-
- function onChange(e: React.ChangeEvent): void {
- setEmail(e.target.value);
- }
-
- return (
-
-
- 👋 Hey
-
-
- We’re working hard to create an amazing experiment tracker and need your
- feedback for the Aim. If you’d like to contribute to improving it and
- share what you like/dislike about Aim, please leave your email.
-
-
-
- Submit
-
-
- );
-}
-
-export default AskForm;
diff --git a/aim/web/ui/src/pages/Home/components/ExploreAim/ExploreAim.scss b/aim/web/ui/src/pages/Home/components/ExploreAim/ExploreAim.scss
deleted file mode 100644
index 0c82121fae..0000000000
--- a/aim/web/ui/src/pages/Home/components/ExploreAim/ExploreAim.scss
+++ /dev/null
@@ -1,63 +0,0 @@
-@use 'src/styles/abstracts' as *;
-
-.ExploreAim {
- display: flex;
- flex-direction: column;
- border-left: $border-grey;
- padding: 1.5rem 1.5rem 1.5rem 2.5rem;
- min-width: 600px;
- h2 {
- margin: 0;
- }
- &__card__container {
- margin-top: 2rem;
- display: flex;
- flex-wrap: wrap;
- max-width: 840px;
- }
- &__social {
- margin-top: 2rem;
- &__item {
- border: $border-grey;
- border-radius: 0.5rem;
- display: flex;
- align-items: center;
- padding: 0 1.5rem;
- height: 3.5rem;
- margin-bottom: 0.5rem;
- text-decoration: none;
- max-width: 33rem;
- &:last-child {
- margin-bottom: 0;
- }
- &:visited {
- color: unset;
- }
- &:hover {
- border-color: #1473e6;
- i {
- color: #1473e6;
- }
- }
- span {
- font-size: 1rem;
- line-height: 1.1875rem;
- color: $pico;
- flex: 1;
- }
-
- img {
- margin-right: 1rem;
- width: 2rem;
- }
-
- i {
- font-size: 0.625rem;
- color: $text-color;
- }
- }
- }
- &__block__item {
- margin-top: 3rem;
- }
-}
diff --git a/aim/web/ui/src/pages/Home/components/ExploreAim/ExploreAim.tsx b/aim/web/ui/src/pages/Home/components/ExploreAim/ExploreAim.tsx
deleted file mode 100644
index 5d7f6161f5..0000000000
--- a/aim/web/ui/src/pages/Home/components/ExploreAim/ExploreAim.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import React from 'react';
-
-import githubIcon from 'assets/icons/github.svg';
-import slackIcon from 'assets/icons/slack.svg';
-
-import { Icon, Text } from 'components/kit';
-import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary';
-
-import { ANALYTICS_EVENT_KEYS } from 'config/analytics/analyticsKeysMap';
-
-import { trackEvent } from 'services/analytics';
-
-import ExploreAimCard from '../ExploreAimCard/ExploreAimCard';
-
-import './ExploreAim.scss';
-
-export interface IExploreCard {
- title: string;
- description: string;
- path: string;
- icon: string;
-}
-const cardsData: IExploreCard[] = [
- {
- title: 'Runs Explorer',
- description:
- 'View all your runs holistically on Runs Explorer: all hyperparameters, all metric last values',
- path: 'runs',
- icon: 'runs',
- },
- {
- title: 'Metrics Explorer',
- description: 'Compare 100s of metrics in a few clicks on Metrics Explorer',
- path: 'metrics',
- icon: 'metrics',
- },
- {
- title: 'Images Explorer',
- description:
- 'Track intermediate images and search, compare them on Images Explorer',
- path: 'images',
- icon: 'images',
- },
- {
- title: 'Params Explorer',
- description:
- 'The Params explorer enables a parallel coordinates view for metrics and params',
- path: 'params',
- icon: 'params',
- },
- {
- title: 'Scatters Explorer',
- description:
- 'Explore and learn relationship, correlations, and clustering effects between metrics and parameters on Scatters Explorer',
- path: 'scatters',
- icon: 'scatterplot',
- },
-];
-
-function ExploreAim(): React.FunctionComponentElement {
- return (
-
-
-
-
-
- Explore Aim
-
-
- {cardsData.map((item: IExploreCard) => (
-
- ))}
-
-
-
-
- );
-}
-
-export default React.memo(ExploreAim);
diff --git a/aim/web/ui/src/pages/Home/components/ExploreAimCard/ExploreAimCard.scss b/aim/web/ui/src/pages/Home/components/ExploreAimCard/ExploreAimCard.scss
deleted file mode 100644
index 930fac9697..0000000000
--- a/aim/web/ui/src/pages/Home/components/ExploreAimCard/ExploreAimCard.scss
+++ /dev/null
@@ -1,61 +0,0 @@
-@use 'src/styles/abstracts' as *;
-
-.ExploreAimCard {
- width: 15.5rem;
- background-color: transparent;
- border-radius: 0.5rem;
- border: $border-grey;
- transition: background-color 0.3s ease-out;
- padding: 1.5rem 1.5rem 2rem 1.5rem;
- margin-bottom: 2rem;
- margin-right: 2rem;
- text-decoration: none;
- position: relative;
- color: $pico;
- &:hover {
- background-color: $primary-color;
- color: #fff;
- .ExploreAimCard__desc,
- .ExploreAimCard__title {
- color: #fff;
- }
- .ExploreAimCard__icon {
- color: $primary-color;
- background-color: #fff;
- }
- }
- &__icon {
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: $border-radius-circle;
- width: 2.75rem;
- height: 2.75rem;
- background: $primary-color;
- color: #fff;
- margin-bottom: 0.875rem;
- transition: all 0.3s ease-out;
- i {
- font-size: 1.375rem;
- }
- }
- &__title {
- margin: 0;
- }
- &__desc {
- margin-top: 0.5rem;
- display: inline-block;
- }
- &__arrow__icon {
- position: absolute;
- right: 1rem;
- bottom: 0.25rem;
- display: flex;
- justify-content: flex-end;
-
- i {
- color: #fff;
- font-size: 1.75rem;
- }
- }
-}
diff --git a/aim/web/ui/src/pages/Home/components/ExploreAimCard/ExploreAimCard.tsx b/aim/web/ui/src/pages/Home/components/ExploreAimCard/ExploreAimCard.tsx
deleted file mode 100644
index 5754ab9910..0000000000
--- a/aim/web/ui/src/pages/Home/components/ExploreAimCard/ExploreAimCard.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import React from 'react';
-import { NavLink } from 'react-router-dom';
-
-import { Icon, Text } from 'components/kit';
-import { IconName } from 'components/kit/Icon';
-import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary';
-
-import { IExploreCard } from '../ExploreAim/ExploreAim';
-
-import './ExploreAimCard.scss';
-
-function ExploreAimCard({
- title,
- path,
- description,
- icon,
-}: IExploreCard): React.FunctionComponentElement {
- return (
-
-
-
-
-
-
- {title}
-
-
- {description}
-
-
-
-
-
-
- );
-}
-
-export default React.memo(ExploreAimCard);
diff --git a/aim/web/ui/src/pages/Home/components/SetupGuide/SetupGuide.scss b/aim/web/ui/src/pages/Home/components/SetupGuide/SetupGuide.scss
deleted file mode 100644
index 42b2a04f25..0000000000
--- a/aim/web/ui/src/pages/Home/components/SetupGuide/SetupGuide.scss
+++ /dev/null
@@ -1,72 +0,0 @@
-@use 'src/styles/abstracts' as *;
-
-.SetupGuide {
- &__container {
- max-width: 800px;
- padding: 1.5rem 2.5rem 1.5rem 1.5rem;
- flex: 1 100%;
- h3 {
- margin: 0 0 0.5rem;
- }
-
- h2 {
- margin: 0;
- }
- }
- &__code {
- margin-top: 2rem;
- max-width: 44.5rem;
- }
- &__resources {
- &__container {
- margin-top: 2.75rem;
- display: flex;
- }
- &__item {
- margin-right: 2.375em;
- text-decoration: none;
-
- span {
- display: inline-block;
- margin-top: 0.5rem;
- }
- &:last-child {
- margin-right: 0;
- }
- &__icon {
- margin: 0 auto;
- width: 2.75rem;
- height: 2.75rem;
- border: 0.125rem solid $primary-color;
- border-radius: $border-radius-circle;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.3s ease-out;
- cursor: pointer;
- i {
- color: $primary-color;
- transition: all 0.3s ease-out;
- }
- &_co {
- margin-left: 1px;
- font-size: 1.875rem;
- }
- &_fullDocs {
- margin-bottom: 1px;
- font-size: 1.3125rem;
- }
- &_liveDemo {
- font-size: 1.375rem;
- }
- &:hover {
- color: #ffffff;
- background-color: #1473e6;
- i {
- color: #ffffff;
- }
- }
- }
- }
- }
-}
diff --git a/aim/web/ui/src/pages/Home/components/SetupGuide/SetupGuide.tsx b/aim/web/ui/src/pages/Home/components/SetupGuide/SetupGuide.tsx
deleted file mode 100644
index 2d5cea9c90..0000000000
--- a/aim/web/ui/src/pages/Home/components/SetupGuide/SetupGuide.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import React from 'react';
-
-import CodeBlock from 'components/CodeBlock/CodeBlock';
-import { Icon, Text } from 'components/kit';
-import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary';
-
-import { ANALYTICS_EVENT_KEYS } from 'config/analytics/analyticsKeysMap';
-import { DOCUMENTATIONS, GUIDES, DEMOS } from 'config/references';
-
-import { trackEvent } from 'services/analytics';
-
-import { ISetupGuideProps } from 'types/pages/home/components/SetupGuide/SetupGuide';
-
-import './SetupGuide.scss';
-
-function SetupGuide({
- askEmailSent,
- onSendEmail,
-}: ISetupGuideProps): React.FunctionComponentElement {
- return (
-
-
-
- Integrate Aim with your code
-
-
-
- 1. Import Aim
-
-
-
-
-
- 2. Track your training runs
-
-
-
-
- {/*{askEmailSent ? null :
}*/}
-
-
- );
-}
-
-export default React.memo(SetupGuide);
diff --git a/aim/web/ui/src/pages/Metrics/components/Table/CompareSelectedRunsPopover/CompareSelectedRunsPopover.d.ts b/aim/web/ui/src/pages/Metrics/components/Table/CompareSelectedRunsPopover/CompareSelectedRunsPopover.d.ts
index 3e4be72ce6..b5888c2d43 100644
--- a/aim/web/ui/src/pages/Metrics/components/Table/CompareSelectedRunsPopover/CompareSelectedRunsPopover.d.ts
+++ b/aim/web/ui/src/pages/Metrics/components/Table/CompareSelectedRunsPopover/CompareSelectedRunsPopover.d.ts
@@ -2,5 +2,6 @@ import { AppNameEnum } from 'services/models/explorer';
export interface ICompareSelectedRunsPopoverProps {
appName: AppNameEnum;
- selectedRows: { [key: string]: any };
+ disabled?: boolean;
+ query: string;
}
diff --git a/aim/web/ui/src/pages/Metrics/components/Table/CompareSelectedRunsPopover/CompareSelectedRunsPopover.tsx b/aim/web/ui/src/pages/Metrics/components/Table/CompareSelectedRunsPopover/CompareSelectedRunsPopover.tsx
index 12dbefb957..45034bd471 100644
--- a/aim/web/ui/src/pages/Metrics/components/Table/CompareSelectedRunsPopover/CompareSelectedRunsPopover.tsx
+++ b/aim/web/ui/src/pages/Metrics/components/Table/CompareSelectedRunsPopover/CompareSelectedRunsPopover.tsx
@@ -1,5 +1,4 @@
import React from 'react';
-import _ from 'lodash-es';
import { useHistory } from 'react-router-dom';
import { MenuItem, Tooltip } from '@material-ui/core';
@@ -7,6 +6,7 @@ import { MenuItem, Tooltip } from '@material-ui/core';
import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary';
import ControlPopover from 'components/ControlPopover/ControlPopover';
import { Button, Icon, Text } from 'components/kit';
+import { IconName } from 'components/kit/Icon';
import { EXPLORE_SELECTED_RUNS_CONFIG } from 'config/table/tableConfigs';
import { ANALYTICS_EVENT_KEYS } from 'config/analytics/analyticsKeysMap';
@@ -21,8 +21,9 @@ import { ICompareSelectedRunsPopoverProps } from './CompareSelectedRunsPopover.d
import './CompareSelectedRunsPopover.scss';
function CompareSelectedRunsPopover({
- selectedRows,
appName,
+ query,
+ disabled = false,
}: ICompareSelectedRunsPopoverProps): React.FunctionComponentElement {
const history = useHistory();
@@ -35,14 +36,6 @@ function CompareSelectedRunsPopover({
e.stopPropagation();
e.preventDefault();
if (value) {
- const runHashArray: string[] = _.uniq([
- ...Object.values(selectedRows).map((row: any) => row.runHash),
- ]);
-
- const query = `run.hash in [${runHashArray
- .map((hash) => `"${hash}"`)
- .join(',')}]`;
-
const search = encode({
query,
advancedMode: true,
@@ -61,7 +54,7 @@ function CompareSelectedRunsPopover({
history.push(path);
}
},
- [appName, history, selectedRows],
+ [appName, history, query],
);
return (
@@ -80,26 +73,27 @@ function CompareSelectedRunsPopover({
variant='text'
color='secondary'
size='small'
+ disabled={disabled}
onClick={onAnchorClick}
className={`CompareSelectedRunsPopover__trigger ${
opened ? 'opened' : ''
}`}
>
-
+
Compare
)}
component={
- {EXPLORE_SELECTED_RUNS_CONFIG[appName as AppNameEnum].map(
+ {EXPLORE_SELECTED_RUNS_CONFIG?.[appName as AppNameEnum]?.map(
(item: AppNameEnum) => (
-
+
{
- if (path === PathEnum.Home) {
+ if (path === PathEnum.Dashboard) {
setDocumentTitle();
} else if (path !== PathEnum.Run_Detail) {
setDocumentTitle(title, true);
diff --git a/aim/web/ui/src/routes/routes.tsx b/aim/web/ui/src/routes/routes.tsx
index 66473fd5e0..e63876d38d 100644
--- a/aim/web/ui/src/routes/routes.tsx
+++ b/aim/web/ui/src/routes/routes.tsx
@@ -22,8 +22,8 @@ const Bookmarks = React.lazy(
/* webpackChunkName: "bookmarks" */ 'pages/Bookmarks/BookmarksContainer'
),
);
-const Home = React.lazy(
- () => import(/* webpackChunkName: "home" */ 'pages/Home/HomeContainer'),
+const Dashboard = React.lazy(
+ () => import(/* webpackChunkName: "dashboard" */ 'pages/Dashboard/Dashboard'),
);
const TagsContainer = React.lazy(
() => import(/* webpackChunkName: "tags" */ 'pages/Tags/TagsContainer'),
@@ -57,13 +57,14 @@ export interface IRoute {
}
const routes = {
- HOME: {
- path: PathEnum.Home,
- component: Home,
- showInSidebar: false,
- displayName: null,
+ DASHBOARD: {
+ path: PathEnum.Dashboard,
+ component: Dashboard,
+ showInSidebar: true,
+ displayName: 'Dashboard',
+ icon: 'dashboard',
isExact: true,
- title: pageTitlesEnum.HOME,
+ title: pageTitlesEnum.DASHBOARD,
},
RUNS: {
path: PathEnum.Runs,
diff --git a/aim/web/ui/src/services/NetworkService/index.ts b/aim/web/ui/src/services/NetworkService/index.ts
index acf59409c7..bcc2c31547 100644
--- a/aim/web/ui/src/services/NetworkService/index.ts
+++ b/aim/web/ui/src/services/NetworkService/index.ts
@@ -76,7 +76,10 @@ class NetworkService {
if (Array.isArray(arg)) {
return [this.uri, ...arg].join('/');
}
- return `${this.uri}/${arg}`;
+ if (arg) {
+ return `${this.uri}/${arg}`;
+ }
+ return `${this.uri}`;
};
private createQueryParams = (queryParams: Record) => {
@@ -170,10 +173,7 @@ class NetworkService {
const fetchOptions: RequestInit = {
method: options.method,
- headers: options.headers || {
- Accept: 'application/json',
- 'Content-Type': 'application/json',
- },
+ headers: options.headers || this.getRequestHeaders(),
};
if (options.headers) {
@@ -212,6 +212,18 @@ class NetworkService {
}
this.interceptors.push(interceptor);
}
+
+ private getTimezoneOffset = (): string => {
+ return `${new Date().getTimezoneOffset()}`;
+ };
+
+ public getRequestHeaders = () => {
+ return {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ 'X-Timezone-Offset': this.getTimezoneOffset(),
+ };
+ };
}
export * from './types';
diff --git a/aim/web/ui/src/services/api/endpoints.ts b/aim/web/ui/src/services/api/endpoints.ts
index 75add03bd7..d15916afc3 100644
--- a/aim/web/ui/src/services/api/endpoints.ts
+++ b/aim/web/ui/src/services/api/endpoints.ts
@@ -10,6 +10,34 @@ const ENDPOINTS = {
BASE: '/runs',
GET: '',
SEARCH: 'search',
+ ACTIVE: 'active',
+ },
+
+ EXPERIMENTS: {
+ BASE: '/experiments',
+ GET: '',
+ CREATE: '',
+ SEARCH: 'search',
+ },
+
+ DASHBOARDS: {
+ BASE: '/dashboards',
+ GET: '',
+ CREATE: '',
+ SEARCH: 'search',
+ },
+
+ TAGS: {
+ BASE: '/tags',
+ GET: '',
+ CREATE: '',
+ UPDATE: '',
+ DELETE: '',
+ },
+ RELEASE_NOTES: {
+ BASE: 'https://api.github.com/repos/aimhubio/aim/releases',
+ GET: '',
+ GET_BY_TAG_NAME: 'tags',
},
};
diff --git a/aim/web/ui/src/services/api/experiments/experimentsService.ts b/aim/web/ui/src/services/api/experiments/experimentsService.ts
new file mode 100644
index 0000000000..ed1f736bd1
--- /dev/null
+++ b/aim/web/ui/src/services/api/experiments/experimentsService.ts
@@ -0,0 +1,57 @@
+import { IExperimentData } from 'modules/core/api/experimentsApi';
+
+import { IApiRequest } from 'types/services/services';
+
+import API from '../api';
+
+const endpoints = {
+ EXPERIMENTS: 'experiments',
+ GET_EXPERIMENT_BY_ID: (id: string) => `experiments/${id}`,
+ UPDATE_EXPERIMENT_BY_ID: (id: string) => `experiments/${id}`,
+ SEARCH_EXPERIMENT: (query: string) => `experiments/search=${query}`,
+ GET_RUNS_BY_EXPERIMENT_ID: (id: string) => `experiments/${id}/runs`,
+};
+
+function getExperimentsData(): IApiRequest {
+ return API.get(endpoints.EXPERIMENTS);
+}
+
+function searchExperiment(query: string): IApiRequest {
+ return API.get(endpoints.SEARCH_EXPERIMENT(query));
+}
+
+function getExperimentById(id: string): IApiRequest {
+ return API.get(endpoints.GET_EXPERIMENT_BY_ID(id));
+}
+
+function updateExperimentById(
+ reqBody: { name?: string; archived?: boolean },
+ id: string,
+): IApiRequest<{ status: string; id: string }> {
+ return API.put(endpoints.UPDATE_EXPERIMENT_BY_ID(id), reqBody);
+}
+
+function createExperiment(reqBody: {
+ name: string;
+}): IApiRequest<{ id: string; status: string }> {
+ return API.post(endpoints.EXPERIMENTS, reqBody);
+}
+
+function getRunsOfExperiment(
+ id: string,
+ params: { limit: number; offset?: string } = { limit: 10 },
+) {
+ return API.get(endpoints.GET_RUNS_BY_EXPERIMENT_ID(id), params);
+}
+
+const experimentsService = {
+ endpoints,
+ getExperimentsData,
+ searchExperiment,
+ getExperimentById,
+ updateExperimentById,
+ createExperiment,
+ getRunsOfExperiment,
+};
+
+export default experimentsService;
diff --git a/aim/web/ui/src/services/api/runs/runsService.ts b/aim/web/ui/src/services/api/runs/runsService.ts
index 557d2def73..777001f1f1 100644
--- a/aim/web/ui/src/services/api/runs/runsService.ts
+++ b/aim/web/ui/src/services/api/runs/runsService.ts
@@ -7,7 +7,6 @@ const endpoints = {
GET_EXPERIMENTS: 'experiments',
GET_RUN_INFO: (id: string) => `runs/${id}/info`,
GET_RUN_LOGS: (id: string) => `runs/${id}/logs`,
- GET_RUNS_BY_EXPERIMENT_ID: (id: string) => `experiments/${id}/runs`,
GET_RUN_METRICS_BATCH_BY_TRACES: (id: string) =>
`runs/${id}/metric/get-batch`,
EDIT_RUN: (id: string) => `runs/${id}`,
@@ -39,13 +38,6 @@ function getRunInfo(id: string) {
return API.get(endpoints.GET_RUN_INFO(id));
}
-function getRunsOfExperiment(
- id: string,
- params: { limit: number; offset?: string } = { limit: 10 },
-) {
- return API.get(endpoints.GET_RUNS_BY_EXPERIMENT_ID(id), params);
-}
-
function getExperimentsData() {
return API.get(endpoints.GET_EXPERIMENTS);
}
@@ -123,7 +115,6 @@ const runsService = {
getRunLogs,
getRunMetricsBatch,
getExperimentsData,
- getRunsOfExperiment,
archiveRun,
deleteRun,
attachRunsTag,
diff --git a/aim/web/ui/src/services/models/explorer/createAppModel.ts b/aim/web/ui/src/services/models/explorer/createAppModel.ts
index 615918d500..a53bb822d5 100644
--- a/aim/web/ui/src/services/models/explorer/createAppModel.ts
+++ b/aim/web/ui/src/services/models/explorer/createAppModel.ts
@@ -197,6 +197,7 @@ import { onCopyToClipBoard } from 'utils/onCopyToClipBoard';
import { getMetricsInitialRowData } from 'utils/app/getMetricsInitialRowData';
import { getMetricHash } from 'utils/app/getMetricHash';
import { getMetricLabel } from 'utils/app/getMetricLabel';
+import saveRecentSearches from 'utils/saveRecentSearches';
import { AppDataTypeEnum, AppNameEnum } from './index';
@@ -508,6 +509,7 @@ function createAppModel(appConfig: IAppInitialConfig) {
function initialize(appId: string): void {
model.init();
+
const state: Partial = {};
if (grouping) {
state.groupingSelectOptions = [];
@@ -528,6 +530,7 @@ function createAppModel(appConfig: IAppInitialConfig) {
if (!appId) {
setModelDefaultAppConfigData();
}
+
projectsService
.getProjectParams(['metric'])
.call()
@@ -635,6 +638,7 @@ function createAppModel(appConfig: IAppInitialConfig) {
if (shouldUrlUpdate) {
updateURL({ configData, appName });
}
+ saveRecentSearches(appName, query);
updateData(runData);
} catch (ex: Error | any) {
if (ex.name === 'AbortError') {
@@ -2328,6 +2332,7 @@ function createAppModel(appConfig: IAppInitialConfig) {
},
},
});
+ saveRecentSearches(appName, query);
if (shouldUrlUpdate) {
updateURL({ configData, appName });
}
@@ -5691,6 +5696,7 @@ function createAppModel(appConfig: IAppInitialConfig) {
liveUpdateInstance?.start({
q: configData?.select?.query,
});
+ //Changed the layout/styles of the experiments and tags tables to look more like lists|| Extend the contributions section (add activity feed under the contributions)
} catch (ex: Error | any) {
if (ex.name === 'AbortError') {
onNotificationAdd({
diff --git a/aim/web/ui/src/services/models/home/homeAppModel.ts b/aim/web/ui/src/services/models/home/homeAppModel.ts
deleted file mode 100644
index 617013bcd7..0000000000
--- a/aim/web/ui/src/services/models/home/homeAppModel.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-import projectsService from 'services/api/projects/projectsService';
-
-import exceptionHandler from 'utils/app/exceptionHandler';
-import onNotificationAdd from 'utils/app/onNotificationAdd';
-import onNotificationDelete from 'utils/app/onNotificationDelete';
-import { getItem, setItem } from 'utils/storage';
-
-import createModel from '../model';
-
-const model = createModel({});
-
-let activityRequestRef: {
- call: (exceptionHandler: (detail: any) => void) => Promise;
- abort: () => void;
-};
-
-function getActivityData() {
- const { call, abort } = projectsService.fetchActivityData();
- return {
- call: () =>
- call((detail: any) => {
- exceptionHandler({ detail, model });
- }).then((data: any) => {
- model.setState({
- activityData: data,
- });
- }),
- abort,
- };
-}
-
-function onSendEmail(data: object): Promise {
- return fetch('https://formspree.io/f/xeqvdval', {
- method: 'Post',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(data),
- })
- .then(async (res) => await res.json())
- .then((data) => {
- if (data.ok) {
- onNotificationAdd({
- notification: {
- severity: 'success',
- messages: ['Email Successfully sent'],
- id: Date.now(),
- },
- model,
- });
- model.setState({ askEmailSent: true });
- setItem('askEmailSent', true);
- } else {
- onNotificationAdd({
- notification: {
- severity: 'error',
- messages: ['Please enter valid email'],
- id: Date.now(),
- },
- model,
- });
- }
- return data;
- });
-}
-function initialize() {
- model.init();
- activityRequestRef = getActivityData();
- try {
- activityRequestRef.call((detail) => {
- exceptionHandler({ detail, model });
- });
- } catch (err: any) {
- onNotificationAdd({
- notification: {
- messages: [err.message],
- severity: 'error',
- id: Date.now(),
- },
- model,
- });
- }
- const isAskEmailSent: boolean = getItem('askEmailSent') === 'true';
- model.setState({ askEmailSent: isAskEmailSent });
-}
-
-function onHomeNotificationDelete(id: number) {
- onNotificationDelete({
- model,
- id,
- });
-}
-
-function destroy() {
- model.destroy();
- activityRequestRef.abort();
-}
-
-const homeAppModel = {
- ...model,
- destroy,
- initialize,
- getActivityData,
- onSendEmail,
- onHomeNotificationDelete,
-};
-
-export default homeAppModel;
diff --git a/aim/web/ui/src/services/models/imagesExplore/imagesExploreAppModel.ts b/aim/web/ui/src/services/models/imagesExplore/imagesExploreAppModel.ts
index deaf04af09..8b523b376b 100644
--- a/aim/web/ui/src/services/models/imagesExplore/imagesExploreAppModel.ts
+++ b/aim/web/ui/src/services/models/imagesExplore/imagesExploreAppModel.ts
@@ -94,6 +94,7 @@ import { onCopyToClipBoard } from 'utils/onCopyToClipBoard';
import getFilteredRow from 'utils/app/getFilteredRow';
import { getMetricHash } from 'utils/app/getMetricHash';
import onRunsTagsChange from 'utils/app/onRunsTagsChange';
+import saveRecentSearches from 'utils/saveRecentSearches';
import createModel from '../model';
import { AppNameEnum } from '../explorer';
@@ -418,6 +419,7 @@ function getImagesData(
if (shouldUrlUpdate) {
updateURL(configData);
}
+ saveRecentSearches(AppNameEnum.IMAGES, query!);
} catch (ex: Error | any) {
if (ex.name === 'AbortError') {
// Abort Error
diff --git a/aim/web/ui/src/services/models/runs/runDetailAppModel.ts b/aim/web/ui/src/services/models/runs/runDetailAppModel.ts
index 07116aae88..deef338f06 100644
--- a/aim/web/ui/src/services/models/runs/runDetailAppModel.ts
+++ b/aim/web/ui/src/services/models/runs/runDetailAppModel.ts
@@ -4,6 +4,7 @@ import { IRunBatch } from 'pages/RunDetail/types';
import runsService from 'services/api/runs/runsService';
import * as analytics from 'services/analytics';
+import experimentsService from 'services/api/experiments/experimentsService';
import { INotification } from 'types/components/NotificationContainer/NotificationContainer';
import { IApiRequest } from 'types/services/services';
@@ -94,7 +95,7 @@ function getRunsOfExperiment(
if (getRunsOfExperimentRequestRef) {
getRunsOfExperimentRequestRef.abort();
}
- getRunsOfExperimentRequestRef = runsService.getRunsOfExperiment(
+ getRunsOfExperimentRequestRef = experimentsService.getRunsOfExperiment(
runHash,
params,
);
diff --git a/aim/web/ui/src/styles/abstracts/_variables.scss b/aim/web/ui/src/styles/abstracts/_variables.scss
index 68e893a49f..458b4cac55 100644
--- a/aim/web/ui/src/styles/abstracts/_variables.scss
+++ b/aim/web/ui/src/styles/abstracts/_variables.scss
@@ -60,6 +60,7 @@ $pico-30: #b5b9c5;
$pico-20: #d2d4dc;
$pico-10: #e8eaee;
$pico-5: #f4f4f6;
+$pico-2: #fafafb;
$cuddle-110: #90afda;
$cuddle-70: #d1ddef;
@@ -90,16 +91,18 @@ $error-color-10: #fdeded;
$error-color-5: #f7f1f5;
// border
+$border-width-main: 0.0625rem solid;
$border-color-main: $primary-color-10;
-$border-main: 0.0625rem solid $border-color-main;
-$border-separator: 0.0625rem solid $primary-color-20;
-$border-main-darker: 0.0625rem solid $primary-color-40;
-$border-main-active: 0.0625rem solid $primary-color-50;
-$border-grey: 0.0625rem solid $cuddle-50;
-$border-grey-light: 0.0625rem solid $cuddle-30;
-$border-grey-lighter: 0.0625rem solid $cuddle-20;
-$border-transparent: 0.0625rem solid $transparent;
-$border-dark: 1px solid $pico-30;
+$border-main: $border-width-main $border-color-main;
+$border-separator: $border-width-main $primary-color-20;
+$border-main-darker: $border-width-main $primary-color-40;
+$border-main-active: $border-width-main $primary-color-50;
+$border-grey: $border-width-main $cuddle-50;
+$border-grey-light: $border-width-main $cuddle-30;
+$border-grey-lighter: $border-width-main $cuddle-20;
+$border-transparent: $border-width-main $transparent;
+$border-dark: $border-width-main $pico-30;
+$border-dark-lighter: $border-width-main $pico-20;
$border-radius-main: 0.375rem;
$radius-main: 0.375rem;
@@ -142,6 +145,7 @@ $font-900: 850;
// space
$space-unit: 1rem;
+$space-xxxxxs: $space-unit * 0.0625;
$space-xxxxs: $space-unit * 0.125;
$space-xxxs: $space-unit * 0.25;
$space-xxs: $space-unit * 0.375;
diff --git a/aim/web/ui/src/styles/components/_inputs.scss b/aim/web/ui/src/styles/components/_inputs.scss
index 22f64316aa..87493792ab 100644
--- a/aim/web/ui/src/styles/components/_inputs.scss
+++ b/aim/web/ui/src/styles/components/_inputs.scss
@@ -16,7 +16,7 @@ body {
}
.Mui-disabled {
- color: $text-color-50;
+ color: $text-color-50 !important;
.MuiOutlinedInput-notchedOutline {
border-color: $cuddle-70 !important;
}
diff --git a/aim/web/ui/src/styles/components/_tooltip.scss b/aim/web/ui/src/styles/components/_tooltip.scss
index 59864c23f1..720d27a167 100644
--- a/aim/web/ui/src/styles/components/_tooltip.scss
+++ b/aim/web/ui/src/styles/components/_tooltip.scss
@@ -4,3 +4,13 @@
max-height: $tooltip-max-height;
overflow: hidden;
}
+
+.MuiTooltip-tooltipPlacementTop,
+.MuiTooltip-tooltipPlacementBottom {
+ margin: 6px 0;
+}
+
+.MuiTooltip-tooltipPlacementLeft,
+.MuiTooltip-tooltipPlacementRight {
+ margin: 0 6px;
+}
diff --git a/aim/web/ui/src/types/components/IllustrationBlock/IllustrationBlock.d.ts b/aim/web/ui/src/types/components/IllustrationBlock/IllustrationBlock.d.ts
index afeea5a8ae..b1e1514996 100644
--- a/aim/web/ui/src/types/components/IllustrationBlock/IllustrationBlock.d.ts
+++ b/aim/web/ui/src/types/components/IllustrationBlock/IllustrationBlock.d.ts
@@ -19,4 +19,5 @@ export interface IIllustrationBlockProps {
| 'tags';
type?: IllustrationsEnum;
size?: 'small' | 'medium' | 'large' | 'xLarge';
+ showImage?: boolean;
}
diff --git a/aim/web/ui/src/types/components/Table/Table.d.ts b/aim/web/ui/src/types/components/Table/Table.d.ts
index 4410b18455..f98a50b334 100644
--- a/aim/web/ui/src/types/components/Table/Table.d.ts
+++ b/aim/web/ui/src/types/components/Table/Table.d.ts
@@ -79,6 +79,7 @@ export interface ITableProps {
disableRowClick?: boolean;
columnsColorScales?: { [key: string]: boolean };
visualizationElementType?: VisualizationElementEnum;
+ noColumnActions?: boolean;
}
export interface ITableRef {
@@ -104,4 +105,5 @@ export interface IIllustrationConfig {
type?: IIllustrationBlockProps['type'];
title?: IIllustrationBlockProps['title'];
content?: IIllustrationBlockProps['content'];
+ showImage?: IIllustrationBlockProps['showImage'];
}
diff --git a/aim/web/ui/src/types/core/AimObjects/CustomObject.d.ts b/aim/web/ui/src/types/core/AimObjects/CustomObject.d.ts
index 2c9649f958..f699bab3b7 100644
--- a/aim/web/ui/src/types/core/AimObjects/CustomObject.d.ts
+++ b/aim/web/ui/src/types/core/AimObjects/CustomObject.d.ts
@@ -6,8 +6,8 @@ import { Params, RunProps } from './Run';
export interface BaseRangeInfo {
record_range_used: Tuple;
record_range_total: Tuple;
- index_range_used: ?Tuple;
- index_range_total: ?Tuple;
+ index_range_used: Tuple | null;
+ index_range_total: Tuple | null;
}
export interface ObjectSequenceBase extends BaseRangeInfo, SequenceBaseView {
diff --git a/aim/web/ui/src/types/core/AimObjects/Run.d.ts b/aim/web/ui/src/types/core/AimObjects/Run.d.ts
index 0b82095b6e..ea2c9af409 100644
--- a/aim/web/ui/src/types/core/AimObjects/Run.d.ts
+++ b/aim/web/ui/src/types/core/AimObjects/Run.d.ts
@@ -16,12 +16,12 @@ export interface Experiment {
export interface RunProps {
hash: string;
- name: ?string;
- description: ?string;
- experiment: ?Experiment;
- tags: ?Array;
+ name: string | null;
+ description: string | null;
+ experiment: Experiment | null;
+ tags: Array | null;
creation_time: number;
- end_time: ?number;
+ end_time: number | null;
}
export interface RunInfo {
diff --git a/aim/web/ui/src/types/core/AimObjects/Sequence.d.ts b/aim/web/ui/src/types/core/AimObjects/Sequence.d.ts
index d17cd93ee5..2f175569bc 100644
--- a/aim/web/ui/src/types/core/AimObjects/Sequence.d.ts
+++ b/aim/web/ui/src/types/core/AimObjects/Sequence.d.ts
@@ -22,14 +22,14 @@ export interface MetricsBaseView extends SequenceBaseView {
}
export interface SequenceAlignedView extends SequenceBase {
- x_axis_values: ?EncodedNumpyArray;
- x_axis_iters: ?EncodedNumpyArray;
+ x_axis_values: EncodedNumpyArray | null;
+ x_axis_iters: EncodedNumpyArray | null;
}
export interface SequenceFullView extends SequenceAlignedView {
slice: [number, number, number];
- values: ?EncodedNumpyArray;
+ values: EncodedNumpyArray | null;
epochs: Array;
iters: Array;
- timestamps: ?EncodedNumpyArray;
+ timestamps: EncodedNumpyArray | null;
}
diff --git a/aim/web/ui/src/types/core/enums/index.ts b/aim/web/ui/src/types/core/enums/index.ts
index 171f1521f7..3e7b068d35 100644
--- a/aim/web/ui/src/types/core/enums/index.ts
+++ b/aim/web/ui/src/types/core/enums/index.ts
@@ -19,3 +19,8 @@ export enum AimObjectDepths {
Step = 2,
Index = 3,
}
+
+/**
+ * Sequence names as union type
+ */
+export type SequenceTypesUnion = `${SequenceTypesEnum}`;
diff --git a/aim/web/ui/src/types/pages/home/Home.d.ts b/aim/web/ui/src/types/pages/home/Home.d.ts
deleted file mode 100644
index 1f763f9b71..0000000000
--- a/aim/web/ui/src/types/pages/home/Home.d.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { INotification } from 'types/components/NotificationContainer/NotificationContainer';
-
-export interface IHomeProps {
- activityData: IActivityData[];
- notifyData: INotification[];
- askEmailSent: boolean;
- onSendEmail: (data: object) => Promise;
- onNotificationDelete: (id: number) => void;
-}
-
-export interface IActivityData {
- activity_map: { [key: string]: number };
- num_experiments: number;
- num_runs: number;
-}
diff --git a/aim/web/ui/src/types/pages/home/components/Activity/Activity.d.ts b/aim/web/ui/src/types/pages/home/components/Activity/Activity.d.ts
deleted file mode 100644
index 138306a326..0000000000
--- a/aim/web/ui/src/types/pages/home/components/Activity/Activity.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { IActivityData } from 'types/pages/home/components/Home';
-
-export interface IActivityProps {
- activityData: IActivityData;
-}
diff --git a/aim/web/ui/src/types/pages/home/components/AskForm/AskForm.d.ts b/aim/web/ui/src/types/pages/home/components/AskForm/AskForm.d.ts
deleted file mode 100644
index 316b5d73be..0000000000
--- a/aim/web/ui/src/types/pages/home/components/AskForm/AskForm.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { IHomeProps } from 'types/pages/home/Home';
-
-export interface IAskFormProps {
- onSendEmail: IHomeProps['onSendEmail'];
-}
diff --git a/aim/web/ui/src/types/pages/home/components/SetupGuide/SetupGuide.d.ts b/aim/web/ui/src/types/pages/home/components/SetupGuide/SetupGuide.d.ts
deleted file mode 100644
index 1883431613..0000000000
--- a/aim/web/ui/src/types/pages/home/components/SetupGuide/SetupGuide.d.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { IHomeProps } from '../../Home';
-
-export interface ISetupGuideProps {
- askEmailSent: IHomeProps['askEmailSent'];
- onSendEmail: IHomeProps['onSendEmail'];
-}
diff --git a/aim/web/ui/src/utils/saveRecentSearches.ts b/aim/web/ui/src/utils/saveRecentSearches.ts
new file mode 100644
index 0000000000..f2977ab159
--- /dev/null
+++ b/aim/web/ui/src/utils/saveRecentSearches.ts
@@ -0,0 +1,35 @@
+import { getItem, setItem } from './storage';
+
+/**
+ * Save last 3 successful searches to local storage
+ * @param appName - name of the explorer
+ * @param query - search query
+ * @example saveRecentSearches("metrics", "(run.active == False) and ((metric.name == "best_loss") or (metric.name == "bleu"))")
+ * @returns void
+ */
+function saveRecentSearches(appName: string, query: string): void {
+ if (query) {
+ // get recent searches from local storage
+ const recentSearches = JSON.parse(getItem('recentSearches') || '[]');
+
+ // find if search already exists
+ const searchIndex: number = recentSearches.findIndex(
+ (search: { explorer: string; query: string }) =>
+ search.explorer === appName && search.query === query,
+ );
+
+ // skip adding search if it already exists
+ if (searchIndex !== -1) {
+ recentSearches.splice(searchIndex, 1);
+ } else if (recentSearches.length === 3) {
+ // remove first element if array length is 3
+ recentSearches.shift();
+ }
+ // push new search to the start of array
+ recentSearches.unshift({ explorer: appName, query });
+ // save recent searches to local storage
+ setItem('recentSearches', JSON.stringify(recentSearches));
+ }
+}
+
+export default saveRecentSearches;