pixelfed/pixelfed

View on GitHub
resources/assets/components/admin/AdminInstances.vue

Summary

Maintainability
Test Coverage
<template>
<div>
    <div class="header bg-primary pb-3 mt-n4">
        <div class="container-fluid">
            <div class="header-body">
                <div class="row align-items-center py-4">
                    <div class="col-lg-6 col-7">
                        <p class="display-1 text-white d-inline-block mb-0">Instances</p>
                    </div>
                </div>
                <div class="row">
                    <div class="col-xl-2 col-md-6">
                        <div class="mb-3">
                            <h5 class="text-light text-uppercase mb-0">Total Instances</h5>
                            <span class="text-white h2 font-weight-bold mb-0 human-size">{{ prettyCount(stats.total_count) }}</span>
                        </div>
                    </div>

                    <div class="col-xl-2 col-md-6">
                        <div class="mb-3">
                            <h5 class="text-light text-uppercase mb-0">New (past 14 days)</h5>
                            <span class="text-white h2 font-weight-bold mb-0 human-size">{{ prettyCount(stats.new_count) }}</span>
                        </div>
                    </div>

                    <div class="col-xl-2 col-md-6">
                        <div class="mb-3">
                            <h5 class="text-light text-uppercase mb-0">Banned Instances</h5>
                            <span class="text-white h2 font-weight-bold mb-0 human-size">{{ prettyCount(stats.banned_count) }}</span>
                        </div>
                    </div>

                    <div class="col-xl-2 col-md-6">
                        <div class="mb-3">
                            <h5 class="text-light text-uppercase mb-0">NSFW Instances</h5>
                            <span class="text-white h2 font-weight-bold mb-0 human-size">{{ prettyCount(stats.nsfw_count) }}</span>
                        </div>
                    </div>
                    <div class="col-xl-2 col-md-6">
                        <div class="mb-3">
                            <button class="btn btn-outline-white btn-block btn-sm mt-1" @click.prevent="showAddModal = true">Create New Instance</button>
                            <div v-if="showImportForm">
                                <div class="form-group mt-3">
                                    <div class="custom-file">
                                        <input ref="importInput" type="file" class="custom-file-input" id="customFile" v-on:change="onImportUpload">
                                        <label class="custom-file-label" for="customFile">Choose file</label>
                                    </div>
                                </div>
                                <p class="mb-0 mt-n3">
                                    <a href="#" class="text-white font-weight-bold small" @click.prevent="showImportForm = false">Cancel</a>
                                </p>
                            </div>
                            <div v-else class="d-flex mt-1">
                                <button class="btn btn-outline-white btn-sm mt-1" @click="openImportForm">Import</button>
                                <button class="btn btn-outline-white btn-block btn-sm mt-1" @click="downloadBackup()">Download Backup</button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div v-if="!loaded" class="my-5 text-center">
        <b-spinner />
    </div>

    <div v-else class="m-n2 m-lg-4">
        <div class="container-fluid mt-4">
            <div class="row mb-3 justify-content-between">
                <div class="col-12 col-md-8">
                    <ul class="nav nav-pills">
                        <li class="nav-item">
                            <button :class="['nav-link', { active: tabIndex == 0}]" @click="toggleTab(0)">All</button>
                        </li>
                        <li class="nav-item">
                            <button :class="['nav-link', { active: tabIndex == 1}]" @click="toggleTab(1)">New</button>
                        </li>
                        <li class="nav-item">
                            <button :class="['nav-link', { active: tabIndex == 2}]" @click="toggleTab(2)">Banned</button>
                        </li>
                        <li class="nav-item">
                            <button :class="['nav-link', { active: tabIndex == 3}]" @click="toggleTab(3)">NSFW</button>
                        </li>
                        <li class="nav-item">
                            <button :class="['nav-link', { active: tabIndex == 4}]" @click="toggleTab(4)">Unlisted</button>
                        </li>
                        <li class="nav-item">
                            <button :class="['nav-link', { active: tabIndex == 5}]" @click="toggleTab(5)">Most Users</button>
                        </li>
                        <li class="nav-item">
                            <button :class="['nav-link', { active: tabIndex == 6}]" @click="toggleTab(6)">Most Statuses</button>
                        </li>
                    </ul>
                </div>
                <div class="col-12 col-md-4">
                    <autocomplete
                        :search="composeSearch"
                        :disabled="searchLoading"
                        :defaultValue="searchQuery"
                        placeholder="Search instances by domain"
                        aria-label="Search instances by domain"
                        :get-result-value="getTagResultValue"
                        @submit="onSearchResultClick"
                        ref="autocomplete"
                        >
                            <template #result="{ result, props }">
                                <li
                                v-bind="props"
                                class="autocomplete-result d-flex justify-content-between align-items-center"
                                >
                                <div class="font-weight-bold" :class="{ 'text-danger': result.banned }">
                                    {{ result.domain }}
                                </div>
                                <div class="small text-muted">
                                    {{ prettyCount(result.user_count) }} users
                                </div>
                            </li>
                        </template>
                    </autocomplete>
                </div>
            </div>

            <div class="table-responsive">
                <table class="table table-dark">
                    <thead class="thead-dark">
                        <tr>
                            <th scope="col" class="cursor-pointer" v-html="buildColumn('ID', 'id')" @click="toggleCol('id')"></th>
                            <th scope="col" class="cursor-pointer" v-html="buildColumn('Domain', 'domain')" @click="toggleCol('domain')"></th>
                            <th scope="col" class="cursor-pointer" v-html="buildColumn('Software', 'software')" @click="toggleCol('software')"></th>
                            <th scope="col" class="cursor-pointer" v-html="buildColumn('User Count', 'user_count')" @click="toggleCol('user_count')"></th>
                            <th scope="col" class="cursor-pointer" v-html="buildColumn('Status Count', 'status_count')" @click="toggleCol('status_count')"></th>
                            <th scope="col" class="cursor-pointer" v-html="buildColumn('Banned', 'banned')" @click="toggleCol('banned')"></th>
                            <th scope="col" class="cursor-pointer" v-html="buildColumn('NSFW', 'auto_cw')" @click="toggleCol('auto_cw')"></th>
                            <th scope="col" class="cursor-pointer" v-html="buildColumn('Unlisted', 'unlisted')" @click="toggleCol('unlisted')"></th>
                            <th scope="col">Created</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr v-for="(instance, idx) in instances">
                            <td class="font-weight-bold text-monospace text-muted">
                                <a href="#" @click.prevent="openInstanceModal(instance.id)">
                                    {{ instance.id }}
                                </a>
                            </td>
                            <td class="font-weight-bold">{{ instance.domain }}</td>
                            <td class="font-weight-bold">{{ instance.software }}</td>
                            <td class="font-weight-bold">{{ prettyCount(instance.user_count) }}</td>
                            <td class="font-weight-bold">{{ prettyCount(instance.status_count) }}</td>
                            <td class="font-weight-bold" v-html="boolIcon(instance.banned, 'text-danger')"></td>
                            <td class="font-weight-bold" v-html="boolIcon(instance.auto_cw, 'text-danger')"></td>
                            <td class="font-weight-bold" v-html="boolIcon(instance.unlisted, 'text-danger')"></td>
                            <td class="font-weight-bold">{{ timeAgo(instance.created_at) }}</td>
                        </tr>
                    </tbody>
                </table>
            </div>

            <div class="d-flex align-items-center justify-content-center">
                <button
                    class="btn btn-primary rounded-pill"
                    :disabled="!pagination.prev"
                    @click="paginate('prev')">
                    Prev
                </button>
                <button
                    class="btn btn-primary rounded-pill"
                    :disabled="!pagination.next"
                    @click="paginate('next')">
                    Next
                </button>
            </div>
        </div>
    </div>

    <b-modal
        v-model="showInstanceModal"
        title="View Instance"
        header-class="d-flex align-items-center justify-content-center mb-0 pb-0"
        ok-title="Save"
        :ok-disabled="!editingInstanceChanges"
        @ok="saveInstanceModalChanges">
        <div v-if="editingInstance && canEditInstance" class="list-group">
            <div class="list-group-item d-flex align-items-center justify-content-between">
                <div class="text-muted small">Domain</div>
                <div class="font-weight-bold">{{ editingInstance.domain }}</div>
            </div>
            <div class="list-group-item d-flex align-items-center justify-content-between">
                <div v-if="editingInstance.software">
                    <div class="text-muted small">Software</div>
                    <div class="font-weight-bold">{{ editingInstance.software ?? 'Unknown' }}</div>
                </div>
                <div>
                    <div class="text-muted small">Total Users</div>
                    <div class="font-weight-bold">{{ formatCount(editingInstance.user_count ?? 0) }}</div>
                </div>
                <div>
                    <div class="text-muted small">Total Statuses</div>
                    <div class="font-weight-bold">{{ formatCount(editingInstance.status_count ?? 0) }}</div>
                </div>
            </div>
            <div class="list-group-item d-flex align-items-center justify-content-between">
                <div class="text-muted small">Banned</div>
                <div class="mr-n2 mb-1">
                    <b-form-checkbox v-model="editingInstance.banned" switch size="lg"></b-form-checkbox>
                </div>
            </div>
            <div class="list-group-item d-flex align-items-center justify-content-between">
                <div class="text-muted small">Apply CW to Media</div>
                <div class="mr-n2 mb-1">
                    <b-form-checkbox v-model="editingInstance.auto_cw" switch size="lg"></b-form-checkbox>
                </div>
            </div>
            <div class="list-group-item d-flex align-items-center justify-content-between">
                <div class="text-muted small">Unlisted</div>
                <div class="mr-n2 mb-1">
                    <b-form-checkbox v-model="editingInstance.unlisted" switch size="lg"></b-form-checkbox>
                </div>
            </div>
            <div class="list-group-item d-flex justify-content-between" :class="[ instanceModalNotes ? 'flex-column gap-2' : 'align-items-center']">
                <div class="text-muted small">Notes</div>
                <transition name="fade">
                    <div v-if="instanceModalNotes" class="w-100">
                        <b-form-textarea v-model="editingInstance.notes" rows="3" max-rows="5" maxlength="500"></b-form-textarea>
                        <p class="small text-muted">{{editingInstance.notes ? editingInstance.notes.length : 0}}/500</p>
                    </div>
                    <div v-else class="mb-1">
                        <a href="#" class="font-weight-bold small" @click.prevent="showModalNotes()">{{editingInstance.notes ? 'View' : 'Add'}}</a>
                    </div>
                </transition>
            </div>
        </div>
        <template #modal-footer>
        <div class="w-100 d-flex justify-content-between align-items-center">
            <div>
                <b-button
                    variant="outline-danger"
                    size="sm"
                    @click="deleteInstanceModal"
                >
                    Delete
                </b-button>
                <b-button
                    v-if="!refreshedModalStats"
                    variant="outline-primary"
                    size="sm"
                    @click="refreshModalStats"
                >
                    Refresh Stats
                </b-button>
            </div>
          <div>
              <b-button
                variant="link-dark"
                size="sm"
                @click="onViewMoreInstance"
              >
                View More
              </b-button>
              <b-button
                variant="primary"
                @click="saveInstanceModalChanges"
              >
                Save
              </b-button>
          </div>
        </div>
      </template>
    </b-modal>

    <b-modal
        v-model="showAddModal"
        title="Add Instance"
        ok-title="Save"
        :ok-disabled="addNewInstance.domain.length < 2"
        @ok="saveNewInstance">
        <div class="list-group">
            <div class="list-group-item d-flex align-items-center justify-content-between">
                <div class="text-muted small">Domain</div>
                <div>
                    <b-form-input v-model="addNewInstance.domain" placeholder="Add domain here" />
                    <p class="small text-light mb-0">Enter a valid domain without https://</p>
                </div>
            </div>

            <div class="list-group-item d-flex align-items-center justify-content-between">
                <div class="text-muted small">Banned</div>
                <div class="mr-n2 mb-1">
                    <b-form-checkbox v-model="addNewInstance.banned" switch size="lg"></b-form-checkbox>
                </div>
            </div>
            <div class="list-group-item d-flex align-items-center justify-content-between">
                <div class="text-muted small">Apply CW to Media</div>
                <div class="mr-n2 mb-1">
                    <b-form-checkbox v-model="addNewInstance.auto_cw" switch size="lg"></b-form-checkbox>
                </div>
            </div>
            <div class="list-group-item d-flex align-items-center justify-content-between">
                <div class="text-muted small">Unlisted</div>
                <div class="mr-n2 mb-1">
                    <b-form-checkbox v-model="addNewInstance.unlisted" switch size="lg"></b-form-checkbox>
                </div>
            </div>
            <div class="list-group-item d-flex flex-column gap-2 justify-content-between">
                <div class="text-muted small">Notes</div>
                <div class="w-100">
                    <b-form-textarea v-model="addNewInstance.notes" rows="3" max-rows="5" maxlength="500" placeholder="Add optional notes here"></b-form-textarea>
                    <p class="small text-muted">{{addNewInstance.notes ? addNewInstance.notes.length : 0}}/500</p>
                </div>
            </div>
        </div>
    </b-modal>

    <b-modal
        v-model="showImportModal"
        title="Import Instance Backup"
        ok-title="Import"
        scrollable
        :ok-disabled="!importData || (!importData.banned.length && !importData.unlisted.length && !importData.auto_cw.length)"
        @ok="completeImport"
        @cancel="cancelImport">
        <div v-if="showImportModal && importData">
            <div v-if="importData.auto_cw && importData.auto_cw.length" class="mb-5">
                <p class="font-weight-bold text-center my-0">NSFW Instances ({{importData.auto_cw.length}})</p>
                <p class="small text-center text-muted mb-1">Tap on an instance to remove it.</p>
                <div class="list-group">
                    <a v-for="(instance, idx) in importData.auto_cw" class="list-group-item d-flex align-items-center justify-content-between" href="#" @click.prevent="filterImportData('auto_cw', idx)">
                        {{ instance }}

                        <span class="badge badge-warning">Auto CW</span>
                    </a>
                </div>
            </div>

            <div v-if="importData.unlisted && importData.unlisted.length" class="mb-5">
                <p class="font-weight-bold text-center my-0">Unlisted Instances ({{importData.unlisted.length}})</p>
                <p class="small text-center text-muted mb-1">Tap on an instance to remove it.</p>
                <div class="list-group">
                    <a v-for="(instance, idx) in importData.unlisted" class="list-group-item d-flex align-items-center justify-content-between" href="#" @click.prevent="filterImportData('unlisted', idx)">
                        {{ instance }}

                        <span class="badge badge-primary">Unlisted</span>
                    </a>
                </div>
            </div>

            <div v-if="importData.banned && importData.banned.length" class="mb-5">
                <p class="font-weight-bold text-center my-0">Banned Instances ({{importData.banned.length}})</p>
                <p class="small text-center text-muted mb-1">Review instances, tap on an instance to remove it.</p>
                <div class="list-group">
                    <a v-for="(instance, idx) in importData.banned" class="list-group-item d-flex align-items-center justify-content-between" href="#" @click.prevent="filterImportData('banned', idx)">
                        {{ instance }}

                        <span class="badge badge-danger">Banned</span>
                    </a>
                </div>
            </div>

            <div v-if="!importData.banned.length && !importData.unlisted.length && !importData.auto_cw.length">
                <div class="text-center">
                    <p>
                        <i class="far fa-check-circle fa-4x text-success"></i>
                    </p>
                    <p class="lead">Nothing to import!</p>
                </div>
            </div>
        </div>
    </b-modal>
</div>
</template>

<script type="text/javascript">
    import Autocomplete from '@trevoreyre/autocomplete-vue'
    import '@trevoreyre/autocomplete-vue/dist/style.css'

    export default {
        components: {
            Autocomplete,
        },

        data() {
            return {
                loaded: false,
                tabIndex: 0,
                stats: {
                    total_count: 0,
                    new_count: 0,
                    banned_count: 0,
                    nsfw_count: 0
                },
                instances: [],
                pagination: [],
                sortCol: undefined,
                sortDir: undefined,
                searchQuery: undefined,
                filterMap: [
                    'all',
                    'new',
                    'banned',
                    'cw',
                    'unlisted',
                    'popular_users',
                    'popular_statuses'
                ],
                searchLoading: false,
                showInstanceModal: false,
                instanceModal: {},
                editingInstanceChanges: false,
                canEditInstance: false,
                editingInstance: {},
                editingInstanceIndex: 0,
                instanceModalNotes: false,
                showAddModal: false,
                refreshedModalStats: false,
                addNewInstance: {
                    domain: "",
                    banned: false,
                    auto_cw: false,
                    unlisted: false,
                    notes: undefined
                },
                showImportForm: false,
                showImportModal: false,
                importData: undefined,
            }
        },

        mounted() {
            this.fetchStats();

            let u = new URLSearchParams(window.location.search);
            if(u.has('filter') && !u.has('q') && !u.has('sort')) {
                const url = new URL(window.location.origin + '/i/admin/api/instances/get');

                if(u.has('filter')) {
                    this.tabIndex = this.filterMap.indexOf(u.get('filter'));
                    url.searchParams.set('filter', u.get('filter'));
                }
                if(u.has('cursor')) {
                    url.searchParams.set('cursor', u.get('cursor'));
                }

                this.fetchInstances(url.toString());
            } else if(u.has('sort') && !u.has('q')) {
                const url = new URL(window.location.origin + '/i/admin/api/instances/get');
                url.searchParams.set('sort', u.get('sort'));

                if(u.has('dir')) {
                    url.searchParams.set('dir', u.get('dir'));
                }

                if(u.has('filter')) {
                    url.searchParams.set('filter', u.get('filter'));
                }

                if(u.has('cursor')) {
                    url.searchParams.set('cursor', u.get('cursor'));
                }

                this.fetchInstances(url.toString());
            } else if(u.has('q')) {
                this.tabIndex = -1;
                this.searchQuery = u.get('q');

                const url = new URL(window.location.origin + '/i/admin/api/instances/query');
                url.searchParams.set('q', u.get('q'));

                if(u.has('cursor')) {
                    url.searchParams.set('cursor', u.get('cursor'));
                }

                this.fetchInstances(url.toString());
            } else {
                this.fetchInstances();
            }
        },

        watch: {
            editingInstance: {
                deep: true,
                immediate: true,
                handler: function(updated, old) {
                    if(!this.canEditInstance) {
                        return;
                    }

                    if(
                        JSON.stringify(old) === JSON.stringify(this.instances.filter(i => i.id === updated.id)[0]) &&
                        JSON.stringify(updated) === JSON.stringify(this.instanceModal)
                    ) {
                        this.editingInstanceChanges = true;
                    } else {
                        this.editingInstanceChanges = false;
                    }
                }
            }
        },

        methods: {
            fetchStats() {
                axios.get('/i/admin/api/instances/stats')
                .then(res => {
                    this.stats = res.data;
                })
            },

            fetchInstances(url = '/i/admin/api/instances/get') {
                axios.get(url)
                .then(res => {
                    this.instances = res.data.data;
                    this.pagination = {...res.data.links, ...res.data.meta};
                })
                .then(() => {
                    this.$nextTick(() => {
                        this.loaded = true;
                    })
                })
            },

            toggleTab(idx) {
                this.loaded = false;
                this.tabIndex = idx;
                this.searchQuery = undefined;
                let url = '/i/admin/api/instances/get?filter=' + this.filterMap[idx];
                history.pushState(null, '', '/i/admin/instances?filter=' + this.filterMap[idx]);
                this.fetchInstances(url);
            },

            prettyCount(str) {
                if(str) {
                   return str.toLocaleString('en-CA', { compactDisplay: "short", notation: "compact"});
                } else {
                    return 0;
                }
                return str;
            },

            formatCount(str) {
                if(str) {
                   return str.toLocaleString('en-CA');
                } else {
                    return 0;
                }
                return str;
            },

            timeAgo(str) {
                if(!str) {
                    return str;
                }
                return App.util.format.timeAgo(str);
            },

            boolIcon(val, success = 'text-success', danger = 'text-muted') {
                if(val) {
                    return `<i class="far fa-check-circle fa-lg ${success}"></i>`;
                }

                return `<i class="far fa-times-circle fa-lg ${danger}"></i>`;
            },

            toggleCol(col) {
                if(this.filterMap[this.tabIndex] == col || this.searchQuery) {
                    return;
                }
                this.sortCol = col;

                if(!this.sortDir) {
                    this.sortDir = 'desc';
                } else {
                    this.sortDir = this.sortDir == 'asc' ? 'desc' : 'asc';
                }

                const url = new URL(window.location.origin + '/i/admin/instances');
                url.searchParams.set('sort', col);
                url.searchParams.set('dir', this.sortDir);
                if(this.tabIndex != 0) {
                    url.searchParams.set('filter', this.filterMap[this.tabIndex]);
                }
                history.pushState(null, '', url);

                const apiUrl = new URL(window.location.origin + '/i/admin/api/instances/get');
                apiUrl.searchParams.set('sort', col);
                apiUrl.searchParams.set('dir', this.sortDir);
                if(this.tabIndex != 0) {
                    apiUrl.searchParams.set('filter', this.filterMap[this.tabIndex]);
                }

                this.fetchInstances(apiUrl.toString());
            },

            buildColumn(name, col) {
                if([1, 5, 6].indexOf(this.tabIndex) != -1 || (this.searchQuery && this.searchQuery.length)) {
                    return name;
                }

                if(this.tabIndex === 2 && col === 'banned') {
                    return name;
                }

                if(this.tabIndex === 3 && col === 'auto_cw') {
                    return name;
                }

                if(this.tabIndex === 4 && col === 'unlisted') {
                    return name;
                }

                let icon = `<i class="far fa-sort"></i>`;
                if(col == this.sortCol) {
                    icon = this.sortDir == 'desc' ?
                    `<i class="far fa-sort-up"></i>` :
                    `<i class="far fa-sort-down"></i>`
                }
                return `${name} ${icon}`;
            },

            paginate(dir) {
                event.currentTarget.blur();
                let apiUrl = dir == 'next' ? this.pagination.next : this.pagination.prev;
                let cursor = dir == 'next' ? this.pagination.next_cursor : this.pagination.prev_cursor;

                const url = new URL(window.location.origin + '/i/admin/instances');

                if(cursor) {
                    url.searchParams.set('cursor', cursor);
                }

                if(this.searchQuery) {
                    url.searchParams.set('q', this.searchQuery);
                }

                if(this.sortCol) {
                    url.searchParams.set('sort', this.sortCol);
                }

                if(this.sortDir) {
                    url.searchParams.set('dir', this.sortDir);
                }

                history.pushState(null, '', url.toString());
                this.fetchInstances(apiUrl);
            },

            composeSearch(input) {
                if (input.length < 1) { return []; };
                this.searchQuery = input;
                history.pushState(null, '', '/i/admin/instances?q=' + input);
                return axios.get('/i/admin/api/instances/query', {
                    params: {
                        q: input,
                    }
                }).then(res => {
                    if(!res || !res.data) {
                        this.fetchInstances();
                    } else {
                        this.tabIndex = -1;
                        this.instances = res.data.data;
                        this.pagination = {...res.data.links, ...res.data.meta};
                    }
                    return res.data.data;
                });
            },

            getTagResultValue(result) {
                return result.name;
            },

            onSearchResultClick(result) {
                this.openInstanceModal(result.id);
                return;
            },

            openInstanceModal(id) {
                const cached = this.instances.filter(i => i.id === id)[0];
                this.refreshedModalStats = false;
                this.editingInstanceChanges = false;
                this.instanceModalNotes = false;
                this.canEditInstance = false;
                this.instanceModal = cached;
                this.$nextTick(() => {
                    this.editingInstance = cached;
                    this.showInstanceModal = true;
                    this.canEditInstance = true;
                })
            },

            showModalNotes() {
                this.instanceModalNotes = true;
            },

            saveInstanceModalChanges() {
                axios.post('/i/admin/api/instances/update', this.editingInstance)
                .then(res => {
                    this.showInstanceModal = false;
                    this.$bvToast.toast(`Successfully updated ${res.data.data.domain}`, {
                        title: 'Instance Updated',
                        autoHideDelay: 5000,
                        appendToast: true,
                        variant: 'success'
                    })
                })
            },

            saveNewInstance() {
                axios.post('/i/admin/api/instances/create', this.addNewInstance)
                .then(res => {
                    this.showInstanceModal = false;
                    this.instances.unshift(res.data.data);
                })
                .catch(err => {
                    swal('Oops!', 'An error occured, please try again later.', 'error');
                    this.addNewInstance = {
                        domain: "",
                        banned: false,
                        auto_cw: false,
                        unlisted: false,
                        notes: undefined
                    }
                })
            },

            refreshModalStats() {
                axios.post('/i/admin/api/instances/refresh-stats', {
                    id: this.instanceModal.id
                })
                .then(res => {
                    this.refreshedModalStats = true;
                    this.instanceModal = res.data.data;
                    this.editingInstance = res.data.data;
                    this.instances = this.instances.map(i => {
                        if(i.id === res.data.data.id) {
                            return res.data.data;
                        }
                        return i;
                    })
                })
            },

            deleteInstanceModal() {
                if(!window.confirm('Are you sure you want to delete this instance? This will not delete posts or profiles from this instance.')) {
                    return;
                }
                axios.post('/i/admin/api/instances/delete', {
                    id: this.instanceModal.id
                })
                .then(res => {
                    this.showInstanceModal = false;
                    this.instances = this.instances.filter(i => i.id != this.instanceModal.id);
                })
                .then(() => {
                    setTimeout(() => this.fetchStats(), 1000);
                })
            },

            openImportForm() {
                let el = document.createElement('p');
                    el.classList.add('text-left');
                    el.classList.add('mb-0');
                    el.innerHTML = '<p class="lead mb-0">Import your instance moderation backup.</span></p><br /><p>Import Instructions:</p><ol><li>Press OK</li><li>Press "Choose File" on Import form input</li><li>Select your <kbd>pixelfed-instances-mod.json</kbd> file</li><li>Review instance moderation actions. Tap on an instance to remove it</li><li>Press "Import" button to finish importing</li></ol>';
                    let wrapper = document.createElement('div');
                    wrapper.appendChild(el);
                swal({
                    title: 'Import Backup',
                    content: wrapper,
                    icon: 'info'
                })
                this.showImportForm = true;
            },

            downloadBackup($event) {
                axios.get('/i/admin/api/instances/download-backup', {
                    responseType: "blob"
                })
                .then(res => {
                    let el = document.createElement('a');
                    el.setAttribute('download', 'pixelfed-instances-mod.json')
                    const href = URL.createObjectURL(res.data);
                      el.href = href;
                      el.setAttribute('target', '_blank');
                      el.click();

                      swal(
                          'Instance Backup Downloading',
                          'Your instance moderation backup is downloading. Use this to import auto_cw, banned and unlisted instances to supported Pixelfed instances.',
                          'success'
                      )
                })
            },

            async onImportUpload(ev) {
                let res = await this.getParsedImport(ev.target.files[0]);

                if(!res.hasOwnProperty('version') || res.version !== 1) {
                    swal('Invalid Backup', 'We cannot validate this backup. Please try again later.', 'error');
                    this.showImportForm = false;
                    this.$refs.importInput.reset();
                    return;
                }
                this.importData = res;
                this.showImportModal = true;
            },

            async getParsedImport(ev) {
                try {
                    return await this.parseJsonFile(ev);
                } catch(err) {
                    let el = document.createElement('p');
                    el.classList.add('text-left');
                    el.classList.add('mb-0');
                    el.innerHTML = '<p class="lead">An error occured when attempting to parse the import file. <span class="font-weight-bold">Please try again later.</span></p><br /><p class="small text-danger mb-0">Error message:</p><div class="card card-body"><code>' + err.message + '</code></div>';
                    let wrapper = document.createElement('div');
                    wrapper.appendChild(el);
                    swal({
                        title: 'Import Error',
                        content: wrapper,
                        icon: 'error'
                    })
                    return;
                }
            },

            async promisedParseJSON(json) {
                return new Promise((resolve, reject) => {
                    try {
                        resolve(JSON.parse(json))
                    } catch (e) {
                        reject(e)
                    }
                })
            },

            async parseJsonFile(file) {
                return new Promise((resolve, reject) => {
                    const fileReader = new FileReader()
                    fileReader.onload = event => resolve(this.promisedParseJSON(event.target.result))
                    fileReader.onerror = error => reject(error)
                    fileReader.readAsText(file)
                })
            },

            filterImportData(type, index) {
                switch(type) {
                    case 'auto_cw':
                        this.importData.auto_cw.splice(index, 1);
                    break;

                    case 'unlisted':
                        this.importData.unlisted.splice(index, 1);
                    break;

                    case 'banned':
                        this.importData.banned.splice(index, 1);
                    break;
                }
            },

            completeImport() {
                this.showImportForm = false;

                axios.post('/i/admin/api/instances/import-data', {
                    'banned': this.importData.banned,
                    'auto_cw': this.importData.auto_cw,
                    'unlisted': this.importData.unlisted,
                })
                .then(res => {
                    swal('Import Uploaded', 'Import successfully uploaded, please allow a few minutes to process.', 'success');
                })
                .then(() => {
                    setTimeout(() => this.fetchStats(), 1000);
                })
            },

            cancelImport(bvModalEvent) {
                if(this.importData.banned.length || this.importData.auto_cw.length || this.importData.unlisted.length) {
                    if(!window.confirm('Are you sure you want to cancel importing?')) {
                        bvModalEvent.preventDefault();
                        return;
                    } else {
                        this.showImportForm = false;
                        this.$refs.importInput.value = '';
                        this.importData = {
                            banned: [],
                            auto_cw: [],
                            unlisted: []
                        };
                    }
                }
            },

            onViewMoreInstance() {
                this.showInstanceModal = false;
                window.location.href = '/i/admin/instances/show/' + this.instanceModal.id
            }
        }
    }
</script>

<style lang="scss" scoped>
    .gap-2 {
        gap: 1rem;
    }
</style>