feat: group history by date and time

This commit is contained in:
liyasthomas
2021-12-31 12:39:23 +05:30
parent 2de73ae7b4
commit a0f7201fae
9 changed files with 189 additions and 162 deletions

View File

@@ -102,6 +102,7 @@ body {
.material-icons {
@apply flex-shrink-0;
@apply overflow-hidden;
font-size: var(--line-height-body) !important;
width: var(--line-height-body);
@@ -109,6 +110,7 @@ body {
.svg-icons {
@apply flex-shrink-0;
@apply overflow-hidden;
height: var(--line-height-body);
width: var(--line-height-body);
@@ -457,6 +459,10 @@ pre.ace_editor {
@apply px-1;
}
.capitalize-first::first-letter {
@apply capitalize;
}
@media (max-width: 767px) {
main {
margin-bottom: env(safe-area-inset-bottom);

View File

@@ -1,7 +1,7 @@
<template>
<div class="bg-error flex justify-between">
<span
class="flex py-2 px-4 transition justify-center group relative items-center"
class="flex py-2 px-4 transition justify-center text-tiny group relative items-center"
>
<i class="mr-2 material-icons">info_outline</i>
<span class="text-secondaryDark">

View File

@@ -9,6 +9,12 @@
<span class="truncate">
{{ entry.request.url }}
</span>
<tippy
v-if="entry.updatedOn"
theme="tooltip"
:delay="[500, 20]"
:content="`${new Date(entry.updatedOn).toLocaleString()}`"
/>
</span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
@@ -49,53 +55,39 @@
</div>
</template>
<script lang="ts">
import {
computed,
defineComponent,
PropType,
ref,
} from "@nuxtjs/composition-api"
<script setup lang="ts">
import { computed, ref } from "@nuxtjs/composition-api"
import { makeGQLRequest } from "@hoppscotch/data"
import { setGQLSession } from "~/newstore/GQLSession"
import { GQLHistoryEntry } from "~/newstore/history"
export default defineComponent({
props: {
entry: { type: Object as PropType<GQLHistoryEntry>, default: () => {} },
showMore: Boolean,
},
setup(props) {
const expand = ref(false)
const props = defineProps<{
entry: GQLHistoryEntry
showMore: Boolean
}>()
const query = computed(() =>
expand.value
? (props.entry.request.query.split("\n") as string[])
: (props.entry.request.query
.split("\n")
.slice(0, 2)
.concat(["..."]) as string[])
)
const expand = ref(false)
const useEntry = () => {
setGQLSession({
request: makeGQLRequest({
name: props.entry.request.name,
url: props.entry.request.url,
headers: props.entry.request.headers,
query: props.entry.request.query,
variables: props.entry.request.variables,
}),
schema: "",
response: props.entry.response,
})
}
const query = computed(() =>
expand.value
? (props.entry.request.query.split("\n") as string[])
: (props.entry.request.query
.split("\n")
.slice(0, 2)
.concat(["..."]) as string[])
)
return {
expand,
query,
useEntry,
}
},
})
const useEntry = () => {
setGQLSession({
request: makeGQLRequest({
name: props.entry.request.name,
url: props.entry.request.url,
headers: props.entry.request.headers,
query: props.entry.request.query,
variables: props.entry.request.variables,
}),
schema: "",
response: props.entry.response,
})
}
</script>

View File

@@ -6,14 +6,14 @@
type="search"
autocomplete="off"
class="bg-transparent flex w-full p-4 py-2"
:placeholder="`${$t('action.search')}`"
:placeholder="`${t('action.search')}`"
/>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/history"
blank
:title="$t('app.wiki')"
:title="t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
@@ -21,30 +21,39 @@
data-testid="clear_history"
:disabled="history.length === 0"
svg="trash-2"
:title="$t('action.clear_all')"
:title="t('action.clear_all')"
@click.native="confirmRemove = true"
/>
</div>
</div>
<div class="flex flex-col">
<div v-for="(entry, index) in filteredHistory" :key="`entry-${index}`">
<HistoryRestCard
v-if="page == 'rest'"
:id="index"
:entry="entry"
:show-more="showMore"
@toggle-star="toggleStar(entry)"
@delete-entry="deleteHistory(entry)"
@use-entry="useHistory(entry)"
/>
<HistoryGraphqlCard
v-if="page == 'graphql'"
:entry="entry"
:show-more="showMore"
@toggle-star="toggleStar(entry)"
@delete-entry="deleteHistory(entry)"
@use-entry="useHistory(entry)"
/>
<div
v-for="(filteredHistoryGroup, filteredHistoryGroupIndex) in groupByDate(
filteredHistory,
'updatedOn'
)"
:key="`filteredHistoryGroup-${filteredHistoryGroupIndex}`"
class="flex flex-col"
>
<span
class="ml-4 capitalize-first px-3 align-start my-2 py-1 text-secondaryLight bg-primaryLight truncate rounded-l-full flex-inline text-tiny"
>
{{ filteredHistoryGroupIndex }}
</span>
<div
v-for="(entry, index) in filteredHistoryGroup"
:key="`entry-${index}`"
>
<component
:is="page == 'rest' ? 'HistoryRestCard' : 'HistoryGraphqlCard'"
:id="index"
:entry="entry"
:show-more="showMore"
@toggle-star="toggleStar(entry)"
@delete-entry="deleteHistory(entry)"
@use-entry="useHistory(entry)"
/>
</div>
</div>
</div>
<div
@@ -53,7 +62,7 @@
>
<i class="opacity-75 pb-2 material-icons">manage_search</i>
<span class="my-2 text-center">
{{ $t("state.nothing_found") }} "{{ filterText }}"
{{ t("state.nothing_found") }} "{{ filterText }}"
</span>
</div>
<div
@@ -64,24 +73,29 @@
:src="`/images/states/${$colorMode.value}/history.svg`"
loading="lazy"
class="flex-col object-contain object-center h-16 my-4 w-16 inline-flex"
:alt="`${$t('empty.history')}`"
:alt="`${t('empty.history')}`"
/>
<span class="text-center mb-4">
{{ $t("empty.history") }}
{{ t("empty.history") }}
</span>
</div>
<SmartConfirmModal
:show="confirmRemove"
:title="`${$t('confirm.remove_history')}`"
:title="`${t('confirm.remove_history')}`"
@hide-modal="confirmRemove = false"
@resolve="clearHistory"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "@nuxtjs/composition-api"
import { useReadonlyStream } from "~/helpers/utils/composables"
<script setup lang="ts">
import { computed, ref } from "@nuxtjs/composition-api"
import * as timeago from "timeago.js"
import {
useI18n,
useReadonlyStream,
useToast,
} from "~/helpers/utils/composables"
import {
restHistory$,
graphqlHistory$,
@@ -96,64 +110,65 @@ import {
} from "~/newstore/history"
import { setRESTRequest } from "~/newstore/RESTSession"
export default defineComponent({
props: {
page: { type: String as PropType<"rest" | "graphql">, default: null },
},
setup(props) {
return {
history: useReadonlyStream<RESTHistoryEntry[] | GQLHistoryEntry[]>(
props.page === "rest" ? restHistory$ : graphqlHistory$,
[]
),
}
},
data() {
return {
filterText: "",
showMore: false,
confirmRemove: false,
}
},
computed: {
filteredHistory(): any[] {
const filteringHistory = this.history as Array<
RESTHistoryEntry | GQLHistoryEntry
>
const props = defineProps<{
page: "rest" | "graphql"
}>()
return filteringHistory.filter(
(entry: RESTHistoryEntry | GQLHistoryEntry) => {
const filterText = this.filterText.toLowerCase()
return Object.keys(entry).some((key) => {
let value = entry[key as keyof typeof entry]
if (value) {
value = `${value}`
return value.toLowerCase().includes(filterText)
}
return false
})
const filterText = ref("")
const showMore = ref(false)
const confirmRemove = ref(false)
const toast = useToast()
const t = useI18n()
const groupByDate = (array: any[], key: string) => {
return array.reduce((rv: any, x: any) => {
;(rv[timeago.format(x[key])] = rv[timeago.format(x[key])] || []).push(x)
return rv
}, {})
}
const history = useReadonlyStream<RESTHistoryEntry[] | GQLHistoryEntry[]>(
props.page === "rest" ? restHistory$ : graphqlHistory$,
[]
)
const filteredHistory = computed(() => {
const filteringHistory = history as any as Array<
RESTHistoryEntry | GQLHistoryEntry
>
return filteringHistory.value.filter(
(entry: RESTHistoryEntry | GQLHistoryEntry) => {
return Object.keys(entry).some((key) => {
let value = entry[key as keyof typeof entry]
if (value) {
value = `${value}`
return value.toLowerCase().includes(filterText.value.toLowerCase())
}
)
},
},
methods: {
clearHistory() {
if (this.page === "rest") clearRESTHistory()
else clearGraphqlHistory()
this.$toast.success(`${this.$t("state.history_deleted")}`)
},
useHistory(entry: any) {
if (this.page === "rest") setRESTRequest(entry.request)
},
deleteHistory(entry: any) {
if (this.page === "rest") deleteRESTHistoryEntry(entry)
else deleteGraphqlHistoryEntry(entry)
this.$toast.success(`${this.$t("state.deleted")}`)
},
toggleStar(entry: any) {
if (this.page === "rest") toggleRESTHistoryEntryStar(entry)
else toggleGraphqlHistoryEntryStar(entry)
},
},
return false
})
}
)
})
const clearHistory = () => {
if (props.page === "rest") clearRESTHistory()
else clearGraphqlHistory()
toast.success(`${t("state.history_deleted")}`)
}
const useHistory = (entry: any) => {
if (props.page === "rest") setRESTRequest(entry.request)
}
const deleteHistory = (entry: any) => {
if (props.page === "rest") deleteRESTHistoryEntry(entry)
else deleteGraphqlHistoryEntry(entry)
toast.success(`${t("state.deleted")}`)
}
const toggleStar = (entry: any) => {
if (props.page === "rest") toggleRESTHistoryEntryStar(entry)
else toggleGraphqlHistoryEntryStar(entry)
}
</script>

View File

@@ -1,6 +1,7 @@
<template>
<div class="flex items-stretch group">
<span
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
class="cursor-pointer flex px-2 w-16 items-center justify-center truncate"
:class="entryStatus.className"
data-testid="restore_history_entry"
@@ -12,12 +13,17 @@
<span
class="cursor-pointer flex flex-1 min-w-0 py-2 pr-2 transition group-hover:text-secondaryDark"
data-testid="restore_history_entry"
:title="`${duration}`"
@click="$emit('use-entry')"
>
<span class="truncate">
{{ entry.request.endpoint }}
</span>
<tippy
v-if="entry.updatedOn"
theme="tooltip"
:delay="[500, 20]"
:content="`${new Date(entry.updatedOn).toLocaleString()}`"
/>
</span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
@@ -40,46 +46,36 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from "@nuxtjs/composition-api"
<script setup lang="ts">
import { computed } from "@nuxtjs/composition-api"
import findStatusGroup from "~/helpers/findStatusGroup"
import { useI18n } from "~/helpers/utils/composables"
import { RESTHistoryEntry } from "~/newstore/history"
export default defineComponent({
props: {
entry: { type: Object as PropType<RESTHistoryEntry>, default: () => {} },
showMore: Boolean,
},
setup(props) {
const t = useI18n()
const props = defineProps<{
entry: RESTHistoryEntry
showMore: Boolean
}>()
const duration = computed(() => {
if (props.entry.responseMeta.duration) {
const responseDuration = props.entry.responseMeta.duration
if (!responseDuration) return ""
const t = useI18n()
return responseDuration > 0
? `${t("request.duration")}: ${responseDuration}ms`
: t("error.no_duration")
} else return t("error.no_duration")
})
const duration = computed(() => {
if (props.entry.responseMeta.duration) {
const responseDuration = props.entry.responseMeta.duration
if (!responseDuration) return ""
const entryStatus = computed(() => {
const foundStatusGroup = findStatusGroup(
props.entry.responseMeta.statusCode
)
return (
foundStatusGroup || {
className: "",
}
)
})
return responseDuration > 0
? `${t("request.duration")}: ${responseDuration}ms`
: t("error.no_duration")
} else return t("error.no_duration")
})
return {
duration,
entryStatus,
const entryStatus = computed(() => {
const foundStatusGroup = findStatusGroup(props.entry.responseMeta.statusCode)
return (
foundStatusGroup || {
className: "",
}
},
)
})
</script>

View File

@@ -206,6 +206,7 @@ export function initHistory() {
historyRef.forEach((doc) => {
const entry = doc.data()
entry.id = doc.id
entry.updatedOn = doc.data().updatedOn.toDate()
history.push(translateToNewRESTHistory(entry))
})
@@ -227,6 +228,7 @@ export function initHistory() {
historyRef.forEach((doc) => {
const entry = doc.data()
entry.id = doc.id
entry.updatedOn = doc.data().updatedOn.toDate()
history.push(translateToNewGQLHistory(entry))
})

View File

@@ -22,6 +22,8 @@ export type RESTHistoryEntry = {
star: boolean
id?: string // For when Firebase Firestore is set
updatedOn?: Date
}
export type GQLHistoryEntry = {
@@ -33,6 +35,8 @@ export type GQLHistoryEntry = {
star: boolean
id?: string // For when Firestore ID is set
updatedOn?: Date
}
export function makeRESTHistoryEntry(
@@ -50,6 +54,7 @@ export function makeGQLHistoryEntry(
return {
v: 1,
...x,
updatedOn: new Date(),
}
}
@@ -61,7 +66,7 @@ export function translateToNewRESTHistory(x: any): RESTHistoryEntry {
const star = x.star ?? false
const duration = x.duration ?? null
const statusCode = x.status ?? null
const updatedOn = x.updatedOn ?? null
const obj: RESTHistoryEntry = makeRESTHistoryEntry({
request,
star,
@@ -69,6 +74,7 @@ export function translateToNewRESTHistory(x: any): RESTHistoryEntry {
duration,
statusCode,
},
updatedOn,
})
if (x.id) obj.id = x.id
@@ -83,11 +89,13 @@ export function translateToNewGQLHistory(x: any): GQLHistoryEntry {
const request = translateToGQLRequest(x)
const star = x.star ?? false
const response = x.response ?? ""
const updatedOn = x.updatedOn ?? ""
const obj: GQLHistoryEntry = makeGQLHistoryEntry({
request,
star,
response,
updatedOn,
})
if (x.id) obj.id = x.id
@@ -313,6 +321,7 @@ completedRESTResponse$.subscribe((res) => {
statusCode: res.statusCode,
},
star: false,
updatedOn: new Date(),
})
)
}

View File

@@ -96,6 +96,7 @@
"splitpanes": "^2.3.8",
"subscriptions-transport-ws": "^0.11.0",
"tern": "^0.24.3",
"timeago.js": "^4.0.2",
"uuid": "8.3.2",
"vue-apollo": "^3.1.0",
"vue-functional-data-merge": "^3.1.0",