-
schedule
-
Block Production
-
- @if (text) {
- {{ text }}
- } @else {
- {{ producingIn ? ('in ' + producingIn) : 'in ...' }}
- }
+
+
+
+ @if (!isMobile) {
+
schedule
+ }
+
Block Production
+
+ @if (text) {
+ {{ text }}
+ } @else {
+ @if (producingIn) {
+ in
+ {{ producingIn }}
+ } @else {
+ in ...
+ }
+ }
+
diff --git a/frontend/src/app/layout/block-production-pill/block-production-pill.component.scss b/frontend/src/app/layout/block-production-pill/block-production-pill.component.scss
index ae1d52e03f..e6cb62f294 100644
--- a/frontend/src/app/layout/block-production-pill/block-production-pill.component.scss
+++ b/frontend/src/app/layout/block-production-pill/block-production-pill.component.scss
@@ -3,56 +3,159 @@
$blue: #57d7ff;
$pink: #fda2ff;
$orange: #ff833d;
+@media (max-width: 767px) {
+ :host.h-sm {
+ height: 24px !important;
+ }
+}
:host {
+ position: relative;
min-width: 170px;
- background-image: linear-gradient(25deg, rgba($blue, 0.8), rgba($pink, 0.8), rgba($orange, 0.8));
- padding: 1px;
- $h-sm: 24px;
- height: calc($h-sm - 2px);
- @media (max-width: 768px) {
- $h-sm: 32px;
- height: calc($h-sm - 2px);
- }
-
- &.hidden {
- min-width: 0;
- max-width: 0;
- padding: 0;
-
- > * {
- display: none;
- }
+ height: 26px;
+ @media (min-width: 768px) {
+ margin-right: 8px;
}
}
.pill {
background-color: $base-background;
height: 100%;
+ min-width: 170px;
+ @media (max-width: 767px) {
+ font-size: 12px;
+ min-width: unset;
+ .pill-inside2 {
+ justify-content: center;
+ }
+ }
- .pill-inside {
- padding-left: 3px;
- background: linear-gradient(25deg, rgba($blue, 0.2), rgba($pink, 0.2), rgba($orange, 0.2));
+ .pill-inside1 {
+ position: absolute;
+ background-color: $base-background;
+ top: 1px;
+ left: 1px;
+ height: calc(100% - 2px);
+ width: calc(100% - 2px);
- .mina-icon,
- .bp,
- .time {
- background: linear-gradient(12deg, $blue, $pink, $orange);
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- }
+ .pill-inside2 {
+ padding-left: 6px;
+ background: linear-gradient(25deg, rgba($blue, 0.2), rgba($pink, 0.2), rgba($orange, 0.2));
+
+ .mina-icon {
+ margin-top: 1px;
+ }
- &:hover {
.mina-icon,
.bp,
.time {
- background: linear-gradient(100deg, $blue, $pink, $orange);
+ background: linear-gradient(12deg, $blue, $pink, $orange);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
+
+ &:hover {
+ .mina-icon,
+ .bp,
+ .time {
+ background: linear-gradient(100deg, $blue, $pink, $orange);
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+ }
+ }
}
}
+}
+
+
+.comet-border {
+ position: relative;
+ margin: auto;
+ border-radius: 5px;
+ overflow: hidden;
+}
+
+.comet-border:before {
+ content: "";
+ background-image: conic-gradient(
+ $orange 5deg,
+ $pink 10deg,
+ $pink 50deg,
+ $blue 90deg,
+ transparent 140deg
+ );
+ height: 80px;
+ width: 80px;
+ position: absolute;
+ animation: rotate 8s infinite linear;
+}
+@media (min-width: 768px) {
+ @keyframes rotate {
+ /* From left bottom to right bottom */
+ 0% {
+ left: -21%;
+ transform: rotate(-45deg);
+ }
+ 5% {
+ left: -21%;
+ transform: rotate(-185deg);
+ }
+ /* Turn around */
+ 15% {
+ transform: rotate(-235deg);
+ }
+ /* From right top to left top */
+ 50% {
+ left: 71%;
+ transform: rotate(-235deg);
+ }
+ 55% {
+ left: 65%;
+ }
+ /* No turn around. -405 = -45 visually */
+ 62% {
+ transform: rotate(-405deg);
+ }
+ 100% {
+ left: -21%;
+ transform: rotate(-405deg);
+ }
+ }
+}
+
+@media (max-width: 767px) {
+ @keyframes rotate {
+ 0% {
+ left: -10%;
+ transform: rotate(-45deg);
+ }
+ 5% {
+ left: -10%;
+ transform: rotate(-185deg);
+ }
+ 15% {
+ transform: rotate(-255deg);
+ }
+ 40% {
+ transform: rotate(-255deg);
+ }
+ 45% {
+ left: 75%;
+ transform: rotate(-340deg);
+ }
+ 50% {
+ transform: rotate(-405deg);
+ }
+ 58% {
+ left: 50%;
+ transform: rotate(-405deg);
+ }
+ 100% {
+ left: -10%;
+ transform: rotate(-405deg);
+ }
+ }
}
diff --git a/frontend/src/app/layout/block-production-pill/block-production-pill.component.ts b/frontend/src/app/layout/block-production-pill/block-production-pill.component.ts
index 2e460cbf11..ccc7c581d9 100644
--- a/frontend/src/app/layout/block-production-pill/block-production-pill.component.ts
+++ b/frontend/src/app/layout/block-production-pill/block-production-pill.component.ts
@@ -1,13 +1,15 @@
import { ChangeDetectionStrategy, Component, HostBinding, OnDestroy, OnInit } from '@angular/core';
import { AppSelectors } from '@app/app.state';
import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class';
-import { AppNodeDetails } from '@shared/types/app/app-node-details.type';
+import { AppNodeDetails, AppNodeStatus } from '@shared/types/app/app-node-details.type';
import { getTimeDiff } from '@shared/helpers/date.helper';
import { Router } from '@angular/router';
import { Routes } from '@shared/enums/routes.enum';
import { BlockProductionWonSlotsStatus } from '@shared/types/block-production/won-slots/block-production-won-slots-slot.type';
import { filter } from 'rxjs';
-import { isFeatureEnabled, isSubFeatureEnabled } from '@shared/constants/config';
+import { isSubFeatureEnabled } from '@shared/constants/config';
+import { getMergedRoute, isMobile, MergedRoute, removeParamsFromURL } from '@openmina/shared';
+import { BlockProductionWonSlotsActions } from '@block-production/won-slots/block-production-won-slots.actions';
@Component({
selector: 'mina-block-production-pill',
@@ -16,28 +18,26 @@ import { isFeatureEnabled, isSubFeatureEnabled } from '@shared/constants/config'
templateUrl: './block-production-pill.component.html',
styleUrl: './block-production-pill.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
- host: { class: 'mr-8 border-rad-6' },
+ host: { class: 'border-rad-6' },
})
export class BlockProductionPillComponent extends StoreDispatcher implements OnInit, OnDestroy {
text: string = null;
producingIn: string = null;
+ isMobile: boolean = isMobile();
private globalSlot: number = null;
private interval: any;
private producingValue: number = null;
-
- @HostBinding('class.hidden') private hideComponent = false;
+ private activeSubMenu: string;
constructor(private router: Router) { super(); }
ngOnInit(): void {
this.listenToActiveNode();
+ this.listenToRouteChange();
}
private listenToActiveNode(): void {
- this.select(AppSelectors.activeNode, (node) => {
- this.hideComponent = !isSubFeatureEnabled(node, 'block-production', 'won-slots');
- });
this.select(AppSelectors.activeNodeDetails, (details: AppNodeDetails) => {
if (details.producingBlockStatus === BlockProductionWonSlotsStatus.Committed) {
this.text = 'active';
@@ -53,6 +53,12 @@ export class BlockProductionPillComponent extends StoreDispatcher implements OnI
}, filter((details: AppNodeDetails) => !!details));
}
+ private listenToRouteChange(): void {
+ this.select(getMergedRoute, (route: MergedRoute) => {
+ this.activeSubMenu = route.url.split('/')[2] ? removeParamsFromURL(route.url.split('/')[2]) : null;
+ }, filter(Boolean));
+ }
+
private clearInterval(): void {
if (this.interval) {
clearInterval(this.interval);
@@ -64,6 +70,10 @@ export class BlockProductionPillComponent extends StoreDispatcher implements OnI
if (!this.globalSlot) {
return;
}
+ if (this.activeSubMenu === Routes.WON_SLOTS) {
+ this.dispatch2(BlockProductionWonSlotsActions.setActiveSlotNumber({ slotNumber: this.globalSlot }));
+ return;
+ }
this.router.navigate([Routes.BLOCK_PRODUCTION, Routes.WON_SLOTS, this.globalSlot], { queryParamsHandling: 'merge' });
}
diff --git a/frontend/src/app/layout/error-preview/error-preview.component.ts b/frontend/src/app/layout/error-preview/error-preview.component.ts
index d75d835854..71f80dac62 100644
--- a/frontend/src/app/layout/error-preview/error-preview.component.ts
+++ b/frontend/src/app/layout/error-preview/error-preview.component.ts
@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, ComponentRef, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { MinaState } from '@app/app.setup';
-import { ManualDetection } from '@openmina/shared';
+import { isDesktop, isMobile, ManualDetection } from '@openmina/shared';
import { selectErrorPreviewErrors } from '@error-preview/error-preview.state';
import { filter, take } from 'rxjs';
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
@@ -23,6 +23,7 @@ export class ErrorPreviewComponent extends ManualDetection implements OnInit {
newError: MinaError;
unreadErrors: boolean;
openedOverlay: boolean;
+ isMobile: boolean = isMobile();
private overlayRef: OverlayRef;
private errorListComponent: ComponentRef
;
diff --git a/frontend/src/app/layout/menu-tabs/menu-tabs.component.html b/frontend/src/app/layout/menu-tabs/menu-tabs.component.html
new file mode 100644
index 0000000000..ec33cab6f3
--- /dev/null
+++ b/frontend/src/app/layout/menu-tabs/menu-tabs.component.html
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/frontend/src/app/layout/menu-tabs/menu-tabs.component.scss b/frontend/src/app/layout/menu-tabs/menu-tabs.component.scss
new file mode 100644
index 0000000000..ae543e9c51
--- /dev/null
+++ b/frontend/src/app/layout/menu-tabs/menu-tabs.component.scss
@@ -0,0 +1,24 @@
+@import 'openmina';
+
+:host {
+ height: 56px;
+
+ .menus {
+ gap: 12px;
+
+ .menu {
+ color: $base-tertiary;
+ min-width: 72px;
+ flex: 1 0 auto;
+
+ &.active {
+ color: $selected-primary;
+
+ .mina-icon {
+ color: $selected-primary;
+ font-variation-settings: 'FILL' 1, 'wght' 300, 'GRAD' 0, 'opsz' 20;
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/src/app/layout/menu-tabs/menu-tabs.component.ts b/frontend/src/app/layout/menu-tabs/menu-tabs.component.ts
new file mode 100644
index 0000000000..b0a02004cc
--- /dev/null
+++ b/frontend/src/app/layout/menu-tabs/menu-tabs.component.ts
@@ -0,0 +1,63 @@
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { getMergedRoute, HorizontalMenuComponent, ManualDetection, MergedRoute, removeParamsFromURL } from '@openmina/shared';
+import { getAvailableFeatures } from '@shared/constants/config';
+import { MENU_ITEMS, MenuItem } from '@app/layout/menu/menu.component';
+import { Store } from '@ngrx/store';
+import { MinaState } from '@app/app.setup';
+import { filter, map, tap } from 'rxjs';
+import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
+import { MinaNode } from '@shared/types/core/environment/mina-env.type';
+import { AppSelectors } from '@app/app.state';
+import { RouterLink } from '@angular/router';
+import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class';
+
+@UntilDestroy()
+@Component({
+ selector: 'mina-menu-tabs',
+ standalone: true,
+ imports: [
+ HorizontalMenuComponent,
+ RouterLink,
+ ],
+ templateUrl: './menu-tabs.component.html',
+ styleUrl: './menu-tabs.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: { class: 'flex-column w-100' },
+})
+export class MenuTabsComponent extends StoreDispatcher implements OnInit {
+
+ menuItems: MenuItem[] = this.allowedMenuItems;
+ activeRoute: string;
+ activeNode: MinaNode;
+ readonly trackMenus = (_: number, item: MenuItem): string => item.name;
+
+ ngOnInit(): void {
+ this.listenToActiveNodeChange();
+ let lastUrl: string;
+ this.store.select(getMergedRoute)
+ .pipe(
+ filter(Boolean),
+ map((route: MergedRoute) => route.url),
+ filter(url => url !== lastUrl),
+ tap(url => lastUrl = url),
+ untilDestroyed(this),
+ )
+ .subscribe((url: string) => {
+ this.activeRoute = removeParamsFromURL(url).split('/')[1];
+ this.detect();
+ });
+ }
+
+ private listenToActiveNodeChange(): void {
+ this.select(AppSelectors.activeNode, (node: MinaNode) => {
+ this.activeNode = node;
+ this.menuItems = this.allowedMenuItems;
+ this.detect();
+ }, filter(node => !!node));
+ }
+
+ private get allowedMenuItems(): MenuItem[] {
+ const features = getAvailableFeatures(this.activeNode || { features: {} } as any);
+ return MENU_ITEMS.filter((opt: MenuItem) => features.find(f => f === opt.name.toLowerCase().split(' ').join('-')));
+ }
+}
diff --git a/frontend/src/app/layout/menu/menu.component.scss b/frontend/src/app/layout/menu/menu.component.scss
index 1e8329053b..57a0ef3e7d 100644
--- a/frontend/src/app/layout/menu/menu.component.scss
+++ b/frontend/src/app/layout/menu/menu.component.scss
@@ -103,7 +103,7 @@
}
}
-@media (max-width: 768px) {
+@media (max-width: 767px) {
.menu {
.menu-toggle {
height: 56px !important;
@@ -148,7 +148,7 @@
transform: translate(0) rotate(0);
animation: rotWithOpc 0.3s linear;
- @media (min-width: 769px) {
+ @media (min-width: 768px) {
left: 42px;
&.collapsed {
transform: translate(-75px, -76px) rotate(-90deg);
diff --git a/frontend/src/app/layout/menu/menu.component.ts b/frontend/src/app/layout/menu/menu.component.ts
index 49d59de000..c3ef94c0db 100644
--- a/frontend/src/app/layout/menu/menu.component.ts
+++ b/frontend/src/app/layout/menu/menu.component.ts
@@ -19,13 +19,13 @@ import { filter, map, tap } from 'rxjs';
import { CONFIG, getAvailableFeatures } from '@shared/constants/config';
import { MinaNetwork } from '@shared/types/core/mina/mina.type';
-interface MenuItem {
+export interface MenuItem {
name: string;
icon: string;
tooltip?: string;
}
-const MENU_ITEMS: MenuItem[] = [
+export const MENU_ITEMS: MenuItem[] = [
{ name: 'Dashboard', icon: 'dashboard' },
{ name: 'Block Production', icon: 'library_add' },
{ name: 'Nodes', icon: 'margin' },
diff --git a/frontend/src/app/layout/server-status/server-status.component.html b/frontend/src/app/layout/server-status/server-status.component.html
index f5baafde0b..8ccba7ebac 100644
--- a/frontend/src/app/layout/server-status/server-status.component.html
+++ b/frontend/src/app/layout/server-status/server-status.component.html
@@ -1,29 +1,26 @@
-
+
blur_circular
-
- {{ details.transactions }} Txs
- {{ details.snarks }} SNARKs
-
+
{{ details.transactions }} Txs
+
{{ details.snarks }} SNARKs
language
- @if (!isMobile) {
-
{{ details.peers }} Peers
-
{{ details.download }} / {{ details.upload }} MBps
- }
+
{{ details.peers }} Peers
+
{{ details.download }} / {{ details.upload }} MBps
@@ -32,33 +29,35 @@
-
dns
-
- {{ details.status }}
- #{{ details.blockHeight }}
- {{ blockTimeAgo ? blockTimeAgo + ' ago' : '' }}
-
+ @if (!isMobile) {
+
dns
+ }
+
{{ details.status }}
+
#{{ details.blockHeight }}
+
{{ blockTimeAgo ? blockTimeAgo + ' ago' : '' }}
-
-
-
+ @if (!isMobile) {
+
+
{{ !switchForbidden ? activeNode?.name : 'All Nodes' }}
-
- arrow_drop_down
+ @if (!switchForbidden && (canAddNodes || nodes.length > 1)) {
+ arrow_drop_down
+ }
+
-
+ }
diff --git a/frontend/src/app/layout/server-status/server-status.component.scss b/frontend/src/app/layout/server-status/server-status.component.scss
index 505c0ea331..5230d13696 100644
--- a/frontend/src/app/layout/server-status/server-status.component.scss
+++ b/frontend/src/app/layout/server-status/server-status.component.scss
@@ -102,22 +102,45 @@
gap: 4px;
margin-left: 4px;
}
+
+ @media (max-width: 767px) {
+ width: 100%;
+ &.h-sm,
+ .h-sm {
+ height: 24px !important;
+ }
+ }
}
.node-status {
- &.can-add-nodes {
- .chip,
- .chip::before {
- border-bottom-right-radius: 0 !important;
- border-top-right-radius: 0 !important;
+
+ @media (min-width: 768px) {
+ &.can-add-nodes {
+ .chip,
+ .chip::before {
+ border-bottom-right-radius: 0 !important;
+ border-top-right-radius: 0 !important;
+ }
}
- }
- .chip {
+ .chip {
+ &,
+ &::before {
+ border-top-right-radius: 0 !important;
+ border-bottom-right-radius: 0 !important;
+ }
+ }
+ }
+ @media (max-width: 767px) {
+ font-size: 12px;
&,
- &::before {
- border-top-right-radius: 0 !important;
- border-bottom-right-radius: 0 !important;
+ .shine-parent,
+ .chip {
+ width: 100%;
+ margin: 0 !important;
+ }
+ .chip {
+ justify-content: center;
}
}
diff --git a/frontend/src/app/layout/server-status/server-status.component.ts b/frontend/src/app/layout/server-status/server-status.component.ts
index 60dba80042..7ba694d134 100644
--- a/frontend/src/app/layout/server-status/server-status.component.ts
+++ b/frontend/src/app/layout/server-status/server-status.component.ts
@@ -73,7 +73,7 @@ export class ServerStatusComponent extends StoreDispatcher implements OnInit {
@ViewChild('overlayOpener') private overlayOpener: ElementRef;
- private nodes: MinaNode[] = [];
+ nodes: MinaNode[] = [];
private tooltipOverlayRef: OverlayRef;
private nodePickerOverlayRef: OverlayRef;
private nodePickerComponent: ComponentRef;
@@ -162,7 +162,7 @@ export class ServerStatusComponent extends StoreDispatcher implements OnInit {
minWidth: isMobile() ? '100%' : '220px',
scrollStrategy: this.overlay.scrollStrategies.close(),
positionStrategy: this.overlay.position()
- .flexibleConnectedTo(this.overlayOpener.nativeElement)
+ .flexibleConnectedTo(this.overlayOpener ? this.overlayOpener.nativeElement : (event.target as HTMLElement))
.withPositions([{
originX: 'end',
originY: 'bottom',
diff --git a/frontend/src/app/layout/toolbar/toolbar.component.html b/frontend/src/app/layout/toolbar/toolbar.component.html
index 16072ce2b9..9a3136eb22 100644
--- a/frontend/src/app/layout/toolbar/toolbar.component.html
+++ b/frontend/src/app/layout/toolbar/toolbar.component.html
@@ -1,17 +1,25 @@