all tsx used, no vue SFC
This commit is contained in:
3
web/src/views/DomainsView.css
Normal file
3
web/src/views/DomainsView.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.n-card {
|
||||
width: 32vw;
|
||||
}
|
87
web/src/views/DomainsView.tsx
Normal file
87
web/src/views/DomainsView.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import './DomainsView.css'
|
||||
|
||||
import { NSpin, NFlex, NCard, NButton, NIcon, NModalProvider, createDiscreteApi } from 'naive-ui'
|
||||
import { PlusSquare } from "@vicons/fa"
|
||||
import { type Domain, useDomainStore } from '@/stores/domains'
|
||||
import { getErrorInfo } from '@/apis/api'
|
||||
import DomainInfo from '@/components/domains/DomainInfo'
|
||||
import DomainOps from '@/components/domains/DomainOps'
|
||||
import DomainRemoveModal from '@/components/domains/DomainRemoveModal'
|
||||
import DomainEditModal from '@/components/domains/DomainEditModal'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const domainStore = useDomainStore()
|
||||
const { notification } = createDiscreteApi(['notification'])
|
||||
|
||||
const loading = ref(true);
|
||||
const removeModalShow = ref(false);
|
||||
const editModalShow = ref(false);
|
||||
const operationDomain = ref({} as Domain)
|
||||
|
||||
function showRemoveModal(domain: Domain) {
|
||||
operationDomain.value = domain
|
||||
removeModalShow.value = true
|
||||
}
|
||||
|
||||
function showEditModal(domain: Domain) {
|
||||
operationDomain.value = domain
|
||||
editModalShow.value = true
|
||||
}
|
||||
|
||||
function addDomain() {
|
||||
const domain = {
|
||||
refresh_interval: 86400,
|
||||
retry_interval: 7200,
|
||||
expiry_period: 3600000,
|
||||
negative_ttl: 86400,
|
||||
serial_number: 1,
|
||||
} as Domain
|
||||
showEditModal(domain)
|
||||
}
|
||||
|
||||
function DomainsView() {
|
||||
try {
|
||||
domainStore.loadDomains()
|
||||
loading.value = false
|
||||
} catch (e) {
|
||||
const msg = getErrorInfo(e)
|
||||
notification.error(msg)
|
||||
console.error(e)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{
|
||||
loading.value ? <NSpin size="large" /> :
|
||||
<NModalProvider>
|
||||
<NFlex vertical>
|
||||
{
|
||||
domainStore.domains.map((domain: Domain) => (
|
||||
<NCard title={domain.domain_name} key={domain.id} size='large' hoverable>
|
||||
{{
|
||||
default: () => <DomainInfo domain={domain} />,
|
||||
action: () => <DomainOps domain={domain} onRemoveDomain={showRemoveModal} onEditDomain={showEditModal} />
|
||||
}}
|
||||
|
||||
</NCard>
|
||||
))
|
||||
}
|
||||
|
||||
<NCard hoverable>
|
||||
<NButton block quaternary size="large" onClick={addDomain}>
|
||||
{{
|
||||
icon: () => <NIcon component={PlusSquare} depth={5} />
|
||||
}}
|
||||
</NButton>
|
||||
</NCard>
|
||||
</NFlex>
|
||||
<DomainRemoveModal show={removeModalShow.value} domain={operationDomain.value} onUpdate:show={(v: boolean) => removeModalShow.value = v} />
|
||||
<DomainEditModal show={editModalShow.value} domain={operationDomain.value} onUpdate:show={(v: boolean) => editModalShow.value = v} />
|
||||
</NModalProvider>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
DomainsView.displayName = 'DomainsView'
|
||||
|
||||
export default DomainsView
|
@@ -1,83 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { NSpin, NFlex, NCard, NButton, NIcon, useNotification, NModalProvider } from 'naive-ui'
|
||||
import { PlusSquare } from "@vicons/fa"
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { type Domain, useDomainStore } from '@/stores/domains'
|
||||
import { getErrorInfo } from '@/apis/api'
|
||||
import DomainInfo from '@/components/domains/DomainInfo'
|
||||
import DomainOps from '@/components/domains/DomainOps'
|
||||
import DomainRemoveModal from '@/components/domains/DomainRemoveModal'
|
||||
import DomainEditModal from '@/components/domains/DomainEditModal'
|
||||
|
||||
const domainStore = useDomainStore()
|
||||
const notification = useNotification()
|
||||
|
||||
const loading = ref(true);
|
||||
const removeModalShow = ref(false);
|
||||
const editModalShow = ref(false);
|
||||
const operationDomain = ref({} as Domain)
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
domainStore.loadDomains()
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
const msg = getErrorInfo(error)
|
||||
notification.error(msg)
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
|
||||
function showRemoveModal(domain: Domain) {
|
||||
operationDomain.value = domain
|
||||
removeModalShow.value = true
|
||||
}
|
||||
|
||||
function showEditModal(domain: Domain) {
|
||||
operationDomain.value = domain
|
||||
editModalShow.value = true
|
||||
}
|
||||
|
||||
function addDomain() {
|
||||
const domain = {
|
||||
refresh_interval: 86400,
|
||||
retry_interval: 7200,
|
||||
expiry_period: 3600000,
|
||||
negative_ttl: 86400,
|
||||
serial_number: 1,
|
||||
} as Domain
|
||||
showEditModal(domain)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<NSpin size="large" v-if="loading" />
|
||||
<NModalProvider v-else>
|
||||
<NFlex id="domains" vertical>
|
||||
<NCard v-for="domain in domainStore.domains" :title="domain.domain_name" v-bind:key="domain.id"
|
||||
size="large" hoverable>
|
||||
<DomainInfo :domain="domain" />
|
||||
<template #action>
|
||||
<DomainOps :domain="domain" @remove-domain="showRemoveModal" @edit-domain="showEditModal" />
|
||||
</template>
|
||||
</NCard>
|
||||
<NCard hoverable>
|
||||
<NButton block quaternary size="large" @click="addDomain">
|
||||
<template #icon>
|
||||
<NIcon :component="PlusSquare" :depth="5" />
|
||||
</template>
|
||||
</NButton>
|
||||
</NCard>
|
||||
</NFlex>
|
||||
<DomainRemoveModal v-model:show="removeModalShow" :domain="operationDomain" />
|
||||
<DomainEditModal v-model:show="editModalShow" :domain="operationDomain" />
|
||||
</NModalProvider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-card {
|
||||
width: 32vw;
|
||||
}
|
||||
</style>
|
@@ -1,6 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import router from '@/router';
|
||||
router.push("/domains")
|
||||
</script>
|
||||
|
||||
<template></template>
|
7
web/src/views/RecordsView.css
Normal file
7
web/src/views/RecordsView.css
Normal file
@@ -0,0 +1,7 @@
|
||||
div#records {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
padding: 1.5rem;
|
||||
}
|
250
web/src/views/RecordsView.tsx
Normal file
250
web/src/views/RecordsView.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import './RecordsView.css'
|
||||
|
||||
import {
|
||||
NSpin, NPageHeader,
|
||||
NFlex, NButton, NIcon, NGrid, NGi,
|
||||
NStatistic, NDataTable, NInput,
|
||||
NModalProvider,
|
||||
createDiscreteApi
|
||||
} from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { useRecordStore, type Record, type SOARecord, RecordTypes, type ARecord } from '@/stores/records'
|
||||
import { getErrorInfo } from '@/apis/api'
|
||||
import { PlusSquare, RedoAlt, CheckCircle, Clock, Cogs, Search } from '@vicons/fa'
|
||||
import router from '@/router';
|
||||
import RecordOps from '@/components/records/RecordOps'
|
||||
import RecordEditModal from '@/components/records/RecordEditModal'
|
||||
import i18n from '@/locale/i18n'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const { t } = i18n.global
|
||||
|
||||
type Props = {
|
||||
domain: string
|
||||
}
|
||||
|
||||
const recordStore = useRecordStore()
|
||||
const { notification } = createDiscreteApi(['notification'])
|
||||
const editModalShow = ref(false)
|
||||
const editingRecord = ref<Record>({} as Record)
|
||||
const loading = ref(true);
|
||||
const records = ref<Record[] | undefined>([] as Record[]);
|
||||
const soa = ref<SOARecord>({} as SOARecord)
|
||||
const reloadRecords = () => records.value = recordStore.records?.filter(e => e.record_type !== RecordTypes.RecordTypeSOA)
|
||||
|
||||
async function refreshRecords(domain: string) {
|
||||
try {
|
||||
await recordStore.loadRecords(domain)
|
||||
reloadRecords()
|
||||
soa.value = recordStore.records?.find(e => e.record_type === RecordTypes.RecordTypeSOA)?.content as SOARecord
|
||||
} catch (err) {
|
||||
const msg = getErrorInfo(err)
|
||||
notification.error(msg)
|
||||
console.error(err)
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/domains')
|
||||
}
|
||||
|
||||
function searchRecord(value: string) {
|
||||
if (value.length > 0) {
|
||||
records.value = recordStore.records?.
|
||||
filter(e => e.record_type !== RecordTypes.RecordTypeSOA).
|
||||
filter(e => !!~e.name.indexOf(value))
|
||||
} else {
|
||||
records.value = recordStore.records?.
|
||||
filter(e => e.record_type !== RecordTypes.RecordTypeSOA)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRecord(domain: string, record: Record) {
|
||||
try {
|
||||
await recordStore.removeRecord(domain, record)
|
||||
reloadRecords()
|
||||
} catch (err) {
|
||||
const msg = getErrorInfo(err)
|
||||
notification.error(msg)
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
function showEditing(domain: string, record: Record) {
|
||||
editModalShow.value = true
|
||||
editingRecord.value = record
|
||||
}
|
||||
|
||||
function newRecord(domain: string) {
|
||||
showEditing(domain, {
|
||||
zone: `${domain}.`,
|
||||
ttl: 500,
|
||||
record_type: RecordTypes.RecordTypeA,
|
||||
content: {
|
||||
ip: ''
|
||||
} as ARecord
|
||||
} as Record)
|
||||
}
|
||||
|
||||
const generateColumns = (domain: string) => [
|
||||
{
|
||||
key: 'no',
|
||||
title: '#',
|
||||
render(_, index) {
|
||||
return index + 1
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
title: t("records.name"),
|
||||
},
|
||||
{
|
||||
key: 'record_type',
|
||||
title: t('records.recordType')
|
||||
},
|
||||
{
|
||||
key: 'content',
|
||||
title: t('records.content'),
|
||||
render(row: Record) {
|
||||
return Object.entries(row.content).map(i => i[1]).join(" ")
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'ttl',
|
||||
title: 'TTL (s)'
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
render(row: Record) {
|
||||
return <RecordOps record={row} domain={domain} onRecord-delete={deleteRecord} onEdit-record={showEditing} />
|
||||
}
|
||||
}
|
||||
] as DataTableColumns<Record>
|
||||
|
||||
const statRefresh = () => (
|
||||
<NGi>
|
||||
<NStatistic value={soa.value.refresh}>
|
||||
{{
|
||||
suffix: () => 's',
|
||||
label: () => (
|
||||
<>
|
||||
<NIcon component={RedoAlt} style={{ transform: 'translateY(2px)' }} />
|
||||
<span>{t('records.refresh')}</span>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</NStatistic>
|
||||
</NGi>
|
||||
)
|
||||
|
||||
const statRetry = () => (
|
||||
<NGi>
|
||||
<NStatistic value={soa.value.retry}>
|
||||
{{
|
||||
suffix: () => 's',
|
||||
label: () => (
|
||||
<>
|
||||
<NIcon component={CheckCircle} style={{ transform: 'translateY(2px)' }} />
|
||||
<span>{t('records.retry')}</span>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</NStatistic>
|
||||
</NGi>
|
||||
)
|
||||
|
||||
const statExpire = () => (
|
||||
<NGi>
|
||||
<NStatistic value={soa.value.expire}>
|
||||
{{
|
||||
suffix: () => 's',
|
||||
label: () => (
|
||||
<>
|
||||
<NIcon component={Clock} style={{ transform: 'translateY(2px)' }} />
|
||||
<span>{t('records.expire')}</span>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</NStatistic>
|
||||
</NGi>
|
||||
)
|
||||
|
||||
const statTTL = () => (
|
||||
<NGi>
|
||||
<NStatistic value={soa.value.minttl}>
|
||||
{{
|
||||
suffix: () => 's',
|
||||
label: () => (
|
||||
<>
|
||||
<NIcon component={Cogs} style={{ transform: 'translateY(2px)' }} />
|
||||
<span>{t('records.ttl')}</span>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</NStatistic>
|
||||
</NGi>
|
||||
)
|
||||
|
||||
function recordsViewBodyHeaderExtra() {
|
||||
return (
|
||||
<NGrid cols={4} >
|
||||
<statRefresh />
|
||||
<statRetry />
|
||||
<statExpire />
|
||||
<statTTL />
|
||||
</NGrid>
|
||||
)
|
||||
}
|
||||
|
||||
function recordsViewBody({ domain }: Props) {
|
||||
const columns = generateColumns(domain)
|
||||
return (
|
||||
<NModalProvider>
|
||||
<NPageHeader title={t('domains.dnsRecord')} subtitle={domain} onBack={goBack}>
|
||||
{{
|
||||
extra: () => (
|
||||
<NFlex wrap={false} justify="end" inline>
|
||||
<NButton type="primary" onClick={() => newRecord(domain)}>
|
||||
{{
|
||||
icon: () => <NIcon component={PlusSquare} />,
|
||||
default: () => t('common.add')
|
||||
}}
|
||||
</NButton>
|
||||
<NInput placeholder={t('records.search')} onUpdate:value={searchRecord} clearable>
|
||||
{{
|
||||
prefix: () => <NIcon component={Search} />
|
||||
}}
|
||||
</NInput>
|
||||
</NFlex>
|
||||
),
|
||||
default: () => <recordsViewBodyHeaderExtra />
|
||||
}}
|
||||
</NPageHeader>
|
||||
<br />
|
||||
<NDataTable data={records.value} columns={columns} pagination={{ pageSize: 20 }} />
|
||||
</NModalProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function RecordsView({ domain }: Props) {
|
||||
try {
|
||||
refreshRecords(domain)
|
||||
} catch (err) {
|
||||
const msg = getErrorInfo(err)
|
||||
notification.error(msg)
|
||||
console.error(err)
|
||||
}
|
||||
return (
|
||||
<div id='records'>
|
||||
<RecordEditModal show={editModalShow.value} domain={domain} record={editingRecord.value} onReloadRecords={reloadRecords} />
|
||||
{
|
||||
loading.value ? <NSpin size='large' /> : <recordsViewBody domain={domain} />
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
RecordsView.displayName = 'RecordsView'
|
||||
export default RecordsView
|
@@ -1,231 +0,0 @@
|
||||
<script setup lang="tsx">
|
||||
import {
|
||||
NSpin, NPageHeader, useNotification,
|
||||
NFlex, NButton, NIcon, NGrid, NGi,
|
||||
NStatistic, NDataTable, NInput,
|
||||
NModalProvider
|
||||
} from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRecordStore, type Record, type SOARecord, RecordTypes } from '@/stores/records'
|
||||
import { getErrorInfo } from '@/apis/api'
|
||||
import { PlusSquare, RedoAlt, CheckCircle, Clock, Cogs, Search } from '@vicons/fa'
|
||||
import router from '@/router';
|
||||
import RecordOps from '@/components/records/RecordOps'
|
||||
import RecordEditModal from '@/components/records/RecordEditModal.vue'
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
domain: string
|
||||
}>()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'no',
|
||||
title: '#',
|
||||
render(_, index) {
|
||||
return index + 1
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
title: t("records.name"),
|
||||
},
|
||||
{
|
||||
key: 'record_type',
|
||||
title: t('records.recordType')
|
||||
},
|
||||
{
|
||||
key: 'content',
|
||||
title: t('records.content'),
|
||||
render(row: Record) {
|
||||
return Object.entries(row.content).map(i => i[1]).join(" ")
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'ttl',
|
||||
title: 'TTL (s)'
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
render(row: Record) {
|
||||
return <RecordOps record={row} domain={props.domain} onRecord-delete={deleteRecord} onEdit-record={showEditing} />
|
||||
}
|
||||
}
|
||||
] as DataTableColumns<Record>
|
||||
|
||||
const recordStore = useRecordStore()
|
||||
const notification = useNotification()
|
||||
|
||||
const records = ref<Record[] | undefined>([] as Record[]);
|
||||
const soa = ref<SOARecord>({} as SOARecord)
|
||||
const editModalShow = ref(false)
|
||||
const editingRecord = ref<Record>({} as Record)
|
||||
const loading = ref(true);
|
||||
onMounted(() => {
|
||||
try {
|
||||
refreshRecords()
|
||||
} catch (err) {
|
||||
const msg = getErrorInfo(err)
|
||||
notification.error(msg)
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
|
||||
const reloadRecords = () => records.value = recordStore.records?.filter(e => e.record_type !== RecordTypes.RecordTypeSOA)
|
||||
|
||||
|
||||
async function refreshRecords() {
|
||||
try {
|
||||
await recordStore.loadRecords(props.domain)
|
||||
reloadRecords()
|
||||
soa.value = recordStore.records?.find(e => e.record_type === RecordTypes.RecordTypeSOA)?.content as SOARecord
|
||||
} catch (err) {
|
||||
const msg = getErrorInfo(err)
|
||||
notification.error(msg)
|
||||
console.error(err)
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/domains')
|
||||
}
|
||||
|
||||
function searchRecord(value: string) {
|
||||
if (value.length > 0) {
|
||||
records.value = recordStore.records?.
|
||||
filter(e => e.record_type !== RecordTypes.RecordTypeSOA).
|
||||
filter(e => !!~e.name.indexOf(value))
|
||||
} else {
|
||||
records.value = recordStore.records?.
|
||||
filter(e => e.record_type !== RecordTypes.RecordTypeSOA)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRecord(domain: string, record: Record) {
|
||||
try {
|
||||
await recordStore.removeRecord(domain, record)
|
||||
reloadRecords()
|
||||
} catch (err) {
|
||||
const msg = getErrorInfo(err)
|
||||
notification.error(msg)
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
function showEditing(domain: string, record: Record) {
|
||||
editModalShow.value = true
|
||||
editingRecord.value = record
|
||||
}
|
||||
|
||||
function newRecord() {
|
||||
showEditing(props.domain, {
|
||||
zone: `${props.domain}.`,
|
||||
ttl: 500
|
||||
} as Record)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="records">
|
||||
<RecordEditModal v-model:show="editModalShow" :domain="domain" :record="editingRecord"
|
||||
@reload-records="reloadRecords" />
|
||||
<NSpin size="large" v-if="loading" />
|
||||
<div v-else class="content">
|
||||
<NModalProvider>
|
||||
<NPageHeader :title="t('domains.dnsRecord')" :subtitle="domain" @back="goBack">
|
||||
<template #extra>
|
||||
<NFlex :wrap="false" justify="end" inline>
|
||||
<NButton type="primary" @click="newRecord">
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<PlusSquare />
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t('common.add') }}
|
||||
</NButton>
|
||||
<NInput :placeholder="t('records.search')" @update:value="searchRecord" clearable>
|
||||
<template #prefix>
|
||||
<NIcon :component="Search" />
|
||||
</template>
|
||||
</NInput>
|
||||
</NFlex>
|
||||
</template>
|
||||
<NGrid :cols="4">
|
||||
<NGi>
|
||||
<NStatistic :value="soa?.refresh">
|
||||
<template #suffix>
|
||||
s
|
||||
</template>
|
||||
<template #label>
|
||||
<NIcon class="icon">
|
||||
<RedoAlt />
|
||||
</NIcon>
|
||||
{{ t('records.refresh') }}
|
||||
</template>
|
||||
</NStatistic>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<NStatistic :value="soa?.retry">
|
||||
<template #suffix>
|
||||
s
|
||||
</template>
|
||||
<template #label>
|
||||
<NIcon class="icon">
|
||||
<CheckCircle />
|
||||
</NIcon>
|
||||
{{ t('records.retry') }}
|
||||
</template>
|
||||
</NStatistic>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<NStatistic :value="soa?.expire">
|
||||
<template #suffix>
|
||||
s
|
||||
</template>
|
||||
<template #label>
|
||||
<NIcon class="icon">
|
||||
<Clock />
|
||||
</NIcon>
|
||||
{{ t('records.expire') }}
|
||||
</template>
|
||||
</NStatistic>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<NStatistic :value="soa?.minttl">
|
||||
<template #suffix>
|
||||
s
|
||||
</template>
|
||||
<template #label>
|
||||
<NIcon class="icon">
|
||||
<Cogs />
|
||||
</NIcon>
|
||||
{{ t('records.ttl') }}
|
||||
</template>
|
||||
</NStatistic>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
</NPageHeader>
|
||||
<br />
|
||||
<NDataTable :data="records" :columns="columns" :pagination="{ pageSize: 20 }" />
|
||||
</NModalProvider>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
div#records {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user