Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ jobs:
- name: Run Task
run: |
export PYTHONPATH=$(pwd)
FLASK_ENV=development uv run flask -A server.app --debug app version
uv run ${{ matrix.task.command }}

- name: Minimize uv cache
Expand Down
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ ENV VIRTUAL_ENV="/code/.venv"
ENV PATH="$VIRTUAL_ENV/bin:$PATH"

WORKDIR /code
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
RUN pip install -U pip && pip install uv

RUN groupadd -g ${GID} ${GROUPNAME} && \
Expand Down
1 change: 1 addition & 0 deletions configs/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const table = {
users: [20, 50, 100] as [number, ...number[]],
history: [20, 50, 100] as [number, ...number[]],
bulks: [20, 50, 100] as [number, ...number[]],
cacheGroups: [20, 50, 100] as number[],
},
}

Expand Down
34 changes: 34 additions & 0 deletions configs/server.config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,40 @@ port = 26379
url = "amqp://guest:guest@rabbitmq:5672//"


[cache_groups]
# Cache key suffix of group information in Redis.
cache_key_suffix = "_gakunin_groups"

# Map groups API endpoint.
api_endpoint = "/api/groups"

# Cache time-to-live of group information in Redis.
# if it specified less than 0, it will be considered as no expiration.
cache_ttl = 86400

# Request timeout (in seconds) when connecting to mAP API.
request_timeout = 20

# Request interval (in seconds) between mAP API requests.
request_interval = 3

# Request retries when failed to fetch groups from mAP API.
request_retries = 3

# Base time (in seconds) for exponential backoff during request retries.
request_retry_base = 4

# Factor (in seconds) for exponential backoff during request retries.
request_retry_factor = 5

# Maximum time (in seconds) for exponential backoff during request retries.
request_retry_max = 90


# Path to the directory containing institution TLS files.
directory_path = "/var/mnt"


# [develop]
# Enable or disable developer login feature.
# developer_login = false
Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies = [
"pydantic[email]>=2.12.5",
"requests>=2.32.5",
"sqlalchemy-utils>=0.42.1",
"weko-group-cache-db",
]

[dependency-groups]
Expand Down Expand Up @@ -77,7 +78,7 @@ select = ["ALL"]
"__init__.py" = ["F401"]
"**/api/**.py" = ["TC001", "TC002", "TC003"]
"**/entities/**.py" = ["TC001", "TC002", "TC003"]
"*.pyi" = ["CPY001"]
"*.pyi" = ["CPY001", "E501"]

[tool.ruff.lint.isort]
# refer to https://docs.astral.sh/ruff/settings/#lintisort
Expand Down Expand Up @@ -105,3 +106,6 @@ skip-magic-trailing-comma = false
include = ["src"]
extraPaths = ["src"]
typeCheckingMode = "standard"

[tool.uv.sources]
weko-group-cache-db = { git = "https://github.com/ivis-weko3-dev/weko-group-cache-db.git", rev = "develop" }
246 changes: 246 additions & 0 deletions src/app/composables/groupCaches.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
/**
* Composable for managing cache groups.
*/
import { UCheckbox, ULink } from '#components'

import type { Row, Table } from '@tanstack/table-core'
import type { DropdownMenuItem, SelectItem, TableColumn } from '@nuxt/ui'

const useCacheGroups = () => {
const route = useRoute()
const router = useRouter()

const { t: $t } = useI18n()

/** Reactive query object */
const query = computed<CacheGroupsSearchQuery>(() => normalizeCacheGroupsQuery(route.query))
/** Update query parameters and push to router */
const updateQuery = (newQuery: Partial<CacheGroupsSearchQuery>) => {
router.push({
query: {
...route.query,
...newQuery,
},
})
}

const searchTerm = ref(query.value.q)
const filter = ref(query.value.f)
const pageNumber = ref(query.value.p)
const pageSize = ref(query.value.l)

const searchIdentityKey = computed(() => {
const { p, l, ...filters } = query.value
return JSON.stringify(filters)
})

const selectedMap = useState<Record<string, RepositorySummary | undefined>>(
`selection-group-caches:${searchIdentityKey.value}`, () => ({}),
)

const selectedCount = computed(() => {
return Object.values(selectedMap.value).filter(value => value !== undefined).length
})
const toggleSelection = (event: Event | undefined, row: Row<RepositoryCache>) => {
selectedMap.value[row.original.id]
= selectedMap.value[row.original.id] ? undefined : row.original
}
const toggleAllPageRows = (table: Table<RepositoryCache>) => {
const pageRows = table.getRowModel().rows
const allSelected = pageRows.every(row => selectedMap.value[row.original.id] !== undefined)

if (allSelected) {
for (const row of pageRows) {
selectedMap.value[row.original.id] = undefined
}
}
else {
for (const row of pageRows) {
selectedMap.value[row.original.id] = row.original
}
}
}
const isAllPageRowsSelected = (table: Table<RepositoryCache>) => {
const pageRows = table.getRowModel().rows
return pageRows.length > 0 && pageRows.every(
row => selectedMap.value[row.original.id] !== undefined,
)
}
const isSomePageRowsSelected = (table: Table<RepositoryCache>) => {
const pageRows = table.getRowModel().rows
const selectedRows = pageRows.filter(row => selectedMap.value[row.original.id] !== undefined)
return selectedRows.length > 0 && selectedRows.length < pageRows.length
}

const getSelected = (): { id: string, serviceName: string, serviceUrl: string }[] => {
return Object.entries(selectedMap.value)
.filter(([_, service]) => service !== undefined)
.map(([id, service]) => ({
id, serviceName: service!.serviceName, serviceUrl: service!.serviceUrl,
}))
}
const clearSelection = () => {
selectedMap.value = {}
}

const modals = reactive<Record<GroupCacheUpdateAction, boolean>>({
'all': false,
'id-specified': false,
})
const isUpdating = ref(false)

/** Column names with translations */
const columnNames = computed(() => ({
id: '#',
serviceName: $t('group-caches.table.column.repository-name'),
serviceUrl: $t('group-caches.table.column.repository-url'),
updated: $t('group-caches.table.column.repository-updated-at'),
}))

const filterItems = computed<SelectItem[]>(() => [
{
label: $t('group-caches.status.cached'),
value: 'e' as GroupCacheStatus,
},
{
label: $t('group-caches.status.no-cached'),
value: 'n' as GroupCacheStatus,
},
])

const selectedRepositoriesAction = computed<[DropdownMenuItem, ...DropdownMenuItem[]]>(() => [
{
type: 'label' as const,
label: $t('repositories.all-repositories-actions'),
},
{
icon: 'i-lucide-refresh-cw',
label: $t('group-caches.button.update-all-repositories'),
onSelect: () => modals.all = true,
},
{
type: 'separator' as const,
},
{
type: 'label' as const,
label: $t('repositories.selected-repositories-actions'),
},
{
icon: 'i-lucide-refresh-cw',
label: $t('group-caches.button.update-selected-repositories'),
onSelect: () => modals['id-specified'] = true,
disabled: selectedCount.value === 0,
},
])

type CacheGroupsTableColumn = TableColumn<RepositoryCache>
const columns = computed<CacheGroupsTableColumn[]>(() => [
{
id: 'select',
header: ({ table }) =>
h(UCheckbox, {
'modelValue': isSomePageRowsSelected(table)
? 'indeterminate'
: isAllPageRowsSelected(table),
'onUpdate:modelValue': () => toggleAllPageRows(table),
'ui': { root: 'py-0.5' },
'disabled': isUpdating.value,
'aria-label': 'Select all',
}),
cell: ({ row }) =>
h(UCheckbox, {
'modelValue': selectedMap.value[row.original.id] !== undefined,
'onUpdate:modelValue': () => toggleSelection(undefined, row),
'disabled': isUpdating.value,
'aria-label': 'Select row',
}),
enableHiding: false,
},
{
accessorKey: 'serviceName',
header: () => h(
'span', { class: 'text-xs text-default font-medium' }, columnNames.value.serviceName,
),
cell: ({ row }) => h(
ULink, {
to: `/repositories/${row.original.id}`,
class: 'font-bold hover:underline inline-flex items-center',
}, () => [
h('span', row.original.serviceName),
],

),
},
{
accessorKey: 'url',
header: () => h(
'span', { class: 'text-xs text-default font-medium' }, columnNames.value.serviceUrl,
),
},
{
accessorKey: 'updated',
header: () => h(
'span', { class: 'text-xs text-default font-medium' }, columnNames.value.updated,
),
cell: ({ row }) =>
row.original.updated
? datetimeFormatter.format(new Date(row.original.updated))
: $t('group-caches.status.no-cached'),
},
])

const makePageInfo = (result: Ref<GroupCachesSearchResult | undefined>) => {
return computed(() => {
const start = result.value?.offset ?? 1
const total = result.value?.total ?? 0
const end = Math.min(start + pageSize.value!, total)
const count = selectedCount.value

if (count > 0)
return `${start} - ${end} / ${total} (${$t('table.selected')} ${count})`
return `${start} - ${end} / ${total}`
})
}

return {
/** Computed reference for the current query */
query,
/** Update query parameters and push to router */
updateQuery,
/** Criteria for filtering and sorting repositories */
criteria: {
/** Reactive object for the search term */
searchTerm,
/** Reactive object for the filter */
filter,
/** Reactive object for the current page number */
pageNumber,
/** Reactive object for the page size */
pageSize,
},
/** Flag indicating if the data is being updated */
isUpdating,
/** Reactive object for the selected repositories */
selectedMap,
/** Computed reference for the count of selected repositories */
selectedCount,
/** Toggle the selection of a repository */
toggleSelection,
/** Get the selected repositories */
getSelected,
/** Clear all selection */
clearSelection,
/** Dropdown items of actions for the selected repositories */
selectedRepositoriesAction,
/** Items for filtering the repositories */
filterItems,
/** Column definitions for the table with translations */
columns,
/** Make indicator for the page information */
makePageInfo,
/** Reactive object for the state of modals */
modals,
}
}

export { useCacheGroups }
4 changes: 2 additions & 2 deletions src/app/composables/useMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ export function useMenu() {

},
{
label: $t('cache-groups.title'),
to: '/cache-groups',
label: $t('group-caches.title'),
to: '/group-caches',
icon: 'i-lucide-database',
requiredSystemAdmin: true,
},
Expand Down
Loading
Loading