Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/replay/frontend/src/pages/header/index.vue
1030 views
<template lang="pug">
.HeaderPage.Page(:style="cssVars")
  .NavBar
    .left-buttons
      AppButton(
        @click="back"
        :autoInvert="true"
        :disabled="!hasBack"
        :icon="ICON_BACK"
        :size=18
      )
    .location(ref="location")
      template(v-if="saSession")
        .script-section.section(@mousedown="showLocationOverlay($event, 'locations-menu')")
          Icon(:src="ICON_SCRIPT" :size=16 iconStyle="transform: 'scale(-1,1)'")
          .text {{scriptName}}
        .session-section.section(@mousedown="showLocationOverlay($event, 'script-instances-menu')")
          Icon(:src="ICON_CLOCK" :size=16 iconStyle="transform: 'scale(-1,1)'")
          .text {{scriptInstanceDate}}
        .window-section.section(@mousedown="showLocationOverlay($event, 'sessions-menu')" v-if="saSession.relatedSessions.length > 1")
          Icon(:src="ICON_NUMBER" :size=16 iconStyle="transform: 'scale(-1,1)'")
          .text ({{sessionIndex}} of {{saSession.relatedSessions.length}}) {{sessionName}}
      template(v-else-if="location")
        .name-section.section(@mousedown="showLocationOverlay($event, 'locations-menu')")
          .text SecretAgent

    .right-buttons
      AppButton(
        @click="forward"
        :autoInvert="true"
        :disabled="!hasNext"
        :icon="ICON_FORWARD"
        :size=18
      )
  .UrlBar(v-if="saSession")
    button(v-if="saSession.tabs.length > 1" @click="showTabs" ref="tabRef")
      .text {{activeTabIdx + 1}}/{{saSession.tabs.length}}
    .address-bar
      Icon(:src="addressIcon" :size=16 iconStyle="transform: 'scale(-1,1)'" disabled="true")
      .detached(v-if="isFrozenTab") Frozen
      .text {{currentUrl}}

</template>

<script lang="ts">
import Vue from 'vue';
import Component from 'vue-class-component';
import { ipcRenderer } from 'electron';
import { TOOLBAR_HEIGHT } from '~shared/constants/design';
import {
  ICON_BACK,
  ICON_CLOCK,
  ICON_FORWARD,
  ICON_LOCK,
  ICON_NUMBER,
  ICON_SCRIPT,
  ICON_WINDOW,
  ICON_BOOKMARK,
} from '~frontend/constants/icons';
import NoCache from '~frontend/lib/NoCache';
import Icon from '~frontend/components/Icon.vue';
import os from 'os';
import IWindowLocation from '~shared/interfaces/IWindowLocation';
import { dateToTimeAgo } from '~shared/utils/formatters';
import AppButton from '~frontend/pages/header/components/AppButton.vue';
import ISaSession from '~shared/interfaces/ISaSession';
import { getTheme } from '~shared/utils/themes';
import moment from 'moment';
import Path from 'path';
import settings from '~frontend/lib/settings';

@Component({ components: { AppButton, Icon } })
export default class HeaderPage extends Vue {
  readonly ICON_BACK = ICON_BACK;
  readonly ICON_FORWARD = ICON_FORWARD;
  readonly ICON_SCRIPT = ICON_SCRIPT;
  readonly ICON_CLOCK = ICON_CLOCK;
  readonly ICON_NUMBER = ICON_NUMBER;
  readonly ICON_LOCK = ICON_LOCK;
  readonly ICON_BOOKMARK = ICON_BOOKMARK;
  readonly ICON_WINDOW = ICON_WINDOW;

  $refs: any;
  tabRef: HTMLElement;
  isFullscreen = false;
  activeTabId = 0;

  currentUrl = 'Loading';

  location: IWindowLocation = 'Dashboard';

  saSession: ISaSession = null;
  hasBack = false;
  hasNext = false;

  get isFrozenTab(): boolean {
    if (!this.activeTabId) return false;
    return !!this.saSession.tabs.find(x => x.tabId === this.activeTabId)?.detachedFromTabId;
  }

  get addressIcon() {
    if (this.isFrozenTab) return ICON_BOOKMARK;
    return ICON_LOCK;
  }

  get sessionIndex() {
    return this.saSession.relatedSessions.findIndex(x => x.id === this.saSession.id) + 1;
  }

  get sessionName() {
    let name = this.saSession.name;
    if (name.length > 30) {
      return name.substr(0, 30) + '... ';
    }
    return name;
  }

  get activeTabIdx() {
    return this.saSession.tabs.findIndex(x => x.tabId === this.activeTabId);
  }

  mounted() {
    ipcRenderer.on('location:updated', (e, args) => {
      const { location, saSession, hasNext, hasBack } = args;
      console.log('location:updated', args);
      this.location = location ?? null;
      this.saSession = saSession ?? null;
      this.hasNext = hasNext;
      this.hasBack = hasBack;
    });

    ipcRenderer.on('replay:tab', (e, tab) => {
      const index = this.saSession.tabs.findIndex(x => x.tabId === tab.tabId);
      if (index === -1) this.saSession.tabs.push(tab);
      else this.saSession.tabs[index] = tab;
    });

    ipcRenderer.on('replay:active-tab', (e, tabId) => {
      this.activeTabId = tabId;
    });

    ipcRenderer.on('replay:page-url', (e, url) => {
      this.currentUrl = url;
    });

    ipcRenderer.on('fullscreen', (e, fullscreen: boolean) => {
      this.isFullscreen = fullscreen;
    });
  }

  get scriptInstanceDate() {
    if (
      this.saSession.relatedScriptInstances?.length &&
      this.saSession.scriptStartDate === this.saSession.relatedScriptInstances[0].startDate
    ) {
      return 'Latest';
    }
    return dateToTimeAgo(this.saSession.scriptStartDate);
  }

  get scriptName() {
    return this.saSession.scriptEntrypoint.split(Path.sep).filter(Boolean).slice(-2).join('/');
  }

  @NoCache
  get cssVars() {
    const theme = getTheme(settings.theme);
    return {
      '--addressBarBackgroundColor': theme.addressBarBackgroundColor,
      '--addressBarTextColor': theme.addressBarTextColor,
      '--paddingLeft': (this.isMac() && !this.isFullscreen ? 78 : 4) + 'px',
      '--toolbarLightForeground': theme.toolbarLightForeground || '1px solid rgba(0, 0, 0, 0.12)',
      '--toolbarHeight': `${TOOLBAR_HEIGHT}px`,
      '--toolbarBackgroundColor': theme.toolbarBackgroundColor,
      '--toolbarBorderBottomColor': theme.toolbarBottomLineBackgroundColor,
      '--navbarWidth': this.saSession?.relatedSessions?.length > 1 ? '60%' : '40%',
    };
  }

  isMac() {
    return os.platform() === 'darwin';
  }

  back() {
    ipcRenderer.send('go-back');
  }

  forward() {
    ipcRenderer.send('go-forward');
  }

  showTabs() {
    const overlayRect = this.$refs.tabRef.getBoundingClientRect().toJSON();

    const firstTabTime = moment(this.saSession.tabs[0].createdTime);
    let tabLabelsById = new Map<number, string>();
    let tabCounter = 0;
    const tabs = this.saSession.tabs.map((x, i) => {
      let label = '';
      let time = '';
      if (tabCounter === 1) label = '2nd ';
      else if (tabCounter === 2) label = '3rd ';
      else if (tabCounter > 2) label = `${i + 1}th `;

      if (i === 0) {
        time = firstTabTime.format('h:mma');
      } else {
        const newMoment = moment(x.createdTime);
        const millis = newMoment.diff(firstTabTime, 'milliseconds');
        if (millis < 1e3) {
          time = `+${millis}ms`;
        } else {
          const secs = newMoment.diff(firstTabTime, 'seconds', true);
          time = `+${secs}s`;
        }
      }
      let title: string;
      if (!x.detachedFromTabId) {
        title = `${i + 1}/${this.saSession.tabs.length} - ${label}tab opened at ${time}`;
        tabLabelsById.set(x.tabId, label || '1st ');
        tabCounter += 1;
      } else {
        const sourceTabNumber = tabLabelsById.get(x.detachedFromTabId);
        title = `${i + 1}/${
          this.saSession.tabs.length
        } - detached ${sourceTabNumber}tab at ${time}`;
      }

      return {
        title,
        isActive: this.activeTabId === x.tabId,
        id: x.tabId,
      };
    });
    overlayRect.y += 3;
    overlayRect.width = 500;
    ipcRenderer.send(
      'overlay:show',
      'list-menu',
      overlayRect,
      tabs,
      'ICON_WINDOW',
      'navigate-to-session-tab',
    );
  }

  showLocationOverlay(e: any, name) {
    const overlayRect = e.target.getBoundingClientRect().toJSON();
    const parentRect = this.$refs.location.getBoundingClientRect();
    const parentWidth = this.$refs.location.getBoundingClientRect().width;
    const fromLeft = overlayRect.left - parentRect.left;
    if (name === 'locations-menu') {
      overlayRect.width = parentWidth;
      overlayRect.x -= fromLeft;
      return ipcRenderer.send('overlay:toggle', name, overlayRect);
    }

    overlayRect.width = parentWidth - fromLeft;

    if (name === 'script-instances-menu') {
      const instances = this.saSession.relatedScriptInstances.map((x, i) => {
        return {
          id: x.id,
          title: i === 0 ? 'Latest' : dateToTimeAgo(x.startDate),
          scriptInstanceId: x.id,
          isActive: this.saSession.scriptInstanceId === x.id,
          dataLocation: this.saSession.dataLocation,
          sessionName: this.saSession.name,
        };
      });
      ipcRenderer.send(
        'overlay:show',
        'list-menu',
        overlayRect,
        instances,
        'ICON_CLOCK',
        'navigate-to-history',
      );
    } else if (name === 'sessions-menu') {
      ipcRenderer.send(
        'overlay:show',
        'list-menu',
        overlayRect,
        this.saSession.relatedSessions.map((x, i) => ({
          id: x.id,
          title: x.name,
          isActive: this.saSession.name === x.name,
          name: x.name,
        })),
        'ICON_NUMBER',
        'navigate-to-session',
      );
    }
  }
}
</script>

<style lang="scss">
@import '../../assets/style/common-mixins';
@include baseStyle();
.HeaderPage {
  background-color: var(--toolbarBackgroundColor);
  -webkit-app-region: drag;
  box-shadow: 0 0 2px rgba(0, 0, 0, 0.8);
  padding-top: 2px;
  padding-bottom: 2px;
  box-sizing: border-box;

  .NavBar {
    display: flex;
    align-items: center;
    margin: 0 auto;
    width: var(--navbarWidth);
    min-width: 400px;
    position: relative;
    z-index: 2;
    top: 10px;
    background: white;
    cursor: pointer;
    color: rgba(0, 0, 0, 0.8);
    box-shadow: 0 0 1px rgba(0, 0, 0, 0.12), 0 1px 1px rgba(0, 0, 0, 0.16);
    border: 1px solid rgba(0, 0, 0, 0.2);
    height: 30px;
    overflow: hidden;
    border-radius: 3px;
    -webkit-app-region: no-drag;

    .left-buttons {
      width: 30px;
      border-right: 1px solid rgba(0, 0, 0, 0.1);
    }

    .right-buttons {
      width: 30px;
      border-left: 1px solid rgba(0, 0, 0, 0.1);
      position: relative;
    }

    .location {
      cursor: pointer;
      flex: 10;
      display: flex;
      height: 100%;
      align-items: center;
      color: var(--addressBarTextColor);

      .section {
        display: flex;
        flex: 1;
        cursor: pointer;
        position: relative;
        align-items: center;
        height: 100%;
        font-size: 13px;
        white-space: nowrap;
        border-left: 1px solid rgba(0, 0, 0, 0.1);
        .text {
          pointer-events: none;
          cursor: pointer;
          padding-left: 5px;
        }
        .Icon {
          pointer-events: none;
          cursor: pointer;
          margin-top: 1px;
          vertical-align: middle;
          width: 20px;
          margin-left: 10px;
        }
        &:first-child {
          border-left: none;
        }
        &:hover {
          background: rgba(0, 0, 0, 0.05);
        }
        &.name-section {
          justify-content: center;
        }
      }
    }
  }

  .UrlBar {
    flex: 1;
    display: flex;
    align-items: stretch;
    margin: 3px 10px 6px;
    -webkit-app-region: no-drag;
    button {
      margin-left: 0;
      min-width: 36px;
      margin-right: 5px;
      font-weight: bold;
      border: 1px solid var(--toolbarBorderBottomColor);
      border-radius: 4px;
      padding: 4px 10px;
      cursor: pointer;
    }
    .detached {
      margin: auto 5px;
      padding: 0 5px;
      background: #3498db;
      line-height: 20px;
      color: white;
      height: 20px;
      border-radius: 5px;
      font-size: 10px;
      vertical-align: middle;
    }
    .address-bar {
      flex: 10;
      display: flex;
      align-items: center;
      pointer-events: none;
      position: relative;
      font-size: 15px;
      overflow: hidden;
      height: 30px;
      border-radius: 5px;
      border: 1px solid rgba(0, 0, 0, 0.1);
      background-color: #fafafa;
      box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1);
      padding-left: 10px;

      .text {
        padding-left: 5px;
        font-weight: lighter;
        color: #676767;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
      }
      .Icon {
        margin-top: 1px;
        width: 16px;
        height: 13px;
        flex: 0 0 auto;
      }
    }
  }
}
</style>