Use standard firstSeed/seedCount fallback for workflows with no name in profile details (#1852)

Fixes #1833 

- Add firstSeed and seedCount to workflow information in profile detail
API endpoint (tests updated accordingly), update name of model used for
limited workflow information to be more accurate
- Fix name display in Crawl Workflows list at bottom of Profile detail
page to be consistent with rest of application

---------

Co-authored-by: Emma Segal-Grossman <hi@emma.cafe>
This commit is contained in:
Tessa Walsh 2024-06-06 14:28:19 -04:00 committed by GitHub
parent a85f9496b0
commit 4edc05d503
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 63 additions and 25 deletions

View File

@ -23,7 +23,7 @@ from .models import (
ConfigRevision, ConfigRevision,
CrawlConfig, CrawlConfig,
CrawlConfigOut, CrawlConfigOut,
CrawlConfigIdNameOut, CrawlConfigProfileOut,
CrawlOut, CrawlOut,
EmptyStr, EmptyStr,
UpdateCrawlConfig, UpdateCrawlConfig,
@ -547,17 +547,24 @@ class CrawlConfigOps:
return configs, total return configs, total
async def get_crawl_config_ids_for_profile( async def get_crawl_config_info_for_profile(
self, profileid: UUID, org: Optional[Organization] = None self, profileid: UUID, org: Organization
): ):
"""Return all crawl configs that are associated with a given profileid""" """Return all crawl configs that are associated with a given profileid"""
query = {"profileid": profileid, "inactive": {"$ne": True}} query = {"profileid": profileid, "inactive": {"$ne": True}}
if org: if org:
query["oid"] = org.id query["oid"] = org.id
cursor = self.crawl_configs.find(query, projection=["_id", "name"]) results = []
results = await cursor.to_list(length=1000)
results = [CrawlConfigIdNameOut.from_dict(res) for res in results] cursor = self.crawl_configs.find(query, projection=["_id"])
workflows = await cursor.to_list(length=1000)
for workflow_dict in workflows:
workflow_out = await self.get_crawl_config_out(
workflow_dict.get("_id"), org
)
results.append(CrawlConfigProfileOut.from_dict(workflow_out.to_dict()))
return results return results
async def get_running_crawl( async def get_running_crawl(

View File

@ -412,10 +412,12 @@ class CrawlConfigOut(CrawlConfigCore, CrawlConfigAdditional):
# ============================================================================ # ============================================================================
class CrawlConfigIdNameOut(BaseMongoModel): class CrawlConfigProfileOut(BaseMongoModel):
"""Crawl Config id and name output only""" """Crawl Config basic info for profiles"""
name: str name: str
firstSeed: str
seedCount: int
# ============================================================================ # ============================================================================
@ -1197,7 +1199,7 @@ class Profile(BaseMongoModel):
class ProfileWithCrawlConfigs(Profile): class ProfileWithCrawlConfigs(Profile):
"""Profile with list of crawlconfigs using this profile""" """Profile with list of crawlconfigs using this profile"""
crawlconfigs: List[CrawlConfigIdNameOut] = [] crawlconfigs: List[CrawlConfigProfileOut] = []
# ============================================================================ # ============================================================================

View File

@ -336,9 +336,7 @@ class ProfileOps:
return Profile.from_dict(res) return Profile.from_dict(res)
async def get_profile_with_configs( async def get_profile_with_configs(self, profileid: UUID, org: Organization):
self, profileid: UUID, org: Optional[Organization] = None
):
"""get profile for api output, with crawlconfigs""" """get profile for api output, with crawlconfigs"""
profile = await self.get_profile(profileid, org) profile = await self.get_profile(profileid, org)
@ -369,16 +367,14 @@ class ProfileOps:
except: except:
return None return None
async def get_crawl_configs_for_profile( async def get_crawl_configs_for_profile(self, profileid: UUID, org: Organization):
self, profileid: UUID, org: Optional[Organization] = None """Get list of crawl configs with basic info for that use a particular profile"""
):
"""Get list of crawl config id, names for that use a particular profile"""
crawlconfig_names = await self.crawlconfigs.get_crawl_config_ids_for_profile( crawlconfig_info = await self.crawlconfigs.get_crawl_config_info_for_profile(
profileid, org profileid, org
) )
return crawlconfig_names return crawlconfig_info
async def delete_profile(self, profileid: UUID, org: Organization): async def delete_profile(self, profileid: UUID, org: Organization):
"""delete profile, if not used in active crawlconfig""" """delete profile, if not used in active crawlconfig"""

View File

@ -238,6 +238,8 @@ def test_get_profile(admin_auth_headers, default_org_id, profile_id, profile_con
assert len(crawl_configs) == 1 assert len(crawl_configs) == 1
assert crawl_configs[0]["id"] == profile_config_id assert crawl_configs[0]["id"] == profile_config_id
assert crawl_configs[0]["name"] == "Profile Test Crawl" assert crawl_configs[0]["name"] == "Profile Test Crawl"
assert crawl_configs[0]["firstSeed"] == "https://webrecorder.net/"
assert crawl_configs[0]["seedCount"] == 1
break break
except: except:
if time.monotonic() - start_time > time_limit: if time.monotonic() - start_time > time_limit:

View File

@ -1,12 +1,12 @@
import { localized, msg, str } from "@lit/localize"; import { localized, msg, str } from "@lit/localize";
import { html, nothing } from "lit"; import { html, nothing, type TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators.js"; import { customElement, property, query, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { when } from "lit/directives/when.js"; import { when } from "lit/directives/when.js";
import { capitalize } from "lodash/fp"; import { capitalize } from "lodash/fp";
import queryString from "query-string"; import queryString from "query-string";
import type { Profile } from "./types"; import type { Profile, ProfileWorkflow } from "./types";
import { TailwindElement } from "@/classes/TailwindElement"; import { TailwindElement } from "@/classes/TailwindElement";
import type { Dialog } from "@/components/ui/dialog"; import type { Dialog } from "@/components/ui/dialog";
@ -339,17 +339,16 @@ export class BrowserProfilesDetail extends TailwindElement {
if (this.profile?.crawlconfigs?.length) { if (this.profile?.crawlconfigs?.length) {
return html`<ul> return html`<ul>
${this.profile.crawlconfigs.map( ${this.profile.crawlconfigs.map(
({ id, name }) => html` (workflow) => html`
<li <li
class="border-x border-b first:rounded-t first:border-t last:rounded-b" class="border-x border-b first:rounded-t first:border-t last:rounded-b"
> >
<a <a
class="block p-2 transition-colors focus-within:bg-neutral-50 hover:bg-neutral-50" class="block p-2 transition-colors focus-within:bg-neutral-50 hover:bg-neutral-50"
href=${`${this.navigate.orgBasePath}/workflows/crawl/${id}`} href=${`${this.navigate.orgBasePath}/workflows/crawl/${workflow.id}`}
@click=${this.navigate.link} @click=${this.navigate.link}
> >
${name || ${this.renderWorkflowName(workflow)}
html`<span class="text-neutral-400">${msg("(no name)")}</span>`}
</a> </a>
</li> </li>
`, `,
@ -362,6 +361,31 @@ export class BrowserProfilesDetail extends TailwindElement {
</div>`; </div>`;
} }
private renderWorkflowName(workflow: ProfileWorkflow) {
if (workflow.name)
return html`<span class="truncate">${workflow.name}</span>`;
if (!workflow.firstSeed)
return html`<span class="truncate font-mono">${workflow.id}</span>
<span class="text-neutral-400">${msg("(no name)")}</span>`;
const remainder = workflow.seedCount - 1;
let nameSuffix: string | TemplateResult<1> = "";
if (remainder) {
if (remainder === 1) {
nameSuffix = html`<span class="ml-2 text-neutral-500"
>${msg(str`+${remainder} URL`)}</span
>`;
} else {
nameSuffix = html`<span class="ml-2 text-neutral-500"
>${msg(str`+${remainder} URLs`)}</span
>`;
}
}
return html`
<span class="primaryUrl truncate">${workflow.firstSeed}</span
>${nameSuffix}
`;
}
private readonly renderVisitedSites = () => { private readonly renderVisitedSites = () => {
return html` return html`
<section class="flex-grow-1 flex flex-col lg:w-[60ch]"> <section class="flex-grow-1 flex flex-col lg:w-[60ch]">

View File

@ -93,6 +93,13 @@ export type ProfileReplica = {
custom?: boolean; custom?: boolean;
}; };
export type ProfileWorkflow = {
id: string;
name: string;
firstSeed: string;
seedCount: number;
};
export type Profile = { export type Profile = {
id: string; id: string;
name: string; name: string;
@ -105,7 +112,7 @@ export type Profile = {
profileId: string; profileId: string;
baseProfileName: string; baseProfileName: string;
oid: string; oid: string;
crawlconfigs?: { id: string; name: string }[]; crawlconfigs?: ProfileWorkflow[];
resource?: { resource?: {
name: string; name: string;
path: string; path: string;