Skip to content
Open
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
25 changes: 25 additions & 0 deletions includes/Logging/AI_Request_Log_Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,31 @@ public function handle_cleanup_old_logs(): void {
$this->cleanup_old_logs();
}

/**
* Deletes logs older than a given number of days.
*
* When $days is 0, all logs are purged. Otherwise only logs with a
* timestamp older than $days days ago are removed, in batches.
*
* @since 1.0.0
*
* @param int $days Number of days to retain. 0 means purge everything.
* @return int Number of logs deleted.
*/
public function delete_logs_older_than( int $days ): int {
if ( $days <= 0 ) {
return $this->purge_all_logs();
}

$deleted = $this->repository->cleanup_by_retention( $days );

if ( $deleted > 0 ) {
$this->repository->invalidate_caches();
}

return $deleted;
}

/**
* Purges all logs from the database.
*
Expand Down
18 changes: 15 additions & 3 deletions includes/Logging/REST/AI_Request_Log_Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ public function register_routes(): void {
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'purge_logs' ),
'permission_callback' => array( $this, 'permissions_check' ),
'args' => array(
'before_days' => array(
'description' => __( 'Delete logs older than this many days. 0 deletes all logs.', 'ai' ),
'type' => 'integer',
'minimum' => 0,
'default' => 0,
),
),
),
)
);
Expand Down Expand Up @@ -215,21 +223,25 @@ public function get_filters( WP_REST_Request $request ): WP_REST_Response {
}

/**
* Purges all logs.
* Deletes logs, optionally limiting to entries older than a given number of days.
*
* When before_days is 0 (the default), all logs are purged. When before_days
* is a positive integer, only logs older than that many days are removed.
*
* @param \WP_REST_Request $request Request.
* @return \WP_REST_Response
*/
public function purge_logs( WP_REST_Request $request ): WP_REST_Response {
$deleted = $this->manager->purge_all_logs();
$before_days = (int) $request->get_param( 'before_days' );
$deleted = $this->manager->delete_logs_older_than( $before_days );

return rest_ensure_response(
array(
'success' => true,
'deleted' => $deleted,
'message' => sprintf(
/* translators: %d: Number of deleted logs. */
__( 'Successfully purged %d log entries.', 'ai' ),
__( 'Successfully deleted %d log entries.', 'ai' ),
$deleted
),
)
Expand Down
87 changes: 62 additions & 25 deletions src/admin/ai-request-logs/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
/**
* WordPress dependencies
*/
import { Button, Card, CardBody, CardHeader } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { Button, Card, CardBody, CardHeader, SelectControl } from '@wordpress/components';

Check failure on line 4 in src/admin/ai-request-logs/components/SettingsPanel.tsx

View workflow job for this annotation

GitHub Actions / Run JavaScript code quality checks

Replace `·Button,·Card,·CardBody,·CardHeader,·SelectControl·` with `⏎↹Button,⏎↹Card,⏎↹CardBody,⏎↹CardHeader,⏎↹SelectControl,⏎`
import { __, sprintf } from '@wordpress/i18n';
import { useRef, useState } from '@wordpress/element';

const DELETE_OPTIONS = [
{ value: '30', label: __( 'Older than 30 days', 'ai' ) },
{ value: '90', label: __( 'Older than 90 days', 'ai' ) },
{ value: '365', label: __( 'Older than 1 year', 'ai' ) },
{ value: '0', label: __( 'All logs', 'ai' ) },
];

interface SettingsPanelProps {
hasLogs: boolean;
onPurgeLogs: () => void;
onDeleteLogs: ( days: number ) => void;
purging: boolean;
}

const SettingsPanel: React.FC< SettingsPanelProps > = ( {
hasLogs,
onPurgeLogs,
onDeleteLogs,
purging,
} ) => {
const [ showPurgeConfirm, setShowPurgeConfirm ] = useState( false );
const focusPurgeButtonRef = useRef< boolean >( false );
const [ showConfirm, setShowConfirm ] = useState( false );
const [ selectedDays, setSelectedDays ] = useState( '30' );
const focusDeleteButtonRef = useRef< boolean >( false );
const focusCancelButtonRef = useRef< boolean >( false );

function focusPurgeButtonOnMount( node: HTMLButtonElement | null ) {
if ( focusPurgeButtonRef.current && node ) {
function focusDeleteButtonOnMount( node: HTMLButtonElement | null ) {
if ( focusDeleteButtonRef.current && node ) {
node.focus();
focusPurgeButtonRef.current = false;
focusDeleteButtonRef.current = false;
}
}

Expand All @@ -34,14 +42,27 @@
}
}

const handlePurge = () => {
if ( showPurgeConfirm ) {
onPurgeLogs();
setShowPurgeConfirm( false );
focusPurgeButtonRef.current = true;
const getConfirmMessage = () => {
const days = parseInt( selectedDays, 10 );
if ( days === 0 ) {
return __( 'Permanently delete all logs? This action cannot be undone.', 'ai' );

Check failure on line 48 in src/admin/ai-request-logs/components/SettingsPanel.tsx

View workflow job for this annotation

GitHub Actions / Run JavaScript code quality checks

Replace `·'Permanently·delete·all·logs?·This·action·cannot·be·undone.',·'ai'·` with `⏎↹↹↹↹'Permanently·delete·all·logs?·This·action·cannot·be·undone.',⏎↹↹↹↹'ai'⏎↹↹↹`
}
const option = DELETE_OPTIONS.find( ( o ) => o.value === selectedDays );
return sprintf(
/* translators: %s: human-readable age threshold, e.g. "older than 30 days". */
__( 'Permanently delete logs %s? This action cannot be undone.', 'ai' ),

Check failure on line 53 in src/admin/ai-request-logs/components/SettingsPanel.tsx

View workflow job for this annotation

GitHub Actions / Run JavaScript code quality checks

Replace `·'Permanently·delete·logs·%s?·This·action·cannot·be·undone.',·'ai'·` with `⏎↹↹↹↹'Permanently·delete·logs·%s?·This·action·cannot·be·undone.',⏎↹↹↹↹'ai'⏎↹↹↹`
option?.label.toLowerCase() ?? ''
);
};

const handleDelete = () => {
if ( showConfirm ) {
onDeleteLogs( parseInt( selectedDays, 10 ) );
setShowConfirm( false );
focusDeleteButtonRef.current = true;
} else {
focusCancelButtonRef.current = true;
setShowPurgeConfirm( true );
setShowConfirm( true );
}
};

Expand All @@ -55,32 +76,46 @@
<h3>{ __( 'Danger Zone', 'ai' ) }</h3>
<p className="description">
{ __(
'Permanently delete all logged requests. This action cannot be undone.',
'Delete log entries by age. This action cannot be undone.',
'ai'
) }
</p>
{ showPurgeConfirm ? (
<SelectControl
label={ __( 'Delete logs', 'ai' ) }
value={ selectedDays }
options={ DELETE_OPTIONS }
onChange={ ( value ) => {
setSelectedDays( value );
setShowConfirm( false );
} }
disabled={ purging || ! hasLogs }
__nextHasNoMarginBottom
/>
<div style={ { marginTop: '8px' } }>
{ showConfirm ? (

Check failure on line 95 in src/admin/ai-request-logs/components/SettingsPanel.tsx

View workflow job for this annotation

GitHub Actions / Run JavaScript code quality checks

Insert `↹`
<div className="ai-request-logs__purge-confirm">

Check failure on line 96 in src/admin/ai-request-logs/components/SettingsPanel.tsx

View workflow job for this annotation

GitHub Actions / Run JavaScript code quality checks

Insert `↹`
<span>{ __( 'Are you sure?', 'ai' ) }</span>
<span>{ getConfirmMessage() }</span>

Check failure on line 97 in src/admin/ai-request-logs/components/SettingsPanel.tsx

View workflow job for this annotation

GitHub Actions / Run JavaScript code quality checks

Insert `↹`
<Button

Check failure on line 98 in src/admin/ai-request-logs/components/SettingsPanel.tsx

View workflow job for this annotation

GitHub Actions / Run JavaScript code quality checks

Insert `↹`
variant="primary"

Check failure on line 99 in src/admin/ai-request-logs/components/SettingsPanel.tsx

View workflow job for this annotation

GitHub Actions / Run JavaScript code quality checks

Insert `↹`
isDestructive

Check failure on line 100 in src/admin/ai-request-logs/components/SettingsPanel.tsx

View workflow job for this annotation

GitHub Actions / Run JavaScript code quality checks

Insert `↹`
onClick={ handlePurge }
onClick={ handleDelete }

Check failure on line 101 in src/admin/ai-request-logs/components/SettingsPanel.tsx

View workflow job for this annotation

GitHub Actions / Run JavaScript code quality checks

Insert `↹`
disabled={ purging }
isBusy={ purging }
accessibleWhenDisabled
__next40pxDefaultSize
>
{ __( 'Yes, Purge All', 'ai' ) }
{ __( 'Yes, Delete', 'ai' ) }
</Button>
<Button
variant="secondary"
ref={ focusCancelButtonOnMount }
onClick={ () => {
setShowPurgeConfirm( false );
focusPurgeButtonRef.current = true;
setShowConfirm( false );
focusDeleteButtonRef.current = true;
} }
disabled={ purging }
accessibleWhenDisabled
__next40pxDefaultSize
>
{ __( 'Cancel', 'ai' ) }
</Button>
Expand All @@ -89,14 +124,16 @@
<Button
variant="secondary"
isDestructive
ref={ focusPurgeButtonOnMount }
onClick={ handlePurge }
ref={ focusDeleteButtonOnMount }
onClick={ handleDelete }
disabled={ purging || ! hasLogs }
accessibleWhenDisabled
__next40pxDefaultSize
>
{ __( 'Purge All Logs', 'ai' ) }
{ __( 'Delete', 'ai' ) }
</Button>
) }
</div>
</div>
</CardBody>
</Card>
Expand Down
21 changes: 10 additions & 11 deletions src/admin/ai-request-logs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,24 +283,23 @@ const App: React.FC = () => {
setLogsQuery( ( previous ) => ( { ...previous, page: 1 } ) );
};

const handlePurge = async () => {
const handleDeleteLogs = async ( days: number ) => {
setPurging( true );

try {
await apiFetch( {
path: settings.rest.routes.logs,
method: 'DELETE',
data: { before_days: days },
} );

setLogs( [] );
setTotal( 0 );
setTotalPages( 1 );
setLogsQuery( ( previous ) => ( {
...previous,
page: 1,
} ) );

showNotice( 'success', __( 'All logs have been purged.', 'ai' ) );
showNotice(
'success',
days > 0
? __( 'Logs have been deleted.', 'ai' )
: __( 'All logs have been purged.', 'ai' )
);
fetchLogs();
fetchSummary( summaryPeriod );
fetchFilters();
} catch ( apiError ) {
Expand Down Expand Up @@ -356,7 +355,7 @@ const App: React.FC = () => {

<SettingsPanel
hasLogs={ total > 0 }
onPurgeLogs={ handlePurge }
onDeleteLogs={ handleDeleteLogs }
purging={ purging }
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ public function test_get_filters_returns_options(): void {
}

/**
* Tests that purge_logs returns a success response structure.
* Tests that purge_logs with before_days=0 deletes all logs.
*
* This test uses TRUNCATE internally via purge_all(), so it must
* run last in the class to avoid breaking the transaction.
Expand All @@ -267,6 +267,64 @@ public function test_purge_logs_returns_success(): void {
$this->assertArrayHasKey( 'message', $data );
}

/**
* Tests that purge_logs with before_days only removes old entries.
*
* @since 1.0.0
*/
public function test_purge_logs_with_before_days_removes_only_old_entries(): void {
global $wpdb;

$admin_id = $this->factory()->user->create( array( 'role' => 'administrator' ) );
wp_set_current_user( $admin_id );

$table = $wpdb->prefix . \WordPress\AI\Logging\AI_Request_Log_Schema::TABLE_NAME;

// Insert a log and backdate it to 60 days ago.
$old_id = $this->insert_log();
$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$table,
array( 'timestamp' => gmdate( 'Y-m-d H:i:s', strtotime( '-60 days' ) ) ),
array( 'log_id' => $old_id ),
array( '%s' ),
array( '%s' )
);

// Insert a recent log.
$this->insert_log();

$request = new WP_REST_Request( 'DELETE', '/ai/v1/logs' );
$request->set_param( 'before_days', 30 );
$response = rest_get_server()->dispatch( $request );

$this->assertSame( 200, $response->get_status() );

$data = $response->get_data();
$this->assertTrue( $data['success'] );
$this->assertSame( 1, $data['deleted'] );

// The recent log must still be present.
$get_request = new WP_REST_Request( 'GET', '/ai/v1/logs' );
$get_response = rest_get_server()->dispatch( $get_request );
$this->assertSame( '1', $get_response->get_headers()['X-WP-Total'] );
}

/**
* Tests that a negative before_days value is rejected.
*
* @since 1.0.0
*/
public function test_purge_logs_rejects_negative_before_days(): void {
$admin_id = $this->factory()->user->create( array( 'role' => 'administrator' ) );
wp_set_current_user( $admin_id );

$request = new WP_REST_Request( 'DELETE', '/ai/v1/logs' );
$request->set_param( 'before_days', -1 );
$response = rest_get_server()->dispatch( $request );

$this->assertSame( 400, $response->get_status() );
}

/**
* Tests that subscribers cannot purge logs.
*
Expand Down
Loading
Loading