Newer
Older
smart-home-server / webclient-vue / src / features / scripts / pages / ScriptDetailPage.vue
@Eugene Sukhodolskiy Eugene Sukhodolskiy 23 hours ago 7 KB Add script detail pages with scope grouping
<template>
  <section class="page">
    <AppLoadingState v-if="isLoadingList" :text="loadingText" />

    <AppErrorState
      v-else-if="listError && !hasList"
      :title="`${kicker} loading failed`"
      :message="listError.message"
      :retry="init"
    />

    <div v-else-if="script">
      <GnPageHeader :title="script.name || script.alias || script.name" :kicker="kicker">
        <template #actions>
          <GnSwitch
            :model-value="script.state === 'enabled'"
            label="Enabled"
            @update:model-value="toggleState($event)"
          />
          <GnButton
            v-if="isAction"
            variant="primary"
            icon="ph-play"
            :loading="scriptsStore.isRunning(script.alias)"
            :disabled="script.state !== 'enabled'"
            @click="run"
          >
            Run
          </GnButton>
        </template>
      </GnPageHeader>

      <div class="script-detail-meta">
        <div v-if="script.icon" v-html="script.icon" class="script-icon" />
        <p v-if="script.description">{{ script.description }}</p>
        <div class="script-meta">
          <GnBadge :variant="script.state === 'enabled' ? 'success' : 'secondary'"
          >{{ script.state }}</GnBadge>
          <code>{{ script.alias || script.name }}</code>
          <small v-if="script.author">{{ script.author }}</small>
        </div>
      </div>

      <div v-if="isScope && scopeActions.length > 0" class="devices-panel">
        <div class="block-title">Action scripts ({{ scopeActions.length }})</div>
        <GnTable :rows="scopeActions" :columns="scriptColumns">
          <template #cell-state="{ row }">
            <GnBadge :variant="row.state === 'enabled' ? 'success' : 'secondary'">{{ row.state }}</GnBadge>
          </template>
          <template #cell-alias="{ row }">
            <router-link
              :to="{ name: 'script-detail', params: { type: 'actions', id: row.alias } }"
              class="script-link"
            >
              {{ row.alias }}
            </router-link>
          </template>
        </GnTable>
      </div>

      <div v-if="isScope && scopeRegular.length > 0" class="devices-panel">
        <div class="block-title">Regular scripts ({{ scopeRegular.length }})</div>
        <GnTable :rows="scopeRegular" :columns="scriptColumns">
          <template #cell-state="{ row }">
            <GnBadge :variant="row.state === 'enabled' ? 'success' : 'secondary'">{{ row.state }}</GnBadge>
          </template>
          <template #cell-alias="{ row }">
            <router-link
              :to="{ name: 'script-detail', params: { type: 'regular', id: row.alias } }"
              class="script-link"
            >
              {{ row.alias }}
            </router-link>
          </template>
        </GnTable>
      </div>

      <GnAlert v-if="resultAlert" :variant="resultAlert.variant" class="result-alert">
        <strong>{{ resultAlert.title }}</strong>
        <p v-if="resultAlert.message">{{ resultAlert.message }}</p>
      </GnAlert>
    </div>

    <AppEmptyState
      v-else
      title="Not found"
      message="The requested script does not exist."
    />
  </section>
</template>

<script setup>
import { ref, computed, onMounted } from "vue";
import { useRoute } from "vue-router";
import { useScriptsStore } from "../../../stores/scripts";
import {
  GnPageHeader,
  GnBadge,
  GnButton,
  GnSwitch,
  GnAlert,
  GnTable,
} from "gnexus-ui-kit/vue";
import AppLoadingState from "../../../components/feedback/AppLoadingState.vue";
import AppErrorState from "../../../components/feedback/AppErrorState.vue";
import AppEmptyState from "../../../components/feedback/AppEmptyState.vue";

const route = useRoute();
const scriptsStore = useScriptsStore();

const scriptType = computed(() => route.params.type);
const scriptId = computed(() => route.params.id);

const isAction = computed(() => scriptType.value === "actions");
const isRegular = computed(() => scriptType.value === "regular");
const isScope = computed(() => scriptType.value === "scopes");

const kicker = computed(() => {
  if (isAction.value) return "Actions";
  if (isRegular.value) return "Regular";
  if (isScope.value) return "Scope";
  return "Script";
});

const loadingText = computed(() => `Loading ${kicker.value.toLowerCase()} details`);

const isLoadingList = computed(() => {
  if (isAction.value) return scriptsStore.isLoadingActions;
  if (isRegular.value) return scriptsStore.isLoadingRegular;
  if (isScope.value) return scriptsStore.isLoadingScopes || scriptsStore.isLoadingActions || scriptsStore.isLoadingRegular;
  return false;
});

const listError = computed(() => {
  if (isAction.value) return scriptsStore.errorActions;
  if (isRegular.value) return scriptsStore.errorRegular;
  if (isScope.value) return scriptsStore.errorScopes;
  return null;
});

const hasList = computed(() => {
  if (isAction.value) return scriptsStore.actions.length > 0;
  if (isRegular.value) return scriptsStore.regular.length > 0;
  if (isScope.value) return scriptsStore.scopes.length > 0;
  return false;
});

const script = computed(() => {
  const id = scriptId.value;
  if (!id) return null;
  if (isAction.value) return scriptsStore.actionByAlias(id);
  if (isRegular.value) return scriptsStore.regularByAlias(id);
  if (isScope.value) return scriptsStore.scopeByName(id);
  return null;
});

const scopeActions = computed(() => {
  const id = scriptId.value;
  if (!id || !isScope.value) return [];
  return scriptsStore.actionsByScope(id);
});

const scopeRegular = computed(() => {
  const id = scriptId.value;
  if (!id || !isScope.value) return [];
  return scriptsStore.regularByScope(id);
});

const resultAlert = ref(null);

const resultAlertComputed = computed(() => {
  const r = scriptsStore.lastRunResult;
  if (!r) return null;

  if (r.ok) {
    return {
      variant: "success",
      title: `Ran ${r.alias}`,
      message: r.execTime ? `Exec time: ${r.execTime}` : undefined,
    };
  }

  return {
    variant: "danger",
    title: `Failed ${r.alias}`,
    message: r.error?.message || "Unknown error",
  };
});

async function run() {
  if (!script.value?.alias) return;
  resultAlert.value = null;
  const result = await scriptsStore.runScript(script.value.alias);
  resultAlert.value = resultAlertComputed.value;
}

async function toggleState(enabled) {
  const id = scriptId.value;
  if (!id) return;

  if (isAction.value) {
    await scriptsStore.setActionState(id, enabled);
  } else if (isRegular.value) {
    await scriptsStore.setRegularState(id, enabled);
  } else if (isScope.value) {
    await scriptsStore.setScopeState(id, enabled);
  }
}

async function init() {
  const id = scriptId.value;
  if (!id) return;

  if (isAction.value && scriptsStore.actions.length === 0) {
    await scriptsStore.loadActions();
  } else if (isRegular.value && scriptsStore.regular.length === 0) {
    await scriptsStore.loadRegular();
  } else if (isScope.value && scriptsStore.scopes.length === 0) {
    await scriptsStore.loadScopes();
  }

  if (isScope.value) {
    if (scriptsStore.actions.length === 0) {
      await scriptsStore.loadActions();
    }
    if (scriptsStore.regular.length === 0) {
      await scriptsStore.loadRegular();
    }
  }
}

const scriptColumns = [
  { key: "alias", label: "Alias" },
  { key: "name", label: "Name" },
  { key: "state", label: "State" },
];

onMounted(() => {
  init();
});
</script>

<style scoped>
.script-detail-meta {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin-bottom: 24px;
}

.script-icon {
  font-size: 32px;
}

.script-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}

.result-alert {
  margin-top: 24px;
}

.devices-panel {
  margin-bottom: 24px;
}

.block-title {
  font-weight: 700;
  text-transform: uppercase;
  margin-bottom: 12px;
  color: var(--color-primary);
}

.script-link {
  color: var(--color-primary);
}
</style>