Skip to content

Commit 021c281

Browse files
committed
feat: quota report download
Signed-off-by: Lukas Schaefer <lukas@lschaefer.xyz>
1 parent 3348d78 commit 021c281

File tree

7 files changed

+173
-6
lines changed

7 files changed

+173
-6
lines changed

appinfo/routes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@
2020
['name' => 'quotaRule#addRule', 'url' => '/quota/rule', 'verb' => 'POST'],
2121
['name' => 'quotaRule#updateRule', 'url' => '/quota/rule', 'verb' => 'PUT'],
2222
['name' => 'quotaRule#deleteRule', 'url' => '/quota/rule', 'verb' => 'DELETE'],
23+
['name' => 'quotaRule#getQuotaUsage', 'url' => '/quota/download-usage', 'verb' => 'GET'],
2324
],
2425
];

lib/Controller/QuotaRuleController.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
use OCA\OpenAi\Service\QuotaRuleService;
1212
use OCP\AppFramework\Controller;
1313
use OCP\AppFramework\Http;
14+
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
1415
use OCP\AppFramework\Http\DataResponse;
16+
use OCP\AppFramework\Http\Response;
17+
use OCP\AppFramework\Http\TextPlainResponse;
1518
use OCP\IRequest;
1619

1720
class QuotaRuleController extends Controller {
@@ -79,4 +82,35 @@ public function deleteRule(int $id): DataResponse {
7982
return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
8083
}
8184
}
85+
/**
86+
* Gets the quota usage between two dates
87+
* @param int $startDate
88+
* @param int $endDate
89+
* @param int $type
90+
* @return Http\StreamResponse|TextPlainResponse
91+
*/
92+
#[NoCSRFRequired]
93+
public function getQuotaUsage(int $startDate, int $endDate, int $type): Response {
94+
try {
95+
$result = $this->quotaRuleService->getQuotaUsage($startDate, $endDate, $type);
96+
$csv = fopen('php://memory', 'w');
97+
try {
98+
foreach ($result as $row) {
99+
fputcsv($csv, $row);
100+
}
101+
rewind($csv);
102+
103+
$response = new Http\StreamResponse($csv, Http::STATUS_OK);
104+
$response->setHeaders([
105+
'Content-Type' => 'text/csv',
106+
'Content-Disposition' => 'attachment; filename="quota_usage.csv"'
107+
]);
108+
return $response;
109+
} catch (Exception $e) {
110+
return new TextPlainResponse('Failed to get quota usage:' . $e->getMessage(), Http::STATUS_INTERNAL_SERVER_ERROR);
111+
}
112+
} catch (Exception $e) {
113+
return new TextPlainResponse('Failed to get quota usage:' . $e->getMessage(), Http::STATUS_NOT_FOUND);
114+
}
115+
}
82116
}

lib/Db/QuotaUsageMapper.php

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public function getQuotaUnitsInTimePeriod(int $type, int $periodStart): int {
8282
$qb = $this->db->getQueryBuilder();
8383

8484
// Get the sum of the units used in the time period
85-
$qb->select($qb->createFunction('SUM(units)'))
85+
$qb->select($qb->func()->sum('units'))
8686
->from($this->getTableName())
8787
->where(
8888
$qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT))
@@ -113,7 +113,7 @@ public function getQuotaUnitsOfUserInTimePeriod(string $userId, int $type, int $
113113
$qb = $this->db->getQueryBuilder();
114114

115115
// Get the sum of the units used in the time period
116-
$qb->select($qb->createFunction('SUM(units)'))
116+
$qb->select($qb->func()->sum('units'))
117117
->from($this->getTableName())
118118
->where(
119119
$qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT))
@@ -170,7 +170,7 @@ public function getQuotaUsagesOfUser(string $userId, int $type): array {
170170
public function getQuotaUnitsOfUser(string $userId, int $type): int {
171171
$qb = $this->db->getQueryBuilder();
172172

173-
$qb->select($qb->createFunction('SUM(units)'))
173+
$qb->select($qb->func()->sum('units'))
174174
->from($this->getTableName())
175175
->where(
176176
$qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
@@ -274,4 +274,51 @@ public function cleanupQuotaUsages(int $timePeriod): void {
274274
);
275275
$qb->executeStatement();
276276
}
277+
278+
/**
279+
* Gets quota usage of all users
280+
* @param int $startTime
281+
* @param int $endTime
282+
* @return array
283+
* @throws Exception
284+
* @throws RuntimeException
285+
*/
286+
public function getUsersQuotaUsage(int $startTime, int $endTime, $type): array {
287+
$qb = $this->db->getQueryBuilder();
288+
289+
$qb->select('user_id')
290+
->selectAlias($qb->func()->sum('units'), 'usage')
291+
->from($this->getTableName())
292+
->where($qb->expr()->gte('timestamp', $qb->createNamedParameter($startTime, IQueryBuilder::PARAM_INT)))
293+
->andWhere($qb->expr()->lte('timestamp', $qb->createNamedParameter($endTime, IQueryBuilder::PARAM_INT)))
294+
->andWhere($qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT)))
295+
->groupBy('user_id')
296+
->orderBy('usage', 'DESC');
297+
298+
return $qb->executeQuery()->fetchAll();
299+
}
300+
/**
301+
* Gets quota usage of all pools
302+
* @param int $startTime
303+
* @param int $endTime
304+
* @param int $type
305+
* @return array
306+
* @throws Exception
307+
* @throws RuntimeException
308+
*/
309+
public function getPoolsQuotaUsage(int $startTime, int $endTime, int $type): array {
310+
$qb = $this->db->getQueryBuilder();
311+
312+
$qb->select('pool')
313+
->selectAlias($qb->func()->sum('units'), 'usage')
314+
->from($this->getTableName())
315+
->where($qb->expr()->neq('pool', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT)))
316+
->andWhere($qb->expr()->gte('timestamp', $qb->createNamedParameter($startTime, IQueryBuilder::PARAM_INT)))
317+
->andWhere($qb->expr()->lte('timestamp', $qb->createNamedParameter($endTime, IQueryBuilder::PARAM_INT)))
318+
->andWhere($qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT)))
319+
->groupBy('type', 'pool')
320+
->orderBy('usage', 'DESC');
321+
322+
return $qb->executeQuery()->fetchAll();
323+
}
277324
}

lib/Service/OpenAiSettingsService.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ public function getQuotaEnd(): int {
119119
$startDate = new DateTime('2000-01-' . $quotaPeriod['day']);
120120
$months = $startDate->diff($periodEnd)->m + $startDate->diff($periodEnd)->y * 12;
121121
$remainder = $months % $quotaPeriod['length'];
122-
$periodEnd = $periodEnd->add(new DateInterval('P' . $quotaPeriod['length'] - $remainder . 'M'));
122+
if ($remainder != 0) {
123+
$periodEnd = $periodEnd->add(new DateInterval('P' . $quotaPeriod['length'] - $remainder . 'M'));
124+
}
123125
}
124126
}
125127
return $periodEnd->getTimestamp();

lib/Service/QuotaRuleService.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
use OCA\OpenAi\AppInfo\Application;
1212
use OCA\OpenAi\Db\EntityType;
1313
use OCA\OpenAi\Db\QuotaRuleMapper;
14+
use OCA\OpenAi\Db\QuotaUsageMapper;
1415
use OCA\OpenAi\Db\QuotaUserMapper;
1516
use OCP\AppFramework\Db\DoesNotExistException;
1617
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
1718
use OCP\ICacheFactory;
1819
use OCP\IGroupManager;
20+
use OCP\IL10N;
1921
use OCP\IUserManager;
2022
use Psr\Log\LoggerInterface;
2123

@@ -27,6 +29,8 @@ public function __construct(
2729
private IGroupManager $groupManager,
2830
private ICacheFactory $cacheFactory,
2931
private IUserManager $userManager,
32+
private QuotaUsageMapper $quotaUsageMapper,
33+
private IL10N $l10n,
3034
private LoggerInterface $logger,
3135
) {
3236
}
@@ -178,4 +182,29 @@ private function validateEntities(array $entities) {
178182
}
179183
}
180184
}
185+
public function getQuotaUsage(int $startDate, int $endDate, int $type): array {
186+
$data = [[$this->l10n->t('Name'), $this->l10n->t('Usage')]];
187+
$users = $this->quotaUsageMapper->getUsersQuotaUsage($startDate, $endDate, $type);
188+
$pools = $this->quotaUsageMapper->getPoolsQuotaUsage($startDate, $endDate, $type);
189+
$usersIdx = 0;
190+
$poolsIdx = 0;
191+
while ($usersIdx < count($users) && $poolsIdx < count($pools)) {
192+
if ($users[$usersIdx]['usage'] > $pools[$poolsIdx]['usage']) {
193+
$data[] = [$users[$usersIdx]['user_id'], $users[$usersIdx]['usage']];
194+
$usersIdx++;
195+
} else {
196+
$data[] = [$this->l10n->t('Quota pool for rule %d', $pools[$poolsIdx]['pool']), $pools[$poolsIdx]['usage']];
197+
$poolsIdx++;
198+
}
199+
}
200+
while ($usersIdx < count($users)) {
201+
$data[] = [$users[$usersIdx]['user_id'], $users[$usersIdx]['usage']];
202+
$usersIdx++;
203+
}
204+
while ($poolsIdx < count($pools)) {
205+
$data[] = [$this->l10n->t('Quota pool for rule %d', $pools[$poolsIdx]['pool']), $pools[$poolsIdx]['usage']];
206+
$poolsIdx++;
207+
}
208+
return $data;
209+
}
181210
}

lib/Settings/Admin.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ public function getForm(): TemplateResponse {
3333
$adminConfig['basic_password'] = $adminConfig['basic_password'] === '' ? '' : 'dummyPassword';
3434
$isAssistantEnabled = $this->appManager->isEnabledForUser('assistant');
3535
$adminConfig['assistant_enabled'] = $isAssistantEnabled;
36+
$adminConfig['quota_start_date'] = $this->openAiSettingsService->getQuotaStart();
37+
$adminConfig['quota_end_date'] = $this->openAiSettingsService->getQuotaEnd();
3638
$this->initialStateService->provideInitialState('admin-config', $adminConfig);
3739
$rules = $this->quotaRuleService->getRules();
3840
$this->initialStateService->provideInitialState('rules', $rules);

src/components/AdminSettings.vue

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,21 @@
546546
</tr>
547547
</tbody>
548548
</table>
549+
<div class="line-gap">
550+
<NcDateTimePickerNative
551+
v-model="quota_usage.start_date"
552+
:label="t('integration_openai', 'Start date')" />
553+
<NcDateTimePickerNative
554+
v-model="quota_usage.end_date"
555+
:label="t('integration_openai', 'End date')" />
556+
<NcSelect
557+
v-model="quota_usage.quota_type"
558+
:options="quotaTypes"
559+
:input-label="t('integration_openai', 'Quota type')" />
560+
<NcButton :href="downloadQuotaUsageUrl" class="download-button">
561+
{{ t('integration_openai', 'Download quota usage') }}
562+
</NcButton>
563+
</div>
549564
<h2>{{ t('integration_openai', 'Quota Rules') }}</h2>
550565
<QuotaRules :quota-info="quotaInfo" />
551566
</div>
@@ -605,6 +620,7 @@ import NcInputField from '@nextcloud/vue/components/NcInputField'
605620
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
606621
import NcSelect from '@nextcloud/vue/components/NcSelect'
607622
import NcTextField from '@nextcloud/vue/components/NcTextField'
623+
import NcDateTimePickerNative from '@nextcloud/vue/components/NcDateTimePickerNative'
608624
609625
import axios from '@nextcloud/axios'
610626
import { showError, showSuccess } from '@nextcloud/dialogs'
@@ -634,12 +650,14 @@ export default {
634650
NcTextField,
635651
NcInputField,
636652
NcNoteCard,
653+
NcDateTimePickerNative,
637654
QuotaRules,
638655
},
639656
640657
data() {
658+
const state = loadState('integration_openai', 'admin-config')
641659
return {
642-
state: loadState('integration_openai', 'admin-config'),
660+
state,
643661
// to prevent some browsers to fill fields with remembered passwords
644662
readonly: true,
645663
models: null,
@@ -655,10 +673,25 @@ export default {
655673
defaultImageSizeParamHint: t('integration_openai', 'Must be in 256x256 format (default is {default})', { default: '1024x1024' }),
656674
DEFAULT_MODEL_ITEM,
657675
appSettingsAssistantUrl: generateUrl('/settings/apps/integration/assistant'),
676+
quota_usage: {
677+
quota_type: { id: 0, label: '' },
678+
start_date: new Date(state.quota_start_date * 1000),
679+
end_date: new Date(state.quota_end_date * 1000),
680+
},
658681
}
659682
},
660683
661684
computed: {
685+
quotaTypes() {
686+
return (this.quotaInfo ?? []).map((q, idx) => ({ id: idx, label: q.type }))
687+
},
688+
downloadQuotaUsageUrl() {
689+
return generateUrl('/apps/integration_openai/quota/download-usage?type={type}&startDate={startDate}&endDate={endDate}', {
690+
type: this.quota_usage.quota_type?.id,
691+
startDate: this.quota_usage.start_date / 1000,
692+
endDate: this.quota_usage.end_date / 1000,
693+
})
694+
},
662695
modelEndpointUrl() {
663696
if (this.state.url === '') {
664697
return 'https://api.openai.com/v1/models'
@@ -821,6 +854,9 @@ export default {
821854
return axios.get(url)
822855
.then((response) => {
823856
this.quotaInfo = response.data
857+
if (this.quotaInfo.length > 0) {
858+
this.quota_usage.quota_type = { id: 0, label: this.quotaInfo[0].type }
859+
}
824860
})
825861
.catch((error) => {
826862
showError(
@@ -947,8 +983,24 @@ export default {
947983
}
948984
949985
.line {
950-
display: flex;
951986
align-items: center;
987+
}
988+
989+
.line-gap {
990+
gap: 8px;
991+
align-items: normal;
992+
993+
.download-button {
994+
align-self: flex-end;
995+
}
996+
997+
> * {
998+
margin-bottom: 20px;
999+
}
1000+
}
1001+
1002+
.line, .line-gap {
1003+
display: flex;
9521004
margin-top: 12px;
9531005
.icon {
9541006
margin-right: 4px;

0 commit comments

Comments
 (0)