Current File : /home/getxxhzo/app.genicards.com/vendor/opcodesio/log-viewer/resources/js/components/FileList.vue |
<template>
<nav class="flex flex-col h-full py-5">
<div class="mx-3 md:mx-0 mb-1">
<div class="sm:flex sm:flex-col-reverse">
<h1 class="font-semibold text-brand-700 dark:text-brand-600 text-2xl flex items-center">
Log Viewer
<a href="https://www.github.com/opcodesio/log-viewer" target="_blank"
class="rounded ml-3 text-gray-400 hover:text-brand-800 dark:hover:text-brand-600 focus:outline-none focus:ring-2 focus:ring-brand-500 dark:focus:ring-brand-700 p-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" title="">
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"></path>
</svg>
</a>
<span class="md:hidden flex-1 flex justify-end">
<SiteSettingsDropdown class="ml-2" />
<button type="button" class="menu-button">
<XMarkIcon class="w-5 h-5 ml-2" @click="fileStore.toggleSidebar" />
</button>
</span>
</h1>
<div v-if="LogViewer.back_to_system_url">
<a :href="LogViewer.back_to_system_url"
class="rounded shrink inline-flex items-center text-sm text-gray-500 dark:text-gray-400 hover:text-brand-800 dark:hover:text-brand-600 focus:outline-none focus:ring-2 focus:ring-brand-500 dark:focus:ring-brand-700 mt-0">
<ArrowLeftIcon class="h-3 w-3 mr-1.5" />
{{ LogViewer.back_to_system_label || `Back to ${LogViewer.app_name}` }}
</a>
</div>
</div>
<div v-if="LogViewer.assets_outdated" class="bg-yellow-100 dark:bg-yellow-900 bg-opacity-75 dark:bg-opacity-40 border border-yellow-300 dark:border-yellow-800 rounded-md px-2 py-1 mt-2 text-xs leading-5 text-yellow-700 dark:text-yellow-400">
<ExclamationTriangleIcon class="h-4 w-4 mr-1 inline" />
Front-end assets are outdated. To update, please run <code class="font-mono px-2 py-1 bg-gray-100 dark:bg-gray-900 rounded">php artisan log-viewer:publish</code>
</div>
<template v-if="hostStore.supportsHosts && hostStore.hasRemoteHosts">
<host-selector class="mb-8 mt-6" />
</template>
<template v-if="fileStore.fileTypesAvailable && fileStore.fileTypesAvailable.length > 1">
<file-type-selector class="mb-8 mt-6" />
</template>
<div class="flex justify-between items-baseline mt-6" v-if="fileStore.filteredFolders?.length > 0">
<div class="ml-1 block text-sm text-gray-500 dark:text-gray-400 truncate">Log files on {{ fileStore.selectedHost?.name }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
<label for="file-sort-direction" class="sr-only">Sort direction</label>
<select id="file-sort-direction" class="select" v-model="fileStore.direction">
<option value="desc">Newest first</option>
<option value="asc">Oldest first</option>
</select>
</div>
</div>
<p v-if="fileStore.error" class="mx-1 mt-1 text-red-600 text-xs">
{{ fileStore.error }}
</p>
</div>
<div v-show="fileStore.checkBoxesVisibility">
<p class="text-sm text-gray-600 dark:text-gray-400">Please select files to delete and confirm or cancel deletion.</p>
<div class="grid grid-flow-col pr-4 mt-2"
:class="[fileStore.hasFilesChecked ? 'justify-between' : 'justify-end']"
>
<button v-show="fileStore.hasFilesChecked"
@click.stop="confirmDeleteSelectedFiles"
class="button inline-flex">
<TrashIcon class="w-5 mr-1" />
Delete selected files
</button>
<button class="button inline-flex" @click.stop="fileStore.resetChecks()">
Cancel
<XMarkIcon class="w-5 ml-1" />
</button>
</div>
</div>
<div id="file-list-container" class="relative h-full overflow-hidden">
<div class="file-list" @scroll="(event) => fileStore.onScroll(event)">
<div v-for="folder in fileStore.filteredFolders"
:key="folder.identifier"
:id="`folder-${folder.identifier}`"
class="relative folder-container"
>
<Menu v-slot="{ open }">
<div class="folder-item-container"
@click="fileStore.toggle(folder)"
:class="[fileStore.isOpen(folder) ? 'active-folder' : '', fileStore.shouldBeSticky(folder) ? 'sticky ' + (open ? 'z-20' : 'z-10') : '' ]"
>
<div class="file-item group">
<button class="file-item-info group" @keydown="handleKeyboardFileNavigation">
<span class="sr-only" v-if="!fileStore.isOpen(folder)">Open folder</span>
<span class="sr-only" v-if="fileStore.isOpen(folder)">Close folder</span>
<span class="file-icon group-hover:hidden group-focus:hidden">
<FolderIcon v-show="!fileStore.isOpen(folder)" class="w-5 h-5" />
<FolderOpenIcon v-show="fileStore.isOpen(folder)" class="w-5 h-5" />
</span>
<span class="file-icon hidden group-hover:inline-block group-focus:inline-block">
<ChevronRightIcon :class="[fileStore.isOpen(folder) ? 'rotate-90' : '', 'transition duration-100']" />
</span>
<span class="file-name">
<span v-if="String(folder.clean_path || '').startsWith('root')">
<span class="text-gray-500 dark:text-gray-400">root</span>{{ String(folder.clean_path).substring(4) }}
</span>
<span v-else>{{ folder.clean_path }}</span>
</span>
</button>
<MenuButton as="button" class="file-dropdown-toggle group-hover:border-brand-600 group-hover:dark:border-brand-800"
:data-toggle-id="folder.identifier"
@keydown="handleKeyboardFileSettingsNavigation"
@click.stop="calculateDropdownDirection($event.target)">
<span class="sr-only">Open folder options</span>
<EllipsisVerticalIcon class="w-4 h-4 pointer-events-none" />
</MenuButton>
</div>
<transition
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100 scale-100"
leave-to-class="opacity-0 scale-90"
enter-active-class="transition ease-out duration-100"
enter-from-class="opacity-0 scale-90"
enter-to-class="opacity-100 scale-100"
>
<MenuItems static v-show="open" as="div" class="dropdown w-48" :class="[dropdownDirections[folder.identifier]]">
<div class="py-2">
<MenuItem @click.stop.prevent="fileStore.clearCacheForFolder(folder)" v-slot="{ active }">
<button :class="[active ? 'active' : '']">
<CircleStackIcon v-show="!fileStore.clearingCache[folder.identifier]" class="w-4 h-4 mr-2"/>
<SpinnerIcon v-show="fileStore.clearingCache[folder.identifier]" class="w-4 h-4 mr-2" />
<span v-show="!fileStore.cacheRecentlyCleared[folder.identifier] && !fileStore.clearingCache[folder.identifier]">Clear indices</span>
<span v-show="!fileStore.cacheRecentlyCleared[folder.identifier] && fileStore.clearingCache[folder.identifier]">Clearing...</span>
<span v-show="fileStore.cacheRecentlyCleared[folder.identifier]" class="text-brand-500">Indices cleared</span>
</button>
</MenuItem>
<MenuItem v-if="folder.can_download" v-slot="{ active }">
<DownloadLink :url="folder.download_url" @click.stop :class="[active ? 'active' : '']" />
</MenuItem>
<template v-if="folder.can_delete">
<div class="divider"></div>
<MenuItem v-slot="{ active }">
<button @click.stop="confirmDeleteFolder(folder)" :disabled="fileStore.deleting[folder.identifier]" :class="[active ? 'active' : '']">
<TrashIcon v-show="!fileStore.deleting[folder.identifier]" class="w-4 h-4 mr-2" />
<SpinnerIcon v-show="fileStore.deleting[folder.identifier]" />
Delete
</button>
</MenuItem>
</template>
</div>
</MenuItems>
</transition>
</div>
</Menu>
<div class="folder-files pl-3 ml-1 border-l border-gray-200 dark:border-gray-800"
v-show="fileStore.isOpen(folder)">
<file-list-item
v-for="logFile in (folder.files || [])"
:key="logFile.identifier"
:log-file="logFile"
@click="selectFile(logFile.identifier)"
/>
</div>
</div>
<div v-if="fileStore.folders.length === 0" class="text-center text-sm text-gray-600 dark:text-gray-400">
<p class="mb-5">No log files were found.</p>
<div class="flex items-center justify-center px-1">
<button @click.prevent="fileStore.loadFolders()"
class="inline-flex items-center px-4 py-2 text-left text-sm bg-white hover:bg-gray-50 outline-brand-500 dark:outline-brand-800 text-gray-900 dark:text-gray-200 rounded-md dark:bg-gray-700 dark:hover:bg-gray-600"
>
<ArrowPathIcon class="w-4 h-4 mr-1.5" />
Refresh file list
</button>
</div>
</div>
</div>
<!-- gradient to hide the bottom of the file list -->
<div class="pointer-events-none absolute z-10 bottom-0 h-4 w-full bg-gradient-to-t from-gray-100 dark:from-gray-900 to-transparent"></div>
<!-- loading state overlay -->
<div class="absolute inset-y-0 left-3 right-7 lg:left-0 lg:right-0 z-10" v-show="fileStore.loading">
<div class="rounded-md bg-white text-gray-800 dark:bg-gray-700 dark:text-gray-200 opacity-90 w-full h-full flex items-center justify-center">
<SpinnerIcon class="w-14 h-14" />
</div>
</div>
</div>
</nav>
</template>
<script setup>
import { onMounted, watch } from 'vue';
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue';
import {
ArrowLeftIcon,
ArrowPathIcon,
CircleStackIcon,
EllipsisVerticalIcon,
ExclamationTriangleIcon,
FolderIcon,
FolderOpenIcon,
TrashIcon,
XMarkIcon,
ChevronRightIcon,
} from '@heroicons/vue/24/outline';
import { useHostStore } from '../stores/hosts.js';
import { useFileStore } from '../stores/files.js';
import { useRoute, useRouter } from 'vue-router';
import { replaceQuery, useDropdownDirection } from '../helpers.js';
import FileListItem from './FileListItem.vue';
import SpinnerIcon from './SpinnerIcon.vue';
import SiteSettingsDropdown from './SiteSettingsDropdown.vue';
import HostSelector from './HostSelector.vue';
import { handleKeyboardFileNavigation, handleKeyboardFileSettingsNavigation } from '../keyboardNavigation';
import FileTypeSelector from './FileTypeSelector.vue';
import DownloadLink from "./DownloadLink.vue";
const router = useRouter();
const route = useRoute();
const hostStore = useHostStore();
const fileStore = useFileStore();
const { dropdownDirections, calculateDropdownDirection } = useDropdownDirection();
const confirmDeleteFolder = async (folder) => {
if (confirm(`Are you sure you want to delete the log folder '${folder.path}'? THIS ACTION CANNOT BE UNDONE.`)) {
await fileStore.deleteFolder(folder);
if (folder.files.some(file => file.identifier === fileStore.selectedFileIdentifier)) {
replaceQuery(router, 'file', null);
}
}
}
const confirmDeleteSelectedFiles = async () => {
if (confirm('Are you sure you want to delete selected log files? THIS ACTION CANNOT BE UNDONE.')) {
await fileStore.deleteSelectedFiles();
if (fileStore.filesChecked.includes(fileStore.selectedFileIdentifier)) {
replaceQuery(router, 'file', null);
}
fileStore.resetChecks();
await fileStore.loadFolders();
}
}
const selectFile = (fileIdentifier) => {
if (route.query.file && route.query.file === fileIdentifier) {
replaceQuery(router, 'file', null);
} else {
replaceQuery(router, 'file', fileIdentifier);
}
};
onMounted(async () => {
hostStore.selectHost(route.query.host || null);
});
watch(
() => fileStore.direction,
() => fileStore.loadFolders()
);
</script>