From 124a42d7cc5a3cf8688de88299c037befe4727e6 Mon Sep 17 00:00:00 2001 From: Gabe Hamilton Date: Thu, 18 Apr 2024 15:00:37 -0600 Subject: [PATCH 1/5] fix: star count, additional tabs on detail (#773) --- src/Entities/Template/EntityDetails.jsx | 51 +++++++++++-------- src/Entities/Template/EntitySummary.jsx | 2 +- src/Entities/Template/GenericEntityConfig.jsx | 2 +- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/Entities/Template/EntityDetails.jsx b/src/Entities/Template/EntityDetails.jsx index 49581b14..2590f2e9 100644 --- a/src/Entities/Template/EntityDetails.jsx +++ b/src/Entities/Template/EntityDetails.jsx @@ -10,6 +10,7 @@ if (!loadItem) { } const { src, tab, schemaFile, namespace, returnTo } = props; // url params const [accountId, entityType, entityName] = src.split("/") ?? [null, null, null]; +const { additionalTabs } = props; // inline params const summaryComponent = props.summaryComponent ?? "${REPL_ACCOUNT}/widget/Entities.Template.EntitySummary"; const schemaLocation = schemaFile ? schemaFile : `${REPL_ACCOUNT}/widget/Entities.Template.GenericSchema`; @@ -54,7 +55,7 @@ const onLoad = (itemInArray) => { return; } const fetchedItem = itemInArray[0]; - const fullEntity = convertObjectKeysSnakeToPascal(fetchedItem); + const fullEntity = convertObjectKeysSnakeToPascal({ ...fetchedItem, ...fetchedItem?.attributes }); setEntity(fullEntity); }; loadItem(query, "SingleEntity", collection, onLoad); @@ -146,6 +147,32 @@ const listLink = href({ widgetSrc: returnTo, }); +const tabs = () => { + const defaultTabs = [ + { + name: "Properties", + value: "properties", + content: entityProperties(entity), + icon: "ph ph-code", + }, + { + name: editLabel, + value: "edit", + content: ( + + ), + icon: editIcon, + }, + ]; + if (additionalTabs) { + return defaultTabs.concat(additionalTabs(entity)); + } + return defaultTabs; +}; + return ( {returnTo && ( @@ -162,6 +189,8 @@ return ( size: "small", showTags: true, entity, + namespace, + entityType, showActions: true, returnTo, }} @@ -173,25 +202,7 @@ return ( variant: "line", size: "large", defaultValue: "properties", - items: [ - { - name: "Properties", - value: "properties", - content: entityProperties(entity), - icon: "ph ph-code", - }, - { - name: editLabel, - value: "edit", - content: ( - - ), - icon: editIcon, - }, - ], + items: tabs(), }} /> diff --git a/src/Entities/Template/EntitySummary.jsx b/src/Entities/Template/EntitySummary.jsx index afc16f19..15d074ba 100644 --- a/src/Entities/Template/EntitySummary.jsx +++ b/src/Entities/Template/EntitySummary.jsx @@ -253,7 +253,7 @@ return ( ) : ( )}{" "} - {starCount} + {entity.stars} ), }} diff --git a/src/Entities/Template/GenericEntityConfig.jsx b/src/Entities/Template/GenericEntityConfig.jsx index 460d2bbe..94deae03 100644 --- a/src/Entities/Template/GenericEntityConfig.jsx +++ b/src/Entities/Template/GenericEntityConfig.jsx @@ -229,7 +229,7 @@ const defaultRenderTableItem = (rawItem, editFunction) => { ) : ( )}{" "} - {starCount} + {item.stars} ), }} From 4c0e5d1f50d11c79f5af2ccadc65eb57bd9e49ab Mon Sep 17 00:00:00 2001 From: Dmitriy <34593263+shelegdmitriy@users.noreply.github.com> Date: Fri, 19 Apr 2024 19:17:52 +0300 Subject: [PATCH 2/5] chore: update Ankr cta link at d-a page (#775) --- src/NearOrg/DataAvailabilityPage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NearOrg/DataAvailabilityPage.jsx b/src/NearOrg/DataAvailabilityPage.jsx index 3125cdd0..77b2c40d 100644 --- a/src/NearOrg/DataAvailabilityPage.jsx +++ b/src/NearOrg/DataAvailabilityPage.jsx @@ -306,7 +306,7 @@ const web3Teams2 = [ height: "29px", }, { - url: "https://www.ankr.com", + url: "https://www.ankr.com/docs/scaling-services-rollups/data-availability/nearda", name: "Ankr", ipfsImage: ipfsImages.logos.ankr, height: "28px", From 7abce09d68316e6df047dcaf1c150719befad14e Mon Sep 17 00:00:00 2001 From: s-n-park <146785798+s-n-park@users.noreply.github.com> Date: Fri, 19 Apr 2024 09:40:16 -0700 Subject: [PATCH 3/5] Revert da copy --- src/NearOrg/DataAvailabilityPage.jsx | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/NearOrg/DataAvailabilityPage.jsx b/src/NearOrg/DataAvailabilityPage.jsx index 77b2c40d..0cbaafd4 100644 --- a/src/NearOrg/DataAvailabilityPage.jsx +++ b/src/NearOrg/DataAvailabilityPage.jsx @@ -488,10 +488,8 @@ return ( Drastically reduce your costs - Storing rollup calldata on NEAR Protocol is approximately 8000x cheaper than storing the same amount - of data on Ethereum. Harness the power of NEAR's robust blockchain, optimized for speed, - scalability, and cost. Say goodbye to high DA fees and embrace a new era of cost-effective modular - blockchain solutions. + Storing calldata on NEAR Protocol is approximately 8000x cheaper than storing the same amount of + data on Ethereum. @@ -500,9 +498,8 @@ return ( Easily validate proofs - Validate proofs effortlessly with NEAR's trustless off-chain light client. Seamlessly access and - verify the storage of rollup data on-chain. NEAR's developer-friendly interface allows for - convenient validation of blockchain transactions, enhancing transparency and trust in the system. + A trustless off-chain light client for NEAR provides easy access to validate that rollup data was + stored on-chain. @@ -511,9 +508,7 @@ return ( Simple to interact with - NEAR Protocol offers an RPC interface for the effortless retrieval of on-chain data from anywhere. - Accessing transaction data has never been easier. Experience the ease of NEAR's Data Availability - solution and unlock new potential for your Ethereum rollup. + NEAR readily provides an RPC to easily retrieve the on-chain data from anywhere @@ -549,10 +544,7 @@ return ( - Efficiently store state data and commitments on a NEAR contract for your L2 rollup. Whether you're - processing transactions or managing data on the blockchain layer, NEAR's Ethereum-compatible - infrastructure offers unparalleled efficiency through our robust network of nodes. Enhance the - functionality of your rollup and experience the future of modular blockchain technology with NEAR. + Efficiently store state data and commitments on a NEAR contract for your L2 rollup. From 5513c5f405ce927222c5b61ac2a2fd40c490b78e Mon Sep 17 00:00:00 2001 From: Gabe Hamilton Date: Fri, 19 Apr 2024 12:31:17 -0600 Subject: [PATCH 4/5] feat: entity list can be filtered by tags --- src/Entities/Template/EntityList.jsx | 17 ++++++++- src/Entities/Template/Forms/TagsEditor.jsx | 3 +- src/Entities/Template/GenericEntityConfig.jsx | 37 +++++++++++++++++-- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/Entities/Template/EntityList.jsx b/src/Entities/Template/EntityList.jsx index f2cc1abd..e0fedd8e 100644 --- a/src/Entities/Template/EntityList.jsx +++ b/src/Entities/Template/EntityList.jsx @@ -22,6 +22,7 @@ const sortTypes = props.sortTypes ?? [ const [searchKey, setSearchKey] = useState(""); const [sort, setSort] = useState(sortTypes[0].value); +const [tagsFilter, setTagsFilter] = useState(null); const [items, setItems] = useState({ list: [], total: 0 }); const [showCreateModal, setShowCreateModal] = useState(false); const [activeItem, setActiveItem] = useState(null); @@ -48,7 +49,7 @@ const onLoadReset = (newItems, totalItems) => { const loadItemsUseState = (isResetOrPageNumber) => { const loader = isResetOrPageNumber === true ? onLoadReset : onLoad; const offset = isResetOrPageNumber === true ? 0 : items.list.length; - return loadItems(buildQueries(searchKey, sort), queryName, offset, collection, loader); + return loadItems(buildQueries(searchKey, sort, { tags: tagsFilter }), queryName, offset, collection, loader); }; useEffect(() => { if (debounceTimer) clearTimeout(debounceTimer); @@ -66,7 +67,7 @@ useEffect(() => { }, [searchKey]); useEffect(() => { loadItemsUseState(true); -}, [sort]); +}, [sort, tagsFilter]); const Wrapper = styled.div` display: flex; @@ -238,6 +239,18 @@ return ( /> +
+ +
diff --git a/src/Entities/Template/Forms/TagsEditor.jsx b/src/Entities/Template/Forms/TagsEditor.jsx index bcd6367e..85813ee3 100644 --- a/src/Entities/Template/Forms/TagsEditor.jsx +++ b/src/Entities/Template/Forms/TagsEditor.jsx @@ -6,6 +6,7 @@ if (!loadItems) { const { namespace, entityType, value, setValue } = props; const placeholder = props.placeholder ?? "Tags"; +const allowNew = props.allowNew ?? true; const [items, setItems] = useState(null); const [tags, setTags] = useState(value || []); @@ -89,7 +90,7 @@ return ( placeholder={placeholder} selected={tags ?? []} positionFixed - allowNew + allowNew={allowNew} /> ); diff --git a/src/Entities/Template/GenericEntityConfig.jsx b/src/Entities/Template/GenericEntityConfig.jsx index 94deae03..2ac1bf4e 100644 --- a/src/Entities/Template/GenericEntityConfig.jsx +++ b/src/Entities/Template/GenericEntityConfig.jsx @@ -21,12 +21,43 @@ const dataFields = Object.keys(schema).filter((key) => typeof schema[key] === "o const standardFields = ["id", "accountId", "name", "displayName", "logoUrl", "attributes"]; const attributeFields = dataFields.filter((key) => !standardFields.includes(key)); const ns = namespace ? namespace : "default"; -const buildQueries = (searchKey, sort) => { - const queryFilter = searchKey ? `display_name: {_ilike: "%${searchKey}%"}` : ""; +const buildQueries = (searchKey, sort, filters) => { + function arrayInPostgresForm(a) { + if (!a) return a; + try { + const stringArray = JSON.stringify(a); + return stringArray.replaceAll("[", "{").replaceAll("]", "}").replaceAll('"', '\\"'); + } catch (error) { + console.error("Malformed array field - Error parsing JSON", error); + return ""; + } + } + + let queryFilter = searchKey ? `, display_name: {_ilike: "%${searchKey}%"}` : ""; + if (filters) { + Object.keys(filters).forEach((key) => { + const filter = filters[key]; + if (!key || !filter) return; + if (filter) { + switch (key) { + case "tags": + queryFilter += `, tags: {_contains: "${arrayInPostgresForm(filter)}"}`; + break; + case "stars": + queryFilter += `, stars: {_gte: ${filter}}`; + break; + default: + queryFilter += `, ${key}: {_eq: "${filter}"}`; + break; + } + } + }); + } + return ` query ListQuery($offset: Int, $limit: Int) { ${collection}( - where: {namespace: {_eq: "${ns}"}, entity_type: {_eq: "${entityType}"}, ${queryFilter}} + where: {namespace: {_eq: "${ns}"}, entity_type: {_eq: "${entityType}"} ${queryFilter}} order_by: [${sort} ], offset: $offset, limit: $limit) { entity_type From 74034eef63fd01137f80287ee045f0078f576706 Mon Sep 17 00:00:00 2001 From: Gabe Hamilton Date: Fri, 19 Apr 2024 16:29:57 -0600 Subject: [PATCH 5/5] feat: support global tag filtering for entities --- src/Entities/Template/EntityList.jsx | 7 +- src/Entities/Template/Forms/TagCloud.jsx | 157 ++++++++++++++++++ src/Entities/Template/GenericEntityConfig.jsx | 8 +- src/Moderation/Sidebar.jsx | 5 + 4 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 src/Entities/Template/Forms/TagCloud.jsx diff --git a/src/Entities/Template/EntityList.jsx b/src/Entities/Template/EntityList.jsx index e0fedd8e..3e63dd28 100644 --- a/src/Entities/Template/EntityList.jsx +++ b/src/Entities/Template/EntityList.jsx @@ -20,9 +20,13 @@ const sortTypes = props.sortTypes ?? [ { text: "Oldest Updates", value: "{ updated_at: asc }" }, ]; +const initialTagFilter = Storage.get("global-tag-filter"); +if (initialTagFilter) { + Storage.set("global-tag-filter", null); +} const [searchKey, setSearchKey] = useState(""); const [sort, setSort] = useState(sortTypes[0].value); -const [tagsFilter, setTagsFilter] = useState(null); +const [tagsFilter, setTagsFilter] = useState(initialTagFilter); const [items, setItems] = useState({ list: [], total: 0 }); const [showCreateModal, setShowCreateModal] = useState(false); const [activeItem, setActiveItem] = useState(null); @@ -244,6 +248,7 @@ return ( src="${REPL_ACCOUNT}/widget/Entities.Template.Forms.TagsEditor" props={{ placeholder: "Filter by Tag", + value: tagsFilter, setValue: setTagsFilter, namespace: schema.namespace, entityType: schema.entityType, diff --git a/src/Entities/Template/Forms/TagCloud.jsx b/src/Entities/Template/Forms/TagCloud.jsx new file mode 100644 index 00000000..ca193847 --- /dev/null +++ b/src/Entities/Template/Forms/TagCloud.jsx @@ -0,0 +1,157 @@ +const { loadItems, fetchGraphQL } = VM.require("${REPL_ACCOUNT}/widget/Entities.QueryApi.Client"); +if (!loadItems) { + return

Loading modules...

; +} + +const { namespace, entityType, onSelect, limit } = props; +const tagLimit = limit ?? 100; + +const [items, setItems] = useState(null); +const [tags, setTags] = useState(value || []); + +const user = "dataplatform_near"; +const entityIndexer = "entities"; +const tagsTable = "tags"; +const collection = `${user}_${entityIndexer}_${tagsTable}`; + +const buildQueries = () => { + const namespaceClause = namespace ? `namespace: {_eq: "${namespace}"}` : ""; + const entityTypeClause = entityType ? `entity_type: {_eq: "${entityType}"}` : ""; + const whereClause = namespace || entityType ? `where: {${namespaceClause}, ${entityTypeClause}}` : ""; + + return ` +query ListQuery($offset: Int, $limit: Int) { + ${collection}( + ${whereClause} + order_by: {count: desc, tag: asc}, + offset: $offset, limit: $limit) { + namespace + entity_type + tag + count + } + ${collection}_aggregate ( + ${whereClause} + ){ + aggregate { + count + } + } +} +`; +}; +const queryName = "ListQuery"; + +const onLoad = (newItems, totalItems) => { + // no paging, just top tags + setItems({ list: newItems ?? [], total: totalItems }); +}; +loadItems(buildQueries(), queryName, 0, collection, onLoad, tagLimit); +if (items === null) { + return "Loading tags"; +} + +const Wrapper = styled.div` + position: relative; + + ${(p) => + p.scroll + ? ` + ul { + padding-right: 16px; + } + + &::after { + content: ''; + display: block; + height: 100%; + width: 16px; + background: linear-gradient(to right, rgba(255,255,255,0), rgba(255,255,255,1)); + position: absolute; + top: 0; + right: 0; + } + + ul { + flex-wrap: nowrap; + } + ` + : ""} +`; + +const Tags = styled.ul` + display: flex; + flex-wrap: wrap; + list-style: none; + gap: 6px; + overflow: auto; + margin: 0; + padding: 0; + scrollbar-width: none; + -ms-overflow-style: none; + &::-webkit-scrollbar { + display: none; + } +`; + +const Tag = styled.li` + padding: 3px 6px; + border-radius: 6px; + font-size: 12px; + line-height: 12px; + border: 1px solid currentcolor; + border-color: ${(p) => (p.primary ? "currentcolor" : "#E6E8EB")}; + color: ${(p) => (p.primary ? "#26A65A" : "#687076")}; + font-weight: 500; + white-space: nowrap; +`; + +const Count = styled.span` + font-family: var(--font-mono); + color: #687076; + margin-left: 6px; +`; +const TooltipTag = styled.span` + font-family: var(--font-mono); + font-weight: bold; + margin-left: 4px; +`; + +const selectTag = (tag) => { + if (onSelect) { + onSelect(tag); + } +}; +const humanize = (str) => { + if (!str) return ""; + return str.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()); +}; + +return ( +
+

Filter by Tag

+ + + {items && items.list + ? items.list.map((tag, i) => ( + + {humanize(tag.entity_type)}s with Tag {tag.tag} + + ), + trigger: ( + selectTag(tag)}> + {tag.tag} {tag.count} + + ), + }} + /> + )) + : ""} + + +
+); diff --git a/src/Entities/Template/GenericEntityConfig.jsx b/src/Entities/Template/GenericEntityConfig.jsx index 2ac1bf4e..d20c2131 100644 --- a/src/Entities/Template/GenericEntityConfig.jsx +++ b/src/Entities/Template/GenericEntityConfig.jsx @@ -3,7 +3,9 @@ if (!href) { return <>; } -const { namespace, entityType, title, schemaFile, homeLink } = props; +const { namespace, entityType, schemaFile } = props; // data props +const { title, homeLink } = props; // display props + const schemaLocation = schemaFile ? schemaFile : `${REPL_ACCOUNT}/widget/Entities.Template.GenericSchema`; const { genSchema } = VM.require(schemaLocation); if (!genSchema) { @@ -41,7 +43,9 @@ const buildQueries = (searchKey, sort, filters) => { if (filter) { switch (key) { case "tags": - queryFilter += `, tags: {_contains: "${arrayInPostgresForm(filter)}"}`; + if (filter && filter.length > 0) { + queryFilter += `, tags: {_contains: "${arrayInPostgresForm(filter)}"}`; + } break; case "stars": queryFilter += `, stars: {_gte: ${filter}}`; diff --git a/src/Moderation/Sidebar.jsx b/src/Moderation/Sidebar.jsx index f36b8dfb..1a940564 100644 --- a/src/Moderation/Sidebar.jsx +++ b/src/Moderation/Sidebar.jsx @@ -7,6 +7,10 @@ const Wrapper = styled.div` width: 240px; height: 100%; `; +const Section = styled.div` + border-top: 1px solid #eceef0; + padding-top: 24px; +`; const Title = styled.h2` font: var(text-l); @@ -69,5 +73,6 @@ return ( {item.name} ))} + {props.additionalContent &&
{props.additionalContent}
} );