Skip to content

Commit

Permalink
WIP cache control panel
Browse files Browse the repository at this point in the history
  • Loading branch information
juandjara committed Nov 10, 2023
1 parent 11fc169 commit e8515dc
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 12 deletions.
46 changes: 40 additions & 6 deletions app/lib/cache.server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import type { ParsedFile, TreeItem } from "./github"
import { withRedis } from "./redis.server"

const ONE_DAY = 60 * 60 * 24

export async function getCachedFiles(repo: string, branch: string) {
return withRedis<string[]>(async (db) => {
const data = await db.smembers(`caches:${repo}`)
return (data || [])
.filter((key) => key.startsWith(`file:${repo}/${branch}/`))
.map((key) => key.replace(`file:${repo}/${branch}/`, ""))
})
}

export async function deleteAllCaches(repo: string) {
return withRedis(async (db) => {
const cacheKeys = await db.smembers(`caches:${repo}`)
await db.del(cacheKeys)
})
}

export async function getTreeCache(repo: string, sha: string) {
return withRedis<TreeItem[]>(async (db) => {
const data = await db.get(`tree:${repo}:${sha}`)
Expand All @@ -9,15 +27,23 @@ export async function getTreeCache(repo: string, sha: string) {
}

export async function setTreeCache(repo: string, sha: string, tree: TreeItem[]) {
const key = `tree:${repo}:${sha}`
return withRedis(async (db) => {
const oneDay = 60 * 60 * 24
await db.setex(`tree:${repo}:${sha}`, oneDay, JSON.stringify(tree))
await db.pipeline()
.sadd(`caches:${repo}`, key)
.expire(`caches:${repo}`, ONE_DAY)
.setex(key, ONE_DAY, JSON.stringify(tree))
.exec()
})
}

export async function deleteTreeCache(repo: string, sha: string) {
const key = `tree:${repo}:${sha}`
return withRedis(async (db) => {
await db.del(`tree:${repo}:${sha}`)
await db.pipeline()
.srem(`caches:${repo}`, key)
.del(key)
.exec()
})
}

Expand All @@ -29,14 +55,22 @@ export async function getFileCache(repo: string, branch: string, path: string) {
}

export async function setFileCache(repo: string, branch: string, path: string, file: ParsedFile) {
const key = `file:${repo}/${branch}/${path}`
return withRedis(async (db) => {
const oneDay = 60 * 60 * 24
await db.setex(`file:${repo}/${branch}/${path}`, oneDay, JSON.stringify(file))
await db.pipeline()
.sadd(`caches:${repo}`, key)
.expire(`caches:${repo}`, ONE_DAY)
.setex(key, ONE_DAY, JSON.stringify(file))
.exec()
})
}

export async function deleteFileCache(repo: string, branch: string, path: string) {
const key = `file:${repo}/${branch}/${path}`
return withRedis(async (db) => {
await db.del(`file:${repo}/${branch}/${path}`)
await db.pipeline()
.srem(`caches:${repo}`, key)
.del(key)
.exec()
})
}
33 changes: 30 additions & 3 deletions app/lib/projects.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export async function deleteProject(project: Project) {
db.srem(`projects:${project.user}`, project.id),
db.del(`project:${project.id}`),
db.del(`repo:${project.repo}`),
db.del(`drafts:${project.repo}`),
])
})
}
Expand Down Expand Up @@ -190,7 +191,7 @@ export function processFileContent(fileContent: Pick<ParsedFile, 'content' | 'sh
}
}

export async function getCollectionFiles(token: string, project: Project, collection: ProjectCollection) {
export async function getCollectionFiles(token: string, project: Project, collection: ProjectCollection, includeBody = false) {
const tree = await getRepoFiles(token, project.repo, project.branch)
const collectionTree = tree.filter((f) => {
const inCollection = getDirname(f.path) === collection.route.replace(/^\//, '')
Expand Down Expand Up @@ -219,7 +220,8 @@ export async function getCollectionFiles(token: string, project: Project, collec
const collectionFile = processFileContent(fileContent)
parsedFiles.push({
...collectionFile,
body: '', // removing body for collection files to reduce payload size
// removing body for collection files to reduce payload size
body: includeBody ? collectionFile.body : '',
})
}

Expand All @@ -238,14 +240,23 @@ type UpdateOrderParams = {
export async function updateCollectionFileOrder(token: string, payload: UpdateOrderParams) {
const { repo, branch, collectionRoute, files } = payload

const fullFiles = await getCollectionFiles(
token,
{ repo, branch, id: 0, title: '', user: '' },
{ route: collectionRoute, name: '', id: '', template: '' },
true
)

const contents = [] as string[]
for (const file of files) {
const matter = Object.entries(file.attributes)
.map(([key, value]) => `${key}: ${key === 'order' ? files.indexOf(file) : value}`)
.join('\n')

const fullFile = fullFiles.find((f) => f.path === file.path)

await deleteFileCache(repo, branch, file.path)
const content = ['---', matter, '---', '', file.body].join('\n')
const content = ['---', matter, '---', '', fullFile?.body || ''].join('\n')
contents.push(content)
}

Expand Down Expand Up @@ -279,6 +290,15 @@ export async function saveDraft(params: SaveDraftParams) {
})
}

export async function getDraftKeys(project: Project) {
return withRedis(async (db) => {
const keys = await db.smembers(`drafts:${project.repo}`)
return (keys || [])
.filter((k) => k.startsWith(`draft:${project.id}:`))
.map((k) => decodeURIComponent(k.replace(`draft:${project.id}:`, '')))
})
}

export async function getDraft(projectId: number, path: string) {
return withRedis<CollectionFile>(async (db) => {
const data = await db.get(`draft:${projectId}:${encodeURIComponent(path)}`)
Expand All @@ -294,6 +314,13 @@ export async function deleteDraft(project: Project, path: string) {
})
}

export async function deleteAllDrafts(project: Project) {
return withRedis(async (db) => {
const draftKeys = await db.smembers(`drafts:${project.repo}`)
await db.del(draftKeys)
})
}

export async function renameDraft(project: Project, oldPath: string, newPath: string) {
const draft = await getDraft(project.id, oldPath)
if (!draft) {
Expand Down
89 changes: 86 additions & 3 deletions app/routes/p/$project/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
import { ComboBoxLocal } from "@/components/ComboBoxLocal"
import { deleteAllCaches, getCachedFiles } from "@/lib/cache.server"
import type { TreeItem} from "@/lib/github"
import { FileMode } from "@/lib/github"
import metaTitle from "@/lib/metaTitle"
import { getProject, getProjectConfig, updateConfigFile , deleteConfigFile, deleteProject, updateProject } from "@/lib/projects.server"
import { getProject, getProjectConfig, updateConfigFile , deleteConfigFile, deleteProject, updateProject, getDraftKeys, deleteAllDrafts } from "@/lib/projects.server"
import { requireUserSession, setFlashMessage } from "@/lib/session.server"
import { buttonCN, checkboxCN, iconCN, inputCN, labelCN } from "@/lib/styles"
import useProjectConfig, { useProject, useRepoTree } from "@/lib/useProjectConfig"
import { DocumentDuplicateIcon, ListBulletIcon, PlusIcon, FolderOpenIcon } from "@heroicons/react/20/solid"
import type { ActionFunction } from "@remix-run/node"
import type { ActionFunction, LoaderArgs } from "@remix-run/node"
import { redirect } from "@remix-run/node"
import { Form, Link, Outlet, useNavigation } from "@remix-run/react"
import { Form, Link, Outlet, useLoaderData, useNavigation } from "@remix-run/react"
import clsx from "clsx"
import { useMemo } from "react"

export const meta = {
title: metaTitle('Settings')
}

export async function loader({ params }: LoaderArgs) {
const project = await getProject(Number(params.project))
const [cachedFiles, drafts] = await Promise.all([
getCachedFiles(project.repo, project.branch),
getDraftKeys(project),
])

return { cachedFiles, drafts }
}

export const action: ActionFunction = async ({ request, params }) => {
const { token } = await requireUserSession(request)
const project = await getProject(Number(params.project))
Expand Down Expand Up @@ -56,6 +67,16 @@ export const action: ActionFunction = async ({ request, params }) => {
flashMessage = 'Project deleted successfully'
}

if (op === 'clearCache') {
await deleteAllCaches(project.repo)
flashMessage = 'Cache cleared successfully'
}

if (op === 'removeDrafts') {
await deleteAllDrafts(project)
flashMessage = 'Drafts removed successfully'
}

const headers = new Headers({
'cache-control': 'no-cache',
'Set-Cookie': await setFlashMessage(request, flashMessage)
Expand Down Expand Up @@ -171,13 +192,75 @@ export default function ProjectSettings() {
</ul>
</div>
</section>
<CacheSection />
<EditProject folders={folders} />
<DangerZone />
</main>
</div>
)
}

function CacheSection() {
const { cachedFiles, drafts } = useLoaderData<typeof loader>()
const transition = useNavigation()
const busy = transition.state === 'submitting'

return (
<>
<section>
<h3 className="text-slate-500 dark:text-slate-300 font-medium text-2xl mb-2">Cache</h3>
<details>
<summary className="text-slate-700 dark:text-slate-300 mb-2 cursor-pointer">
{cachedFiles.length} cached file{cachedFiles.length === 1 ? '' : 's'}
</summary>
<ul className="">
{cachedFiles.map((file) => (
<li key={file} className="text-slate-700 dark:text-slate-300">
<code>{file}</code>
</li>
))}
</ul>
</details>
<Form method='post' replace>
<button
name="operation"
value="clearCache"
type="submit"
disabled={busy}
className={`mt-4 ${buttonCN.normal} ${buttonCN.slate}`}>
{busy ? 'Clearing cache...' : 'Clear GitHub cache'}
</button>
</Form>
</section>
<section>
<h3 className="text-slate-500 dark:text-slate-300 font-medium text-2xl mb-2">Drafts</h3>
<details>
<summary className="text-slate-700 dark:text-slate-300 mb-2 cursor-pointer">
{drafts.length} saved draft{drafts.length === 1 ? '' : 's'}
</summary>
<ul className="">
{drafts.map((file) => (
<li key={file} className="text-slate-700 dark:text-slate-300">
<code>{file}</code>
</li>
))}
</ul>
</details>
<Form method='post' replace>
<button
name="operation"
value="removeDrafts"
type="submit"
disabled={busy}
className={`mt-4 ${buttonCN.normal} ${buttonCN.slate}`}>
{busy ? 'Removing...' : 'Remove all drafts'}
</button>
</Form>
</section>
</>
)
}

function EditProject({ folders }: { folders: TreeItem[] }) {
const project = useProject()
const config = useProjectConfig()
Expand Down

0 comments on commit e8515dc

Please sign in to comment.