@@ -33,6 +33,7 @@ import { debounce } from '~/common/misc';
3333import { selectFileOrFolder } from '~/common/select-file-or-folder' ;
3434import { useRootLoaderData } from '~/root' ;
3535import { useDeleteProjectRulesetActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.delete-ruleset' ;
36+ import { useRefreshProjectRulesetActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.refresh-ruleset' ;
3637import { useUpdateProjectRulesetActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.update-ruleset' ;
3738import {
3839 useWorkspaceLoaderData ,
@@ -101,6 +102,7 @@ export async function clientLoader({ params }: Route.ClientLoaderArgs) {
101102 // For git, the RepoFileWatcher keeps .spectral.yaml in sync with this record.
102103 const projectLintRuleset = await services . projectLintRuleset . getByParentId ( projectId ) ;
103104 const rulesetContent = projectLintRuleset ?. rulesetContent || '' ;
105+ const rulesetLastCompiledAt = projectLintRuleset ?. modified ?? null ;
104106
105107 let parsedSpec : OpenAPIV3 . Document | undefined ;
106108
@@ -114,6 +116,7 @@ export async function clientLoader({ params }: Route.ClientLoaderArgs) {
114116 isConnectedGitProject,
115117 parsedSpec,
116118 rulesetContent,
119+ rulesetLastCompiledAt,
117120 } ;
118121}
119122
@@ -192,7 +195,7 @@ const Component = ({ params }: Route.ComponentProps) => {
192195
193196 const { isGenerateMockServersWithAIEnabled } = useAIFeatureStatus ( ) ;
194197
195- const { apiSpec, gitSyncRulesetPath, isConnectedGitProject, parsedSpec, rulesetContent } =
198+ const { apiSpec, gitSyncRulesetPath, isConnectedGitProject, parsedSpec, rulesetContent, rulesetLastCompiledAt } =
196199 useLoaderData < typeof clientLoader > ( ) ;
197200
198201 const [ lintMessages , setLintMessages ] = useState < LintMessage [ ] > ( [ ] ) ;
@@ -201,6 +204,8 @@ const Component = ({ params }: Route.ComponentProps) => {
201204 const { submit : updateApiSpec } = useSpecUpdateActionFetcher ( ) ;
202205 const { submit : updateProjectRuleset } = useUpdateProjectRulesetActionFetcher ( ) ;
203206 const { submit : deleteProjectRuleset } = useDeleteProjectRulesetActionFetcher ( ) ;
207+ const { submit : refreshProjectRuleset } = useRefreshProjectRulesetActionFetcher ( ) ;
208+ const [ isRefreshing , setIsRefreshing ] = useState ( false ) ;
204209 const generateRequestCollectionFetcher = useSpecGenerateRequestCollectionActionFetcher ( ) ;
205210 const gitVersion = useGitVCSVersion ( ) ;
206211 const [ isLintPaneOpen , setIsLintPaneOpen ] = useState ( false ) ;
@@ -454,6 +459,25 @@ const Component = ({ params }: Route.ComponentProps) => {
454459 setSelectedRulesetPath ( gitSyncRulesetPath || rulesetWritePath ) ;
455460 } ;
456461
462+ const handleRefreshRuleset = async ( ) => {
463+ if ( ! rulesetContent ) {
464+ return ;
465+ }
466+ setIsRefreshing ( true ) ;
467+ try {
468+ await window . main . refreshCompiledRuleset ( { projectId, rulesetContent } ) ;
469+ refreshProjectRuleset ( { organizationId, projectId } ) ;
470+ editor . current ?. tryToSetOption ( 'lint' , { ...lintOptions } ) ;
471+ } catch ( err ) {
472+ showError ( {
473+ title : 'Refresh Failed' ,
474+ message : `Failed to refresh ruleset: ${ err instanceof Error ? err . message : String ( err ) } ` ,
475+ } ) ;
476+ } finally {
477+ setIsRefreshing ( false ) ;
478+ }
479+ } ;
480+
457481 const handleUnselectSpectralFile = async ( ) => {
458482 showModal ( AskModal , {
459483 title : 'Remove Ruleset File' ,
@@ -1111,22 +1135,49 @@ const Component = ({ params }: Route.ComponentProps) => {
11111135 ) }
11121136 </ span >
11131137 { selectedRulesetPath ? (
1114- < TooltipTrigger delay = { 0 } >
1115- < Button
1116- aria-label = "Remove custom ruleset"
1117- onPress = { handleUnselectSpectralFile }
1118- className = "flex aspect-square h-6 shrink-0 items-center justify-center rounded-xs text-sm text-(--color-font) ring-1 ring-transparent transition-all hover:bg-(--hl-xs) focus:ring-(--hl-md) focus:ring-inset aria-pressed:bg-(--hl-sm)"
1119- >
1120- < Icon icon = "xmark" />
1121- </ Button >
1122- < Tooltip
1123- placement = "top end"
1124- offset = { 8 }
1125- className = "max-h-[85vh] max-w-xs overflow-y-auto rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) px-4 py-2 text-sm text-(--color-font) shadow-lg select-none focus:outline-hidden"
1126- >
1127- < p > Clear custom ruleset and use default OAS ruleset</ p >
1128- </ Tooltip >
1129- </ TooltipTrigger >
1138+ < >
1139+ < TooltipTrigger delay = { 0 } >
1140+ < Button
1141+ aria-label = "Refresh ruleset from remote sources"
1142+ isDisabled = { isRefreshing }
1143+ onPress = { handleRefreshRuleset }
1144+ className = "flex aspect-square h-6 shrink-0 items-center justify-center rounded-xs text-sm text-(--color-font) ring-1 ring-transparent transition-all hover:bg-(--hl-xs) focus:ring-(--hl-md) focus:ring-inset disabled:opacity-50 aria-pressed:bg-(--hl-sm)"
1145+ >
1146+ < Icon
1147+ icon = { isRefreshing ? 'spinner' : 'rotate' }
1148+ className = { isRefreshing ? 'animate-spin' : '' }
1149+ />
1150+ </ Button >
1151+ < Tooltip
1152+ placement = "top end"
1153+ offset = { 8 }
1154+ className = "max-h-[85vh] max-w-xs overflow-y-auto rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) px-4 py-2 text-sm text-(--color-font) shadow-lg select-none focus:outline-hidden"
1155+ >
1156+ < p > Recompile ruleset, including re-fetching any referenced remote entries.</ p >
1157+ { rulesetLastCompiledAt && (
1158+ < p className = "mt-1" >
1159+ { `Last updated ${ new Date ( rulesetLastCompiledAt ) . toLocaleString ( ) } ` } .
1160+ </ p >
1161+ ) }
1162+ </ Tooltip >
1163+ </ TooltipTrigger >
1164+ < TooltipTrigger delay = { 0 } >
1165+ < Button
1166+ aria-label = "Remove custom ruleset"
1167+ onPress = { handleUnselectSpectralFile }
1168+ className = "flex aspect-square h-6 shrink-0 items-center justify-center rounded-xs text-sm text-(--color-font) ring-1 ring-transparent transition-all hover:bg-(--hl-xs) focus:ring-(--hl-md) focus:ring-inset aria-pressed:bg-(--hl-sm)"
1169+ >
1170+ < Icon icon = "xmark" />
1171+ </ Button >
1172+ < Tooltip
1173+ placement = "top end"
1174+ offset = { 8 }
1175+ className = "max-h-[85vh] max-w-xs overflow-y-auto rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) px-4 py-2 text-sm text-(--color-font) shadow-lg select-none focus:outline-hidden"
1176+ >
1177+ < p > Clear custom ruleset and use default OAS ruleset</ p >
1178+ </ Tooltip >
1179+ </ TooltipTrigger >
1180+ </ >
11301181 ) : (
11311182 < Button
11321183 aria-label = "Upload custom ruleset"
0 commit comments