diff --git a/blog/0-streampark-flink-on-k8s.md b/blog/0-streampark-flink-on-k8s.md index 748877590..0b77a2764 100644 --- a/blog/0-streampark-flink-on-k8s.md +++ b/blog/0-streampark-flink-on-k8s.md @@ -2,11 +2,12 @@ slug: streampark-flink-on-k8s title: StreamPark Flink on Kubernetes practice tags: [StreamPark, 生产实践, FlinkSQL, Kubernetes] +description: Wuxin Technology was founded in January 2018. The current main business includes the research and development, design, manufacturing and sales of RELX brand products. With core technologies and capabilities covering the entire industry chain, RELX is committed to providing users with products that are both high quality and safe --- -# StreamPark Flink on Kubernetes practice +Wuxin Technology was founded in January 2018. The current main business includes the research and development, design, manufacturing and sales of RELX brand products. With core technologies and capabilities covering the entire industry chain, RELX is committed to providing users with products that are both high quality and safe. - Wuxin Technology was founded in January 2018. The current main business includes the research and development, design, manufacturing and sales of RELX brand products. With core technologies and capabilities covering the entire industry chain, RELX is committed to providing users with products that are both high quality and safe. + ## **Why Choose Native Kubernetes** diff --git a/blog/1-flink-framework-streampark.md b/blog/1-flink-framework-streampark.md index b232658b2..fbd11b8b0 100644 --- a/blog/1-flink-framework-streampark.md +++ b/blog/1-flink-framework-streampark.md @@ -4,7 +4,11 @@ title: Flink Development Toolkit StreamPark tags: [StreamPark, DataStream, FlinkSQL] --- -
+Although the Hadoop system is widely used today, its architecture is complicated, it has a high maintenance complexity, version upgrades are challenging, and due to departmental reasons, data center scheduling is prolonged. We urgently need to explore agile data platform models. With the current prevalence of cloud-native architecture and the backdrop of lake and warehouse integration, we have decided to use Doris as an offline data warehouse and TiDB (which is already in production) as a real-time data platform. Furthermore, because Doris has ODBC capabilities on MySQL, it can integrate external database resources and uniformly output reports. + +![](/blog/belle/doris.png) + + # 1. Background diff --git a/blog/2-streampark-usercase-chinaunion.md b/blog/2-streampark-usercase-chinaunion.md index 6bef32b16..6f4ea6bfd 100644 --- a/blog/2-streampark-usercase-chinaunion.md +++ b/blog/2-streampark-usercase-chinaunion.md @@ -4,7 +4,7 @@ title: China Union's Flink Real-Time Computing Platform Ops Practice tags: [StreamPark, Production Practice, FlinkSQL] --- -# China Union Flink Real-Time Computing Platform Ops Practices +![](/blog/chinaunion/overall_architecture.png) **Abstract:** This article is compiled from the sharing of Mu Chunjin, the head of China Union Data Science's real-time computing team and Apache StreamPark Committer, at the Flink Forward Asia 2022 platform construction session. The content of this article is mainly divided into four parts: @@ -13,11 +13,10 @@ tags: [StreamPark, Production Practice, FlinkSQL] - Integrated Management Based on StreamPark - Future Planning and Evolution -## **Introduction to the Real-Time Computing Platform Background** - -![](/blog/chinaunion/overall_architecture.png) + +## **Introduction to the Real-Time Computing Platform Background** The image above depicts the overall architecture of the real-time computing platform. At the bottom layer, we have the data sources. Due to some sensitive information, the detailed information of the data sources is not listed. It mainly includes three parts: business databases, user behavior logs, and user location. China Union has a vast number of data sources, with just the business databases comprising tens of thousands of tables. The data is primarily processed through Flink SQL and the DataStream API. The data processing workflow includes real-time parsing of data sources by Flink, real-time computation of rules, and real-time products. Users perform real-time data subscriptions on the visualization subscription platform. They can draw an electronic fence on the map and set some rules, such as where the data comes from, how long it stays inside the fence, etc. They can also filter some features. User information that meets these rules will be pushed in real-time. Next is the real-time security part. If a user connects to a high-risk base station or exhibits abnormal operational behavior, we may suspect fraudulent activity and take actions such as shutting down the phone number, among other things. Additionally, there are some real-time features of users and a real-time big screen display. diff --git a/blog/3-streampark-usercase-bondex-paimon.md b/blog/3-streampark-usercase-bondex-paimon.md index b7eb7f456..eb61a5980 100644 --- a/blog/3-streampark-usercase-bondex-paimon.md +++ b/blog/3-streampark-usercase-bondex-paimon.md @@ -4,8 +4,6 @@ title: 海程邦达基于 Apache Paimon + StreamPark 的流式数仓实践 tags: [StreamPark, 生产实践, paimon, streaming-warehouse] --- -# 海程邦达基于 Apache Paimon + StreamPark 的流式数仓实践 - ![](/blog/bondex/Bondex.png) **导读:**本文主要介绍作为供应链物流服务商海程邦达在数字化转型过程中采用 Paimon + StreamPark 平台实现流式数仓的落地方案。我们以 Apache StreamPark 流批一体平台提供了一个易于上手的生产操作手册,以帮助用户提交 Flink 任务并迅速掌握 Paimon 的使用方法。 @@ -16,6 +14,8 @@ tags: [StreamPark, 生产实践, paimon, streaming-warehouse] - 问题排查分析 - 未来规划 + + ## 01 公司业务情况介绍 海程邦达集团一直专注于供应链物流领域,通过打造优秀的国际化物流平台,为客户提供端到端一站式智慧型供应链物流服务。集团现有员工 2000 余人,年营业额逾 120 亿人民币,网络遍及全球 200 余个港口,在海内外有超 80 家分、子公司,助力中国企业与世界互联互通。 @@ -262,9 +262,9 @@ kubectl create ns streamx 使用 default 账户创建 clusterrolebinding 资源: ```shell -kubectl create secret docker-registry streamparksecret ---docker-server=registry-vpc.cn-zhangjiakou.aliyuncs.com ---docker-username=xxxxxx +kubectl create secret docker-registry streamparksecret +--docker-server=registry-vpc.cn-zhangjiakou.aliyuncs.com +--docker-username=xxxxxx --docker-password=xxxxxx -n streamx``` ``` @@ -283,9 +283,9 @@ kubectl create secret docker-registry streamparksecret 创建 k8s secret 密钥用来拉取 ACR 中的镜像 streamparksecret 为密钥名称 自定义 ```shell -kubectl create secret docker-registry streamparksecret ---docker-server=registry-vpc.cn-zhangjiakou.aliyuncs.com ---docker-username=xxxxxx +kubectl create secret docker-registry streamparksecret +--docker-server=registry-vpc.cn-zhangjiakou.aliyuncs.com +--docker-username=xxxxxx --docker-password=xxxxxx -n streamx ``` @@ -312,13 +312,13 @@ kubectl -f rbac.yaml kubectl -f oss-plugin.yaml ``` -\- 创建 CP&SP 的 PV +\- 创建 CP&SP 的 PV ```shell kubectl -f checkpoints_pv.yaml kubectl -f savepoints_pv.yaml ``` -\- 创建 CP&SP 的 PVC +\- 创建 CP&SP 的 PVC ```shell kubectl -f checkpoints_pvc.yaml kubectl -f savepoints_pvc.yaml @@ -336,7 +336,7 @@ kubectl -f checkpoints_pvc.yaml kubectl -f savepoints_pvc.yaml ```sql SET 'execution.runtime-mode' = 'streaming'; -set 'table.exec.sink.upsert-materialize' = 'none'; +set 'table.exec.sink.upsert-materialize' = 'none'; SET 'sql-client.execution.result-mode' = 'tableau'; -- 创建并使用 FTS Catalog 底层存储方案采用阿里云oss CREATE CATALOG `table_store` WITH ( @@ -368,7 +368,7 @@ Kubernetes Namespace :streamx Kubernetes ClusterId :(任务名自定义即可) #上传到阿里云镜像仓库的基础镜像 -Flink Base Docker Image :registry-vpc.cn-zhangjiakou.aliyuncs.com/xxxxx/flink-table-store:v1.16.0 +Flink Base Docker Image :registry-vpc.cn-zhangjiakou.aliyuncs.com/xxxxx/flink-table-store:v1.16.0 Rest-Service Exposed Type:NodePort @@ -408,7 +408,7 @@ volumes: persistentVolumeClaim: claimName: flink-savepoints-csi-pvc -imagePullSecrets: +imagePullSecrets: - name: streamparksecret ``` @@ -675,7 +675,7 @@ CREATE TABLE IF NOT EXISTS dwm.`dwm_business_order_count` ( `delivery_center_op_name` varchar(200) NOT NULL COMMENT '交付名称', `sum_orderCount` BIGINT NOT NULL COMMENT '订单数', `create_date` timestamp NOT NULL COMMENT '创建时间', -PRIMARY KEY (`l_year`, +PRIMARY KEY (`l_year`, `l_month`, `l_date`, `order_type_name`, @@ -805,12 +805,12 @@ ADS 层聚合表采用 agg sum 会出现 dwd 数据流不产生 update_before, 解决办法: -By specifying 'changelog-producer' = 'full-compaction', -Table Store will compare the results between full compactions and produce the differences as changelog. +By specifying 'changelog-producer' = 'full-compaction', +Table Store will compare the results between full compactions and produce the differences as changelog. The latency of changelog is affected by the frequency of full compactions. -By specifying changelog-producer.compaction-interval table property (default value 30min), -users can define the maximum interval between two full compactions to ensure latency. +By specifying changelog-producer.compaction-interval table property (default value 30min), +users can define the maximum interval between two full compactions to ensure latency. This table property does not affect normal compactions and they may still be performed once in a while by writers to reduce reader costs. 这样能解决上述问题。但是随之而来出现了新的问题。默认 changelog-producer.compaction-interval 是 30min,意味着 上游的改动到 ads 查询要间隔 30min,生产过程中发现将压缩间隔时间改成 1min 或者 2 分钟的情况下,又会出现上述 ADS 层聚合数据不准的情况。 diff --git a/blog/4-streampark-usercase-shunwang.md b/blog/4-streampark-usercase-shunwang.md index 410f162f3..cca5c22f4 100644 --- a/blog/4-streampark-usercase-shunwang.md +++ b/blog/4-streampark-usercase-shunwang.md @@ -4,8 +4,6 @@ title: StreamPark 在顺网科技的大规模生产实践 tags: [StreamPark, 生产实践, FlinkSQL] --- -# StreamPark 在顺网科技的大规模生产实践 - ![](/blog/shunwang/autor.png) **导读:**本文主要介绍顺网科技在使用 Flink 计算引擎中遇到的一些挑战,基于 StreamPark 作为实时数据平台如何来解决这些问题,从而大规模支持公司的业务。 @@ -18,6 +16,8 @@ tags: [StreamPark, 生产实践, FlinkSQL] - 带来的收益 - 未来规划 + + ## **公司业务介绍** 杭州顺网科技股份有限公司成立于 2005 年,秉承科技连接快乐的企业使命,是国内具有影响力的泛娱乐技术服务平台之一。多年来公司始终以产品和技术为驱动,致力于以数字化平台服务为人们创造沉浸式的全场景娱乐体验。 @@ -189,7 +189,7 @@ https://github.com/apache/streampark/issues/2142 -## 带来的收益 +## 带来的收益 我们从 StreamX 1.2.3(StreamPark 前身)开始探索和使用,经过一年多时间的磨合,我们发现 StreamPark 真实解决了 Flink 作业在开发管理和运维上的诸多痛点。 @@ -201,7 +201,7 @@ StreamPark 给顺网科技带来的最大的收益就是降低了 Flink 的使 ![图片](/blog/shunwang/achievements2.png) -## 未 来 规 划 +## 未 来 规 划 顺网科技作为 StreamPark 早期的用户之一,在 1 年期间内一直和社区同学保持交流,参与 StreamPark 的稳定性打磨,我们将生产运维中遇到的 Bug 和新的 Feature 提交给了社区。在未来,我们希望可以在 StreamPark 上管理 Flink 表的元数据信息,基于 Flink 引擎通过多 Catalog 实现跨数据源查询分析功能。目前 StreamPark 正在对接 Flink-SQL-Gateway 能力,这一块在未来对于表元数据的管理和跨数据源查询功能会提供了很大的帮助。 diff --git a/blog/5-streampark-usercase-dustess.md b/blog/5-streampark-usercase-dustess.md index 81fe77522..bec4373ac 100644 --- a/blog/5-streampark-usercase-dustess.md +++ b/blog/5-streampark-usercase-dustess.md @@ -4,8 +4,6 @@ title: StreamPark 在尘锋信息的最佳实践,化繁为简极致体验 tags: [StreamPark, 生产实践, FlinkSQL] --- -# StreamPark 在尘锋信息的最佳实践,化繁为简极致体验 - **摘要:**本文源自 StreamPark 在尘锋信息的生产实践, 作者是资深数据开发工程师Gump。主要内容为: 1. 技术选型 @@ -18,7 +16,9 @@ tags: [StreamPark, 生产实践, FlinkSQL] 目前,尘锋已在全国拥有13个城市中心,覆盖华北、华中、华东、华南、西南五大区域,为超30个行业的10,000+家企业提供数字营销服务。 -## **01 技术选型** + + +## **01 技术选型** 尘锋信息在2021年进入了快速发展时期,随着服务行业和企业客户的增加,实时需求越来越多,落地实时计算平台迫在眉睫。 @@ -26,7 +26,7 @@ tags: [StreamPark, 生产实践, FlinkSQL] - 快:由于业务紧迫,我们需要快速落地规划的技术选型并运用生产 - 稳:满足快的基础上,所选择技术一定要稳定服务业务 -- 新:在以上基础,所选择的技术也尽量的新 +- 新:在以上基础,所选择的技术也尽量的新 - 全:所选择技术能够满足公司快速发展和变化的业务,能够符合团队长期发展目标,能够支持且快速支持二次开发 首先在计算引擎方面:我们选择 Flink,原因如下: @@ -67,9 +67,9 @@ Flink SQL 可以极大提升开发效率和提高 Flink 的普及。StreamPark -Flink SQL 现在虽然足够强大,但使用 Java 和 Scala 等 JVM 语言开发 Flink 任务会更加灵活、定制化更强、便于调优和提升资源利用率。与 SQL 相比 Jar 包提交任务最大的问题是Jar包的上传管理等,没有优秀的工具产品会严重降低开发效率和加大维护成本。 +Flink SQL 现在虽然足够强大,但使用 Java 和 Scala 等 JVM 语言开发 Flink 任务会更加灵活、定制化更强、便于调优和提升资源利用率。与 SQL 相比 Jar 包提交任务最大的问题是Jar包的上传管理等,没有优秀的工具产品会严重降低开发效率和加大维护成本。 -StreamPark 除了支持 Jar 上传,更提供了**在线更新构建**的功能,优雅解决了以上问题: +StreamPark 除了支持 Jar 上传,更提供了**在线更新构建**的功能,优雅解决了以上问题: 1、新建 Project :填写 GitHub/Gitlab(支持企业私服)地址及用户名密码, StreamPark 就能 Pull 和 Build 项目。 @@ -158,7 +158,7 @@ StreamPark 的环境搭建非常简单,跟随官网的搭建教程可以在小 http://www.streamxhub.com/docs/user-guide/deployment ``` -为了快速落地和生产使用,我们选择了稳妥的 On Yarn 资源管理模式(虽然 StreamPark 已经很完善的支持 K8S),且已经有较多公司通过 StreamPark 落地了 K8S 部署方式,大家可以参考: +为了快速落地和生产使用,我们选择了稳妥的 On Yarn 资源管理模式(虽然 StreamPark 已经很完善的支持 K8S),且已经有较多公司通过 StreamPark 落地了 K8S 部署方式,大家可以参考: ``` http://www.streamxhub.com/blog/flink-development-framework-streamx @@ -194,11 +194,11 @@ StreamPark 非常贴心的准备了 Demo SQL 任务,可以直接在刚搭建 StreamingContext = ParameterTool + StreamExecutionEnvironment ``` -- StreamingContext 为 StreamPark 的封装对象 -- ParameterTool 为解析配置文件后的参数对象 +- StreamingContext 为 StreamPark 的封装对象 +- ParameterTool 为解析配置文件后的参数对象 ``` - String value = ParameterTool.get("${user.custom.key}") + String value = ParameterTool.get("${user.custom.key}") ``` - StreamExecutionEnvironment 为 Apache Flink 原生任务上下文 @@ -223,13 +223,13 @@ StreamingContext = ParameterTool + StreamExecutionEnvironment - 计算能力开放:将大数据平台的服务器资源开放业务团队使用 - 解决方案开放:Flink 生态的成熟 Connector、Exactly Once 语义支持,可减少业务团队流处理相关的开发成本及维护成本 -目前 StreamPark 还不支持多业务组功能,多业务组功能会抽象后贡献社区。 +目前 StreamPark 还不支持多业务组功能,多业务组功能会抽象后贡献社区。 -![](/blog/dustess/manager.png) +![](/blog/dustess/manager.png) ![](/blog/dustess/task_retrieval.png) -## **04 未来规划** +## **04 未来规划** ### **01 Flink on K8S** diff --git a/blog/6-streampark-usercase-joyme.md b/blog/6-streampark-usercase-joyme.md index ed4174ced..f983b3a16 100644 --- a/blog/6-streampark-usercase-joyme.md +++ b/blog/6-streampark-usercase-joyme.md @@ -4,8 +4,6 @@ title: StreamPark 在 Joyme 的生产实践 tags: [StreamPark, 生产实践, FlinkSQL] --- -
- **摘要:** 本文带来 StreamPark 在 Joyme 中的生产实践, 作者是 Joyme 的大数据工程师秦基勇, 主要内容为: - 遇见StreamPark @@ -16,6 +14,8 @@ tags: [StreamPark, 生产实践, FlinkSQL] - 社区印象 - 总结 + + ## 1 遇见 StreamPark 遇见 StreamPark 是必然的,基于我们现有的实时作业开发模式,不得不寻找一个开源的平台来支撑我司的实时业务。我们的现状如下: @@ -55,7 +55,7 @@ CREATE TABLE source_table ( 'format.derive-schema' = 'true' ); --- 落地表sink +-- 落地表sink CREATE TABLE sink_table ( `uid` STRING ) WITH ( @@ -92,9 +92,9 @@ SELECT Data.uid FROM source_table; 由于我们的模式部署是 on Yarn,在动态选项配置里配置了 Yarn 的队列名称。也有一些配置了开启增量的 Checkpoint 选项和状态过期时间,基本的这些参数都可以从 Flink 的官网去查询到。之前有一些作业确实经常出现内存溢出的问题,加上增量参数和过期参数以后,作业的运行情况好多了。还有就是 Flink Sql 作业设计到状态这种比较大和逻辑复杂的情况下,我个人感觉还是用 Streaming 代码来实现比较好控制一些。 -- -Dyarn.application.queue= yarn队列名称 -- -Dstate.backend.incremental=true -- -Dtable.exec.state.ttl=过期时间 +- -Dyarn.application.queue= yarn队列名称 +- -Dstate.backend.incremental=true +- -Dtable.exec.state.ttl=过期时间 完成配置以后提交,然后在 application 界面进行部署。 @@ -158,4 +158,4 @@ StreamPark 的监控需要在 setting 模块去配置发送邮件的基本信息 目前我司线上运行 60 个实时作业,Flink sql 与 Custom-code 差不多各一半。后续也会有更多的实时任务进行上线。很多同学都会担心 StreamPark 稳不稳定的问题,就我司根据几个月的生产实践而言,StreamPark 只是一个帮助你开发作业,部署,监控和管理的一个平台。到底稳不稳,还是要看自家的 Hadoop yarn 集群稳不稳定(我们用的onyan模式),其实已经跟 StreamPark关系不大了。还有就是你写的 Flink Sql 或者是代码健不健壮。更多的是这两方面应该是大家要考虑的,这两方面没问题再充分利用 StreamPark 的灵活性才能让作业更好的运行,单从一方面说 StreamPark 稳不稳定,实属偏激。 -以上就是 StreamPark 在乐我无限的全部分享内容,感谢大家看到这里。非常感谢 StreamPark 提供给我们这么优秀的产品,这就是做的利他人之事。从1.0 到 1.2.1 平时遇到那些bug都会被即时的修复,每一个issue都被认真对待。目前我们还是 onyarn的部署模式,重启yarn还是会导致作业的lost状态,重启yarn也不是天天都干的事,关于这个社区也会尽早的会去修复这个问题。相信 StreamPark 会越来越好,未来可期。 \ No newline at end of file +以上就是 StreamPark 在乐我无限的全部分享内容,感谢大家看到这里。非常感谢 StreamPark 提供给我们这么优秀的产品,这就是做的利他人之事。从1.0 到 1.2.1 平时遇到那些bug都会被即时的修复,每一个issue都被认真对待。目前我们还是 onyarn的部署模式,重启yarn还是会导致作业的lost状态,重启yarn也不是天天都干的事,关于这个社区也会尽早的会去修复这个问题。相信 StreamPark 会越来越好,未来可期。 diff --git a/blog/7-streampark-usercase-haibo.md b/blog/7-streampark-usercase-haibo.md index 821cacd3f..6e80ef2c5 100644 --- a/blog/7-streampark-usercase-haibo.md +++ b/blog/7-streampark-usercase-haibo.md @@ -4,7 +4,6 @@ title: StreamPark 一站式计算利器在海博科技的生产实践,助力 tags: [StreamPark, 生产实践, FlinkSQL] --- -# StreamPark 一站式计算利器在海博科技的生产实践,助力智慧城市建设 **摘要:**本文「 StreamPark 一站式计算利器在海博科技的生产实践,助力智慧城市建设 」作者是海博科技大数据架构师王庆焕,主要内容为: @@ -16,6 +15,9 @@ tags: [StreamPark, 生产实践, FlinkSQL] 海博科技是一家行业领先的人工智能物联网产品和解决方案公司。目前在公共安全、智慧城市、智慧制造领域,为全国客户提供包括算法、软件和硬件产品在内的全栈式整体解决方案。 + + + ## **01. 选择 StreamPark** 海博科技自 2020 年开始使用 Flink SQL 汇聚、处理各类实时物联数据。随着各地市智慧城市建设步伐的加快,需要汇聚的各类物联数据的数据种类、数据量也不断增加,导致线上维护的 Flink SQL 任务越来越多,一个专门的能够管理众多 Flink SQL 任务的计算平台成为了迫切的需求。 @@ -52,7 +54,7 @@ StreamPark 在海博主要用于运行实时 Flink SQL任务: 读取 Kafka 上 从2021年10月开始,公司逐渐将 Flink SQL 任务迁移至 StreamPark 平台来集中管理,承载我司实时物联数据的汇聚、计算、预警。 -截至目前,StreamPark 已在多个政府、公安生产环境进行部署,汇聚处理城市实时物联数据、人车抓拍数据。以下是在某市专网部署的 StreamPark 平台截图 : +截至目前,StreamPark 已在多个政府、公安生产环境进行部署,汇聚处理城市实时物联数据、人车抓拍数据。以下是在某市专网部署的 StreamPark 平台截图 : ![](/blog/haibo/application.png) diff --git a/docusaurus.config.js b/docusaurus.config.js index 43f6d3bc2..2ebdb84da 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -76,11 +76,12 @@ const config = { blog: { blogSidebarCount: 15, - postsPerPage: 5, + postsPerPage: 6, showReadingTime: true, + blogSidebarTitle: "近期文章", // Please change this to your repo. editUrl: - 'https://github.com/apache/incubator-streampark-website/edit/dev/', + 'https://github.com/apache/incubator-streampark-website/edit/dev/' }, theme: { customCss: require.resolve('./src/css/custom.css'), diff --git a/i18n/zh-CN/code.json b/i18n/zh-CN/code.json new file mode 100644 index 000000000..e7c2eea5e --- /dev/null +++ b/i18n/zh-CN/code.json @@ -0,0 +1,6 @@ +{ + "theme.blog.newerPost":{ + "message": "最新博客", + "description": "latest blogs heading" + } +} diff --git a/i18n/zh-CN/docusaurus-plugin-content-blog/0-streampark-flink-on-k8s.md b/i18n/zh-CN/docusaurus-plugin-content-blog/0-streampark-flink-on-k8s.md index ccea49e55..4e9096c87 100644 --- a/i18n/zh-CN/docusaurus-plugin-content-blog/0-streampark-flink-on-k8s.md +++ b/i18n/zh-CN/docusaurus-plugin-content-blog/0-streampark-flink-on-k8s.md @@ -4,10 +4,10 @@ title: StreamPark Flink on Kubernetes 实践 tags: [StreamPark, 生产实践, FlinkSQL, Kubernetes] --- -# StreamPark Flink on Kubernetes 实践 - 雾芯科技创立于2018年1月。目前主营业务包括 RELX 悦刻品牌产品的研发、设计、制造及销售。凭借覆盖全产业链的核心技术与能力,RELX 悦刻致力于为用户提供兼具高品质和安全性的产品。 + + ## **为什么选择 Native Kubernetes** Native Kubernetes 具有以下优势: @@ -55,15 +55,15 @@ COPY my-flink-job.jar $FLINK_HOME/usrlib/my-flink-job.jar 5. 使用 Kubectl 命令获取到 Flink 作业的 WebUI 访问地址和 JobId ```shell -kubectl -n flink-cluster get svc +kubectl -n flink-cluster get svc ``` 6. 使用Flink命令停止作业 ```shell -./bin/flink cancel - --target kubernetes-application - -Dkubernetes.cluster-id=my-first-application-cluster +./bin/flink cancel + --target kubernetes-application + -Dkubernetes.cluster-id=my-first-application-cluster -Dkubernetes.namespace=flink-cluster ``` @@ -180,11 +180,11 @@ StreamPark 既支持 Upload Jar 也支持直接编写 Flink SQL 作业, **Flink ## **StreamPark 在雾芯科技的落地实践** -StreamPark 在雾芯科技落地较晚,目前主要用于实时数据集成作业和实时指标计算作业的开发部署,有 Jar 任务也有 Flink SQL 任务,全部使用 Native Kubernetes 部署;数据源有CDC、Kafka 等,Sink 端有 Maxcompute、kafka、Hive 等,以下是公司开发环境StreamPark 平台截图: +StreamPark 在雾芯科技落地较晚,目前主要用于实时数据集成作业和实时指标计算作业的开发部署,有 Jar 任务也有 Flink SQL 任务,全部使用 Native Kubernetes 部署;数据源有CDC、Kafka 等,Sink 端有 Maxcompute、kafka、Hive 等,以下是公司开发环境StreamPark 平台截图: ![](/blog/relx/screenshot.png) -## 遇到的问题 +## 遇到的问题 任何新技术都有探索与踩坑的过程,失败的经验是宝贵的,这里介绍下 StreamPark 在雾芯科技落地过程中踩的一些坑和经验,**这块的内容不仅仅关于 StreamPark 的部分, 相信会带给所有使用 Flink on Kubernetes 的小伙伴一些参考。 @@ -242,7 +242,7 @@ spec: serviceAccount: default containers: - name: flink-main-container - image: + image: imagePullSecrets: - name: regsecret hostAliases: @@ -289,14 +289,14 @@ FROM flink:1.13.6-scala_2.11COPY lib $FLINK_HOME/lib/ **3. 基础镜像制作并推送私有仓库** ```shell -docker login --username=xxxdocker \ +docker login --username=xxxdocker \ build -t udf_flink_1.13.6-scala_2.11:latest \ .docker tag udf_flink_1.13.6-scala_2.11:latest \ k8s-harbor.xxx.com/streamx/udf_flink_1.13.6-scala_2.11:latestdocker \ push k8s-harbor.xxx.com/streamx/udf_flink_1.13.6-scala_2.11:latest ``` -## **未来期待** +## **未来期待** - **StreamPark 对于 Flink 作业 Metric 监控的支持** @@ -304,7 +304,7 @@ StreamPark 如果可以对接 Flink Metric 数据而且可以在 StreamPark 平 - **StreamPark 对于Flink 作业日志持久化的支持** -对于部署到 YARN 的 Flink 来说,如果 Flink 程序挂了,我们可以去 YARN 上看历史日志,但是对于 Kubernetes 来说,如果程序挂了,那么 Kubernetes 的 pod 就消失了,就没法查日志了。所以用户需要借助 Kubernetes 上的工具进行日志持久化,如果 StreamPark 支持 Kubernetes 日志持久化接口就更好了。 +对于部署到 YARN 的 Flink 来说,如果 Flink 程序挂了,我们可以去 YARN 上看历史日志,但是对于 Kubernetes 来说,如果程序挂了,那么 Kubernetes 的 pod 就消失了,就没法查日志了。所以用户需要借助 Kubernetes 上的工具进行日志持久化,如果 StreamPark 支持 Kubernetes 日志持久化接口就更好了。 - **镜像过大的问题改进** diff --git a/i18n/zh-CN/docusaurus-plugin-content-blog/1-flink-framework-streampark.md b/i18n/zh-CN/docusaurus-plugin-content-blog/1-flink-framework-streampark.md index dcd97dd7b..3604cdf9f 100644 --- a/i18n/zh-CN/docusaurus-plugin-content-blog/1-flink-framework-streampark.md +++ b/i18n/zh-CN/docusaurus-plugin-content-blog/1-flink-framework-streampark.md @@ -6,6 +6,12 @@ tags: [StreamPark, DataStream, FlinkSQL]
+Hadoop 体系虽然在目前应用非常广泛,但架构繁琐、运维复杂度过高、版本升级困难,且由于部门原因,数据中台需求排期较长,我们急需探索敏捷性开发的数据平台模式。在目前云原生架构的普及和湖仓一体化的大背景下,我们已经确定了将 Doris 作为离线数据仓库,将 TiDB(目前已经应用于生产)作为实时数据平台,同时因为 Doris 具有 on MySQL 的 ODBC 能力,所以又可以对外部数据库资源进行整合,统一对外输出报表 + +![](/blog/belle/doris.png) + + + # 1. 背景 Hadoop 体系虽然在目前应用非常广泛,但架构繁琐、运维复杂度过高、版本升级困难,且由于部门原因,数据中台需求排期较长,我们急需探索敏捷性开发的数据平台模式。在目前云原生架构的普及和湖仓一体化的大背景下,我们已经确定了将 Doris 作为离线数据仓库,将 TiDB(目前已经应用于生产)作为实时数据平台,同时因为 Doris 具有 on MySQL 的 ODBC 能力,所以又可以对外部数据库资源进行整合,统一对外输出报表 diff --git a/i18n/zh-CN/docusaurus-plugin-content-blog/2-streampark-usercase-chinaunion.md b/i18n/zh-CN/docusaurus-plugin-content-blog/2-streampark-usercase-chinaunion.md index 98e5b3d7c..ca77f9980 100644 --- a/i18n/zh-CN/docusaurus-plugin-content-blog/2-streampark-usercase-chinaunion.md +++ b/i18n/zh-CN/docusaurus-plugin-content-blog/2-streampark-usercase-chinaunion.md @@ -13,6 +13,8 @@ tags: [StreamPark, 生产实践, FlinkSQL] - 基于 StreamPark 一体化管理 - 未来规划与演进 + + ## **实时计算平台背景介绍** ![](/blog/chinaunion/overall_architecture.png) diff --git a/i18n/zh-CN/docusaurus-plugin-content-blog/3-streampark-usercase-bondex-paimon.md b/i18n/zh-CN/docusaurus-plugin-content-blog/3-streampark-usercase-bondex-paimon.md index b7eb7f456..eb61a5980 100644 --- a/i18n/zh-CN/docusaurus-plugin-content-blog/3-streampark-usercase-bondex-paimon.md +++ b/i18n/zh-CN/docusaurus-plugin-content-blog/3-streampark-usercase-bondex-paimon.md @@ -4,8 +4,6 @@ title: 海程邦达基于 Apache Paimon + StreamPark 的流式数仓实践 tags: [StreamPark, 生产实践, paimon, streaming-warehouse] --- -# 海程邦达基于 Apache Paimon + StreamPark 的流式数仓实践 - ![](/blog/bondex/Bondex.png) **导读:**本文主要介绍作为供应链物流服务商海程邦达在数字化转型过程中采用 Paimon + StreamPark 平台实现流式数仓的落地方案。我们以 Apache StreamPark 流批一体平台提供了一个易于上手的生产操作手册,以帮助用户提交 Flink 任务并迅速掌握 Paimon 的使用方法。 @@ -16,6 +14,8 @@ tags: [StreamPark, 生产实践, paimon, streaming-warehouse] - 问题排查分析 - 未来规划 + + ## 01 公司业务情况介绍 海程邦达集团一直专注于供应链物流领域,通过打造优秀的国际化物流平台,为客户提供端到端一站式智慧型供应链物流服务。集团现有员工 2000 余人,年营业额逾 120 亿人民币,网络遍及全球 200 余个港口,在海内外有超 80 家分、子公司,助力中国企业与世界互联互通。 @@ -262,9 +262,9 @@ kubectl create ns streamx 使用 default 账户创建 clusterrolebinding 资源: ```shell -kubectl create secret docker-registry streamparksecret ---docker-server=registry-vpc.cn-zhangjiakou.aliyuncs.com ---docker-username=xxxxxx +kubectl create secret docker-registry streamparksecret +--docker-server=registry-vpc.cn-zhangjiakou.aliyuncs.com +--docker-username=xxxxxx --docker-password=xxxxxx -n streamx``` ``` @@ -283,9 +283,9 @@ kubectl create secret docker-registry streamparksecret 创建 k8s secret 密钥用来拉取 ACR 中的镜像 streamparksecret 为密钥名称 自定义 ```shell -kubectl create secret docker-registry streamparksecret ---docker-server=registry-vpc.cn-zhangjiakou.aliyuncs.com ---docker-username=xxxxxx +kubectl create secret docker-registry streamparksecret +--docker-server=registry-vpc.cn-zhangjiakou.aliyuncs.com +--docker-username=xxxxxx --docker-password=xxxxxx -n streamx ``` @@ -312,13 +312,13 @@ kubectl -f rbac.yaml kubectl -f oss-plugin.yaml ``` -\- 创建 CP&SP 的 PV +\- 创建 CP&SP 的 PV ```shell kubectl -f checkpoints_pv.yaml kubectl -f savepoints_pv.yaml ``` -\- 创建 CP&SP 的 PVC +\- 创建 CP&SP 的 PVC ```shell kubectl -f checkpoints_pvc.yaml kubectl -f savepoints_pvc.yaml @@ -336,7 +336,7 @@ kubectl -f checkpoints_pvc.yaml kubectl -f savepoints_pvc.yaml ```sql SET 'execution.runtime-mode' = 'streaming'; -set 'table.exec.sink.upsert-materialize' = 'none'; +set 'table.exec.sink.upsert-materialize' = 'none'; SET 'sql-client.execution.result-mode' = 'tableau'; -- 创建并使用 FTS Catalog 底层存储方案采用阿里云oss CREATE CATALOG `table_store` WITH ( @@ -368,7 +368,7 @@ Kubernetes Namespace :streamx Kubernetes ClusterId :(任务名自定义即可) #上传到阿里云镜像仓库的基础镜像 -Flink Base Docker Image :registry-vpc.cn-zhangjiakou.aliyuncs.com/xxxxx/flink-table-store:v1.16.0 +Flink Base Docker Image :registry-vpc.cn-zhangjiakou.aliyuncs.com/xxxxx/flink-table-store:v1.16.0 Rest-Service Exposed Type:NodePort @@ -408,7 +408,7 @@ volumes: persistentVolumeClaim: claimName: flink-savepoints-csi-pvc -imagePullSecrets: +imagePullSecrets: - name: streamparksecret ``` @@ -675,7 +675,7 @@ CREATE TABLE IF NOT EXISTS dwm.`dwm_business_order_count` ( `delivery_center_op_name` varchar(200) NOT NULL COMMENT '交付名称', `sum_orderCount` BIGINT NOT NULL COMMENT '订单数', `create_date` timestamp NOT NULL COMMENT '创建时间', -PRIMARY KEY (`l_year`, +PRIMARY KEY (`l_year`, `l_month`, `l_date`, `order_type_name`, @@ -805,12 +805,12 @@ ADS 层聚合表采用 agg sum 会出现 dwd 数据流不产生 update_before, 解决办法: -By specifying 'changelog-producer' = 'full-compaction', -Table Store will compare the results between full compactions and produce the differences as changelog. +By specifying 'changelog-producer' = 'full-compaction', +Table Store will compare the results between full compactions and produce the differences as changelog. The latency of changelog is affected by the frequency of full compactions. -By specifying changelog-producer.compaction-interval table property (default value 30min), -users can define the maximum interval between two full compactions to ensure latency. +By specifying changelog-producer.compaction-interval table property (default value 30min), +users can define the maximum interval between two full compactions to ensure latency. This table property does not affect normal compactions and they may still be performed once in a while by writers to reduce reader costs. 这样能解决上述问题。但是随之而来出现了新的问题。默认 changelog-producer.compaction-interval 是 30min,意味着 上游的改动到 ads 查询要间隔 30min,生产过程中发现将压缩间隔时间改成 1min 或者 2 分钟的情况下,又会出现上述 ADS 层聚合数据不准的情况。 diff --git a/i18n/zh-CN/docusaurus-plugin-content-blog/4-streampark-usercase-shunwang.md b/i18n/zh-CN/docusaurus-plugin-content-blog/4-streampark-usercase-shunwang.md index 410f162f3..776036fb5 100644 --- a/i18n/zh-CN/docusaurus-plugin-content-blog/4-streampark-usercase-shunwang.md +++ b/i18n/zh-CN/docusaurus-plugin-content-blog/4-streampark-usercase-shunwang.md @@ -4,7 +4,6 @@ title: StreamPark 在顺网科技的大规模生产实践 tags: [StreamPark, 生产实践, FlinkSQL] --- -# StreamPark 在顺网科技的大规模生产实践 ![](/blog/shunwang/autor.png) @@ -18,6 +17,8 @@ tags: [StreamPark, 生产实践, FlinkSQL] - 带来的收益 - 未来规划 + + ## **公司业务介绍** 杭州顺网科技股份有限公司成立于 2005 年,秉承科技连接快乐的企业使命,是国内具有影响力的泛娱乐技术服务平台之一。多年来公司始终以产品和技术为驱动,致力于以数字化平台服务为人们创造沉浸式的全场景娱乐体验。 @@ -189,7 +190,7 @@ https://github.com/apache/streampark/issues/2142 -## 带来的收益 +## 带来的收益 我们从 StreamX 1.2.3(StreamPark 前身)开始探索和使用,经过一年多时间的磨合,我们发现 StreamPark 真实解决了 Flink 作业在开发管理和运维上的诸多痛点。 @@ -201,7 +202,7 @@ StreamPark 给顺网科技带来的最大的收益就是降低了 Flink 的使 ![图片](/blog/shunwang/achievements2.png) -## 未 来 规 划 +## 未 来 规 划 顺网科技作为 StreamPark 早期的用户之一,在 1 年期间内一直和社区同学保持交流,参与 StreamPark 的稳定性打磨,我们将生产运维中遇到的 Bug 和新的 Feature 提交给了社区。在未来,我们希望可以在 StreamPark 上管理 Flink 表的元数据信息,基于 Flink 引擎通过多 Catalog 实现跨数据源查询分析功能。目前 StreamPark 正在对接 Flink-SQL-Gateway 能力,这一块在未来对于表元数据的管理和跨数据源查询功能会提供了很大的帮助。 diff --git a/i18n/zh-CN/docusaurus-plugin-content-blog/5-streampark-usercase-dustess.md b/i18n/zh-CN/docusaurus-plugin-content-blog/5-streampark-usercase-dustess.md index 81fe77522..bec4373ac 100644 --- a/i18n/zh-CN/docusaurus-plugin-content-blog/5-streampark-usercase-dustess.md +++ b/i18n/zh-CN/docusaurus-plugin-content-blog/5-streampark-usercase-dustess.md @@ -4,8 +4,6 @@ title: StreamPark 在尘锋信息的最佳实践,化繁为简极致体验 tags: [StreamPark, 生产实践, FlinkSQL] --- -# StreamPark 在尘锋信息的最佳实践,化繁为简极致体验 - **摘要:**本文源自 StreamPark 在尘锋信息的生产实践, 作者是资深数据开发工程师Gump。主要内容为: 1. 技术选型 @@ -18,7 +16,9 @@ tags: [StreamPark, 生产实践, FlinkSQL] 目前,尘锋已在全国拥有13个城市中心,覆盖华北、华中、华东、华南、西南五大区域,为超30个行业的10,000+家企业提供数字营销服务。 -## **01 技术选型** + + +## **01 技术选型** 尘锋信息在2021年进入了快速发展时期,随着服务行业和企业客户的增加,实时需求越来越多,落地实时计算平台迫在眉睫。 @@ -26,7 +26,7 @@ tags: [StreamPark, 生产实践, FlinkSQL] - 快:由于业务紧迫,我们需要快速落地规划的技术选型并运用生产 - 稳:满足快的基础上,所选择技术一定要稳定服务业务 -- 新:在以上基础,所选择的技术也尽量的新 +- 新:在以上基础,所选择的技术也尽量的新 - 全:所选择技术能够满足公司快速发展和变化的业务,能够符合团队长期发展目标,能够支持且快速支持二次开发 首先在计算引擎方面:我们选择 Flink,原因如下: @@ -67,9 +67,9 @@ Flink SQL 可以极大提升开发效率和提高 Flink 的普及。StreamPark -Flink SQL 现在虽然足够强大,但使用 Java 和 Scala 等 JVM 语言开发 Flink 任务会更加灵活、定制化更强、便于调优和提升资源利用率。与 SQL 相比 Jar 包提交任务最大的问题是Jar包的上传管理等,没有优秀的工具产品会严重降低开发效率和加大维护成本。 +Flink SQL 现在虽然足够强大,但使用 Java 和 Scala 等 JVM 语言开发 Flink 任务会更加灵活、定制化更强、便于调优和提升资源利用率。与 SQL 相比 Jar 包提交任务最大的问题是Jar包的上传管理等,没有优秀的工具产品会严重降低开发效率和加大维护成本。 -StreamPark 除了支持 Jar 上传,更提供了**在线更新构建**的功能,优雅解决了以上问题: +StreamPark 除了支持 Jar 上传,更提供了**在线更新构建**的功能,优雅解决了以上问题: 1、新建 Project :填写 GitHub/Gitlab(支持企业私服)地址及用户名密码, StreamPark 就能 Pull 和 Build 项目。 @@ -158,7 +158,7 @@ StreamPark 的环境搭建非常简单,跟随官网的搭建教程可以在小 http://www.streamxhub.com/docs/user-guide/deployment ``` -为了快速落地和生产使用,我们选择了稳妥的 On Yarn 资源管理模式(虽然 StreamPark 已经很完善的支持 K8S),且已经有较多公司通过 StreamPark 落地了 K8S 部署方式,大家可以参考: +为了快速落地和生产使用,我们选择了稳妥的 On Yarn 资源管理模式(虽然 StreamPark 已经很完善的支持 K8S),且已经有较多公司通过 StreamPark 落地了 K8S 部署方式,大家可以参考: ``` http://www.streamxhub.com/blog/flink-development-framework-streamx @@ -194,11 +194,11 @@ StreamPark 非常贴心的准备了 Demo SQL 任务,可以直接在刚搭建 StreamingContext = ParameterTool + StreamExecutionEnvironment ``` -- StreamingContext 为 StreamPark 的封装对象 -- ParameterTool 为解析配置文件后的参数对象 +- StreamingContext 为 StreamPark 的封装对象 +- ParameterTool 为解析配置文件后的参数对象 ``` - String value = ParameterTool.get("${user.custom.key}") + String value = ParameterTool.get("${user.custom.key}") ``` - StreamExecutionEnvironment 为 Apache Flink 原生任务上下文 @@ -223,13 +223,13 @@ StreamingContext = ParameterTool + StreamExecutionEnvironment - 计算能力开放:将大数据平台的服务器资源开放业务团队使用 - 解决方案开放:Flink 生态的成熟 Connector、Exactly Once 语义支持,可减少业务团队流处理相关的开发成本及维护成本 -目前 StreamPark 还不支持多业务组功能,多业务组功能会抽象后贡献社区。 +目前 StreamPark 还不支持多业务组功能,多业务组功能会抽象后贡献社区。 -![](/blog/dustess/manager.png) +![](/blog/dustess/manager.png) ![](/blog/dustess/task_retrieval.png) -## **04 未来规划** +## **04 未来规划** ### **01 Flink on K8S** diff --git a/i18n/zh-CN/docusaurus-plugin-content-blog/6-streampark-usercase-joyme.md b/i18n/zh-CN/docusaurus-plugin-content-blog/6-streampark-usercase-joyme.md index ed4174ced..f983b3a16 100644 --- a/i18n/zh-CN/docusaurus-plugin-content-blog/6-streampark-usercase-joyme.md +++ b/i18n/zh-CN/docusaurus-plugin-content-blog/6-streampark-usercase-joyme.md @@ -4,8 +4,6 @@ title: StreamPark 在 Joyme 的生产实践 tags: [StreamPark, 生产实践, FlinkSQL] --- -
- **摘要:** 本文带来 StreamPark 在 Joyme 中的生产实践, 作者是 Joyme 的大数据工程师秦基勇, 主要内容为: - 遇见StreamPark @@ -16,6 +14,8 @@ tags: [StreamPark, 生产实践, FlinkSQL] - 社区印象 - 总结 + + ## 1 遇见 StreamPark 遇见 StreamPark 是必然的,基于我们现有的实时作业开发模式,不得不寻找一个开源的平台来支撑我司的实时业务。我们的现状如下: @@ -55,7 +55,7 @@ CREATE TABLE source_table ( 'format.derive-schema' = 'true' ); --- 落地表sink +-- 落地表sink CREATE TABLE sink_table ( `uid` STRING ) WITH ( @@ -92,9 +92,9 @@ SELECT Data.uid FROM source_table; 由于我们的模式部署是 on Yarn,在动态选项配置里配置了 Yarn 的队列名称。也有一些配置了开启增量的 Checkpoint 选项和状态过期时间,基本的这些参数都可以从 Flink 的官网去查询到。之前有一些作业确实经常出现内存溢出的问题,加上增量参数和过期参数以后,作业的运行情况好多了。还有就是 Flink Sql 作业设计到状态这种比较大和逻辑复杂的情况下,我个人感觉还是用 Streaming 代码来实现比较好控制一些。 -- -Dyarn.application.queue= yarn队列名称 -- -Dstate.backend.incremental=true -- -Dtable.exec.state.ttl=过期时间 +- -Dyarn.application.queue= yarn队列名称 +- -Dstate.backend.incremental=true +- -Dtable.exec.state.ttl=过期时间 完成配置以后提交,然后在 application 界面进行部署。 @@ -158,4 +158,4 @@ StreamPark 的监控需要在 setting 模块去配置发送邮件的基本信息 目前我司线上运行 60 个实时作业,Flink sql 与 Custom-code 差不多各一半。后续也会有更多的实时任务进行上线。很多同学都会担心 StreamPark 稳不稳定的问题,就我司根据几个月的生产实践而言,StreamPark 只是一个帮助你开发作业,部署,监控和管理的一个平台。到底稳不稳,还是要看自家的 Hadoop yarn 集群稳不稳定(我们用的onyan模式),其实已经跟 StreamPark关系不大了。还有就是你写的 Flink Sql 或者是代码健不健壮。更多的是这两方面应该是大家要考虑的,这两方面没问题再充分利用 StreamPark 的灵活性才能让作业更好的运行,单从一方面说 StreamPark 稳不稳定,实属偏激。 -以上就是 StreamPark 在乐我无限的全部分享内容,感谢大家看到这里。非常感谢 StreamPark 提供给我们这么优秀的产品,这就是做的利他人之事。从1.0 到 1.2.1 平时遇到那些bug都会被即时的修复,每一个issue都被认真对待。目前我们还是 onyarn的部署模式,重启yarn还是会导致作业的lost状态,重启yarn也不是天天都干的事,关于这个社区也会尽早的会去修复这个问题。相信 StreamPark 会越来越好,未来可期。 \ No newline at end of file +以上就是 StreamPark 在乐我无限的全部分享内容,感谢大家看到这里。非常感谢 StreamPark 提供给我们这么优秀的产品,这就是做的利他人之事。从1.0 到 1.2.1 平时遇到那些bug都会被即时的修复,每一个issue都被认真对待。目前我们还是 onyarn的部署模式,重启yarn还是会导致作业的lost状态,重启yarn也不是天天都干的事,关于这个社区也会尽早的会去修复这个问题。相信 StreamPark 会越来越好,未来可期。 diff --git a/i18n/zh-CN/docusaurus-plugin-content-blog/7-streampark-usercase-haibo.md b/i18n/zh-CN/docusaurus-plugin-content-blog/7-streampark-usercase-haibo.md index 821cacd3f..8a5e0041e 100644 --- a/i18n/zh-CN/docusaurus-plugin-content-blog/7-streampark-usercase-haibo.md +++ b/i18n/zh-CN/docusaurus-plugin-content-blog/7-streampark-usercase-haibo.md @@ -4,8 +4,6 @@ title: StreamPark 一站式计算利器在海博科技的生产实践,助力 tags: [StreamPark, 生产实践, FlinkSQL] --- -# StreamPark 一站式计算利器在海博科技的生产实践,助力智慧城市建设 - **摘要:**本文「 StreamPark 一站式计算利器在海博科技的生产实践,助力智慧城市建设 」作者是海博科技大数据架构师王庆焕,主要内容为: 1. 选择 StreamPark @@ -16,6 +14,8 @@ tags: [StreamPark, 生产实践, FlinkSQL] 海博科技是一家行业领先的人工智能物联网产品和解决方案公司。目前在公共安全、智慧城市、智慧制造领域,为全国客户提供包括算法、软件和硬件产品在内的全栈式整体解决方案。 + + ## **01. 选择 StreamPark** 海博科技自 2020 年开始使用 Flink SQL 汇聚、处理各类实时物联数据。随着各地市智慧城市建设步伐的加快,需要汇聚的各类物联数据的数据种类、数据量也不断增加,导致线上维护的 Flink SQL 任务越来越多,一个专门的能够管理众多 Flink SQL 任务的计算平台成为了迫切的需求。 @@ -52,7 +52,7 @@ StreamPark 在海博主要用于运行实时 Flink SQL任务: 读取 Kafka 上 从2021年10月开始,公司逐渐将 Flink SQL 任务迁移至 StreamPark 平台来集中管理,承载我司实时物联数据的汇聚、计算、预警。 -截至目前,StreamPark 已在多个政府、公安生产环境进行部署,汇聚处理城市实时物联数据、人车抓拍数据。以下是在某市专网部署的 StreamPark 平台截图 : +截至目前,StreamPark 已在多个政府、公安生产环境进行部署,汇聚处理城市实时物联数据、人车抓拍数据。以下是在某市专网部署的 StreamPark 平台截图 : ![](/blog/haibo/application.png) diff --git a/package.json b/package.json index 51282403e..9a55b8703 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dependencies": { "@docusaurus/core": "2.4.3", "@docusaurus/plugin-content-docs": "^2.4.3", + "@docusaurus/theme-common": "2.4.3", "@docusaurus/preset-classic": "2.4.3", "@easyops-cn/docusaurus-search-local": "^0.36.0", "@mdx-js/react": "^1.6.22", @@ -26,6 +27,7 @@ "clsx": "^1.2.1", "file-loader": "^6.2.0", "prism-react-renderer": "^1.3.1", + "framer-motion": "^10.13.1", "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.2.0", diff --git a/src/css/custom.css b/src/css/custom.css index 3685f70a2..770fc67db 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -83,15 +83,39 @@ --ifm-pre-color: inherit; --ifm-pre-line-height: 1.45; --ifm-pre-padding: 1rem; - --ifm-container-width-xl: 1200px; + --ifm-container-width-xl: 1320px; --ifm-menu-link-padding-vertical: 0.5rem; --ifm-menu-link-padding-horizontal: 1.25rem; - --default-font: 0.90rem; + --default-font: 0.9rem; --default-line-height: 21px; /* --ifm-navbar-height: 5rem; */ + + /* 卡片背景 */ + --card-background: #ffffff; + + /* 选中未选中 */ + --btn-selected: var(--ifm-color-primary); + --btn-unselected: #cecece; + --ifm-col-width: 100%; } +html[data-theme="dark"] { + /* 卡片背景 */ + --card-background: #0c0c0c; + --content-background-color: #0c0c0c; + --blog-item-background-color: #0c0c0c; + --blog-item-shadow: 0 10px 60px 5px #2c2e40, 0 0 10px 0 #000309; +} html { + --post-title-color: hsl(220deg 79% 58%); + --post-pub-date-color: #8c8c8c; + --post-shadow-color: rgba(20, 85, 182, 0.1); + --post-shadow: 0 0 120px var(--post-shadow-color); + --blog-item-background-color: #fff; + --content-background-color: #fafafa; + + --blog-item-shadow: 0 10px 60px 5px #f1f5f9, 0 0 10px 0 #e4e4e7; + font-variant: tabular-nums; font-feature-settings: "tnum"; } @@ -100,7 +124,17 @@ body { margin: 0; font-size: var(--default-font) !important; line-height: var(--default-line-height); - font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol; + font-family: + -apple-system, + BlinkMacSystemFont, + Segoe UI, + Roboto, + Helvetica, + Arial, + sans-serif, + Apple Color Emoji, + Segoe UI Emoji, + Segoe UI Symbol; } .navbar__title { @@ -374,7 +408,7 @@ footer .subscribe-box ul li a:hover .wechat-dropdown { } .theme-doc-sidebar-menu:not(#\#):not(#\#) { - font-size: 0.90rem; + font-size: 0.9rem; } .theme-doc-sidebar-item-link-level-1:not(:first-child):not(#\#):not(#\#), @@ -477,3 +511,443 @@ p { color: var(--ifm-color-secondary-contrast-foreground); font-weight: 500; } + +/* .blog-wrapper > .container > .row > *:nth-child(3) > div > div:nth-child(1) { + position: static; + top: unset; +} */ + +.footer__copyright a { + color: var(--ifm-color-primary-light); +} + +/* 文档侧栏字体大小 */ +.menu__list-item > .menu__link[tabindex] { + font-size: 0.875rem; + font-weight: normal; + font-family: serif; + letter-spacing: 1.2px; + line-height: 1.8em; +} + +/* 搜索 */ +.navbar__search-input { + outline: none; +} + +@media only screen and (max-width: 996px) { + .blog-wrapper > .container > .row { + display: flex; + } +} + +@media (max-width: 570px) { + .main-wrapper h1, + .markdown > h1 { + font-size: 1.6em; + } + + .markdown > h2 { + font-size: 1.4em; + } + + .markdown > h3 { + font-size: 1.2em; + } + + .blog__section_title { + margin-top: 2em; + } + .post__date-container { + margin-bottom: 1em !important; + } +} + +@media (max-width: 400px) { + .article__footer { + display: grid; + grid-template-columns: 1fr; + justify-items: end; + row-gap: 36px; + } + + .main-wrapper { + max-width: 100vw !important; + overflow: hidden; + } +} + +@keyframes fading { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* 导航收缩相应尺寸调大 */ +@media (max-width: 1100px) { + .navbar > .container, + .navbar > .container-fluid { + padding: 0; + } + .navbar__toggle { + display: inherit; + } + .navbar__item { + display: none; + } + .navbar__search-input { + width: 9rem; + } + .navbar-sidebar { + display: block; + } +} + +/* google adsense */ +.google-auto-placed { + text-align: inherit !important; +} + +ins { + max-height: 100% !important; +} +/* ============================================== 博客配置 ============================================== */ + +html[data-theme="dark"] .bloghome__intro > p { + color: #dfdfdf; +} + +.blog__section_title { + margin: 1em 0 0 0; + /* display: flex; + align-items: center; + justify-content: center; */ + text-align: center; + font-size: calc(1.375rem + 1.5vw); + position: relative; +} + +.blog__section_title svg { + position: absolute; + top: 0; +} + +.newicon { + fill: var(--ifm-color-primary); +} + +.bloghome__posts-list, +.bloghome__posts-card { + animation: fading 0.8s; +} + +/* 切换视图按钮 */ +.bloghome__swith-view { + text-align: center; + margin: 2em 0 1em 0 !important; +} + +.bloghome__swith-view svg { + cursor: pointer; + transition: 0.6s; +} + +.bloghome__switch--selected { + fill: var(--btn-selected); +} + +.bloghome__switch { + fill: var(--btn-unselected); +} + +.bloghome__posts-list { + display: grid; + grid-template-columns: 1fr 1fr; + justify-content: center; + gap: 12px; + padding: 0 0 3em 0; +} + +.post__list-style-ad { + grid-column: 1 / 3; +} + +.post__list-item { + display: grid; + grid-template-columns: max-content 1fr; + grid-template-areas: + "title title" + "tags date"; + column-gap: 2em; + row-gap: 1em; + align-items: center; + padding: 1em 1.2em; + background: var(--blog-item-background-color); + box-shadow: 4px 2px 5px 0 #338bff0d; + border-radius: 6px; +} +[data-theme="dark"] .post__list-item { + box-shadow: none; + border: 1px solid #2c2e40; +} +[data-theme="dark"] hr { + background-color: #2c2e40; +} +.post__list-item .post__list-title { + color: inherit; + font-size: 1em; + text-decoration: none; + transition: 0.6s; + grid-area: title; +} + +.post__list-item .post__list:hover { + color: var(--ifm-color-primary); +} + +.post__list-tags { + grid-area: tags; + overflow-x: auto; + padding: 0.2em 0; +} + +.post__list-tags a { + background: white; + border: 1px solid var(--ifm-color-primary); + color: inherit; +} + +.post__list-date { + grid-area: date; + justify-self: end; + color: var(--ifm-color-emphasis-600); +} + +/* 发布日期 */ +.post__date-container { + display: grid; + justify-items: center; +} + +.post__date { + background: url("/image/circle.svg") no-repeat; + background-size: contain; + background-position: center; + display: grid; + justify-items: center; + align-items: center; + width: 10.5em; + height: 10.5em; + padding-top: 1em; + margin-top: 4em; + grid-auto-rows: auto; +} + +.post__day { + font-size: 4.25em; + line-height: 1em; + font-weight: 900; +} + +.post__year_month { + align-self: start; + color: var(--post-pub-date-color); +} + +.line__decor { + width: 65%; + height: 4px; + background-color: var(--ifm-color-primary); + align-self: end; + margin-bottom: 1.15em; + opacity: 0.25; +} + +.post__tags-container { + white-space: nowrap; + overflow: auto; + padding-bottom: 12px; +} + +.post__tags { + background: var(--ifm-color-primary); + padding: 6px 10px; + border-radius: 6px; + color: #ffffff; +} + +.post__tags:hover { + color: #ffffff; + text-decoration: none; +} + +html[data-theme="dark"] .post__tags { + color: #d4e8fa; + background: #0179fa77; +} + +/* 底部 */ +.footer { + /* margin-top: 4em; */ +} + +.article__footer { + display: flex; + justify-content: flex-end; + align-items: center; +} + +.footer__read_count { + opacity: 0.8; + color: var(--ifm-color-primary); + font-size: 1.8em; +} + +.footer_eye { + fill: var(--ifm-color-primary); +} + +html[data-theme="dark"] .footer__read_count { + color: var(--ifm-color-primary-light); +} + +html[data-theme="dark"] .footer_eye { + fill: var(--ifm-color-primary-light); +} + +.pagination-nav__link { + /* border: none; */ + margin: 2em 0 !important; + background: linear-gradient(90deg, var(--ifm-color-primary-light) 11.3%, var(--ifm-color-primary) 161.54%); + box-shadow: 0px 0px 10px rgb(0 105 165 / 35%); + color: white; + border: none; + height: auto; +} + +.pagination-nav__link:hover { + color: white; +} + +pagination-nav__item:hover .pagination-nav__link { + border: none; +} + +.pagination-nav__item--next > .pagination-nav__link { + background: linear-gradient(90deg, var(--ifm-color-primary) 11.3%, var(--ifm-color-primary-light) 161.54%); +} + +.pagination-nav__sublabel { + color: white; +} + +/* 博客详情页 CSS 覆盖 */ +.blog-wrapper > .container { + width: 100vw; +} + +.blog-wrapper > .container > .row:not([class~="blog-tags__page"]) { + display: grid; + grid-template-areas: + "detail recent" + "detail outline"; + grid-template-rows: auto 1fr; + grid-template-columns: minmax(0, 1fr) 250px; +} + +.blog-wrapper > .container > .row .col[class*="col--"] { + max-width: unset; +} + +/* 近期文章 */ +.blog-wrapper > .container > .row > *:nth-child(1) { + grid-area: recent; + align-self: start; + /* height: 200px; */ +} +.blog-wrapper > .container > .row > *:nth-child(1) > div { + position: static; +} + +.blog-wrapper > .container > .row > *:nth-child(2) { + grid-area: detail; +} +.blog-wrapper > .container > .row > *:nth-child(3) { + grid-area: outline; +} + +.blog-wrapper > .container > .row > *:nth-child(3) > div > div::before { + content: "文章目录"; + display: block; + margin-bottom: 0.5rem; + font-size: var(--ifm-h3-font-size); + color: var(--ifm-heading-color); + font-weight: var(--ifm-heading-font-weight); +} + +@media (max-width: 1000px) { + .blog__section_title { + margin-top: 3em; + } + + .post__date-container { + justify-items: start; + } + + .line__decor { + display: none; + } +} + +/* post list view adjustment */ +@media only screen and (max-width: 700px) { + .bloghome__posts-list { + row-gap: 36px; + grid-template-columns: minmax(0, max-content); + } + .post__list-style-ad { + grid-column: initial; + } +} + +/* 博客目录隐藏后,回复 row 默认样式 */ +@media (max-width: 996px) { + .blog-wrapper > .container > .row:not([class~="blog-tags__page"]) { + display: initial; + } +} + +/* 首页背景 */ +.container-wrapper { + background: var(--content-background-color); +} + +article > p, +article > a, +article > span, +article > li { + line-height: 1.8em; +} +.post-footer { + margin-top: 2em !important; + display: flex; + align-items: center; + justify-content: space-between; + flex-direction: row; +} + +svg:not(:root).svg-inline--fa, +svg:not(:host).svg-inline--fa { + overflow: visible; + box-sizing: content-box; +} + +.svg-inline--fa { + display: var(--fa-display, inline-block); + height: 1em; + overflow: visible; + vertical-align: -0.125em; +} diff --git a/src/pages/home/theme.less b/src/pages/home/theme.less index 3613bffcd..18fd162e4 100644 --- a/src/pages/home/theme.less +++ b/src/pages/home/theme.less @@ -65,10 +65,6 @@ section, overflow: auto !important; } -.overflow-hidden { - overflow: hidden !important; -} - .overflow-visible { overflow: visible !important; } @@ -2568,6 +2564,12 @@ section, } } +@media (min-width: 1440px) { + .container { + max-width: var(--ifm-container-width-xl) + } +} + .row { --bs-gutter-x: 1.5rem; --bs-gutter-y: 0; diff --git a/src/theme/BlogArchivePage/index.tsx b/src/theme/BlogArchivePage/index.tsx new file mode 100644 index 000000000..d72fb8cf0 --- /dev/null +++ b/src/theme/BlogArchivePage/index.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import {translate} from '@docusaurus/Translate'; +import {PageMetadata} from '@docusaurus/theme-common'; +import Layout from '@theme/Layout'; +import type {ArchiveBlogPost, Props} from '@theme/BlogArchivePage'; + +type YearProp = { + year: string; + posts: ArchiveBlogPost[]; +}; + +function Year({year, posts}: YearProp) { + return ( + <> +

{year}

+ + + ); +} + +function YearsSection({years}: {years: YearProp[]}) { + return ( +
+
+
+ {years.map((_props, idx) => ( +
+ +
+ ))} +
+
+
+ ); +} + +function listPostsByYears(blogPosts: readonly ArchiveBlogPost[]): YearProp[] { + const postsByYear = blogPosts.reduceRight((posts, post) => { + const year = post.metadata.date.split('-')[0]!; + const yearPosts = posts.get(year) ?? []; + return posts.set(year, [post, ...yearPosts]); + }, new Map()); + + return Array.from(postsByYear, ([year, posts]) => ({ + year, + posts, + })); +} + +export default function BlogArchive({archive}: Props): JSX.Element { + const title = translate({ + id: 'theme.blog.archive.title', + message: 'Archive', + description: 'The page & hero title of the blog archive page', + }); + const description = translate({ + id: 'theme.blog.archive.description', + message: 'Archive', + description: 'The page & hero description of the blog archive page', + }); + const years = listPostsByYears(archive.blogPosts); + return ( + <> + + +
+
+

{title}

+

{description}

+
+
+
{years.length > 0 && }
+
+ + ); +} diff --git a/src/theme/BlogLayout/index.tsx b/src/theme/BlogLayout/index.tsx new file mode 100644 index 000000000..3c1c5296a --- /dev/null +++ b/src/theme/BlogLayout/index.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import clsx from 'clsx'; +import Layout from '@theme/Layout'; +import BlogSidebar from '@theme/BlogSidebar' +import type { Props } from '@theme/BlogLayout'; + +export default function BlogLayout(props: Props): JSX.Element { + const { sidebar, toc, children, ...layoutProps } = props; + const hasSidebar = sidebar && sidebar.items.length > 0; + return ( + +
+
+
+ +
+ {children} +
+ {toc &&
{toc}
} +
+
+ +
+
+ ); +} diff --git a/src/theme/BlogListPage/img/card.svg b/src/theme/BlogListPage/img/card.svg new file mode 100644 index 000000000..2858167e2 --- /dev/null +++ b/src/theme/BlogListPage/img/card.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/theme/BlogListPage/img/list.svg b/src/theme/BlogListPage/img/list.svg new file mode 100644 index 000000000..c077c769d --- /dev/null +++ b/src/theme/BlogListPage/img/list.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/theme/BlogListPage/index.tsx b/src/theme/BlogListPage/index.tsx new file mode 100644 index 000000000..9eb5eacac --- /dev/null +++ b/src/theme/BlogListPage/index.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import clsx from 'clsx'; + +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import { + PageMetadata, + HtmlClassNameProvider, + ThemeClassNames, +} from '@docusaurus/theme-common'; +import BlogLayout from '@theme/BlogLayout'; +import BlogListPaginator from '@theme/BlogListPaginator'; +import SearchMetadata from '@theme/SearchMetadata'; +import type { Props } from '@theme/BlogListPage'; +import BlogPostItems from '@theme/BlogPostItems'; +import BlogPostLisView from '../BlogPostLisView'; +import Translate from '@docusaurus/Translate'; +import { useViewType } from './useViewType'; +import ListFilter from "./img/list.svg"; +import CardFilter from "./img/card.svg"; + +function BlogListPageMetadata(props: Props): JSX.Element { + const { metadata } = props; + const { + siteConfig: { title: siteTitle }, + } = useDocusaurusContext(); + const { blogDescription, blogTitle, permalink } = metadata; + const isBlogOnlyMode = permalink === '/'; + const title = isBlogOnlyMode ? siteTitle : blogTitle; + return ( + <> + + + + ); +} + + +function BlogHeaderContent() { + return ( +

+ + Latest blog + +   + + + +

+ ) +} +function BlogListPageContent(props: Props): JSX.Element { + const { metadata, items } = props; + const { viewType, toggleViewType } = useViewType(); + const isCardView = viewType === "card"; + const isListView = viewType === "list"; + return ( + + + {/* switch list and card */} +
+ toggleViewType("card")} + className={viewType === "card" ? "bloghome__switch--selected" : "bloghome__switch"} + /> + toggleViewType("list")} + className={viewType === "list" ? "bloghome__switch--selected" : "bloghome__switch"} + /> +
+
+ {isCardView && ( +
+ +
+ )} + + {isListView && ( + + )} + +
+ + + +
+ ); +} + +export default function BlogListPage(props: Props): JSX.Element { + return ( + + + + + ); +} diff --git a/src/theme/BlogListPage/useViewType.ts b/src/theme/BlogListPage/useViewType.ts new file mode 100644 index 000000000..baaae8bd2 --- /dev/null +++ b/src/theme/BlogListPage/useViewType.ts @@ -0,0 +1,19 @@ +import { useCallback, useEffect, useState } from "react"; + +export function useViewType() { + const [viewType, setViewType] = useState("card"); + + useEffect(() => { + setViewType(localStorage.getItem("viewType") || "card"); + }, []); + + const toggleViewType = useCallback((newViewType) => { + setViewType(newViewType); + localStorage.setItem("viewType", newViewType); + }, []); + + return { + viewType, + toggleViewType, + }; +} diff --git a/src/theme/BlogListPaginator/index.tsx b/src/theme/BlogListPaginator/index.tsx new file mode 100644 index 000000000..027394094 --- /dev/null +++ b/src/theme/BlogListPaginator/index.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import Translate, { translate } from '@docusaurus/Translate'; +import PaginatorNavLink from '@theme/PaginatorNavLink'; +import type { Props } from '@theme/BlogListPaginator'; + +export default function BlogListPaginator(props: Props): JSX.Element { + const { metadata } = props; + const { previousPage, nextPage } = metadata; + + return ( + + ); +} diff --git a/src/theme/BlogPostItem/Container/index.tsx b/src/theme/BlogPostItem/Container/index.tsx new file mode 100644 index 000000000..30c2f161b --- /dev/null +++ b/src/theme/BlogPostItem/Container/index.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { useBaseUrlUtils } from '@docusaurus/useBaseUrl'; +//@ts-ignore internal func +import { useBlogPost } from '@docusaurus/theme-common/internal'; +import type { Props } from '@theme/BlogPostItem/Container'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; + +import "./style.less" + +export default function BlogPostItemContainer({ + children, + className, +}: Props): JSX.Element { + const { + frontMatter, + assets, + metadata: { description, date }, + isBlogPostPage + } = useBlogPost(); + const { withBaseUrl } = useBaseUrlUtils(); + const image = assets.image ?? frontMatter.image; + const keywords = frontMatter.keywords ?? []; + const dateObj = new Date(date); + // 当前语言 + const { + i18n: { currentLocale }, + } = useDocusaurusContext(); + + const year = dateObj.getFullYear(); + let month = `${dateObj.getMonth() + 1}`; + const day = dateObj.getDate(); + let dateStr = `${year}年${month}月`; + + if (currentLocale === "en") { + month = dateObj.toLocaleString("default", { month: "long" }); + dateStr = `${month}, ${year}`; + } + + return ( +
+
+ {/* 列表页日期 */} + {!isBlogPostPage && ( +
+
+
{day}
+
{dateStr}
+
+
+
+ )} +
+
+ {description && } + {image && ( + + )} + {keywords.length > 0 && ( + + )} + {children} +
+
+
+
+ ); +} diff --git a/src/theme/BlogPostItem/Container/style.less b/src/theme/BlogPostItem/Container/style.less new file mode 100644 index 000000000..79d1fb547 --- /dev/null +++ b/src/theme/BlogPostItem/Container/style.less @@ -0,0 +1,99 @@ +/* 卡片新拟态特效 */ +.blog-list--box { + margin-top: 0em; + margin-bottom: 7.25em; + line-height: 1.75rem; + + .blog-list--item { + border-radius: 12px; + background: var(--blog-item-background-color); + box-shadow: var(--blog-item-shadow); + padding: 1em 0.5em; + position: relative; + } + + + + @media (max-width: 570px) { + .article__details { + padding: 0; + } + } + + article { + .single-post--date { + color: var(--ifm-color-primary); + font-size: 0.9em; + } + + >header { + >h1 { + font-size: 2em; + + /* color: #2f5c85; */ + @media (max-width: 570px) { + & { + font-size: 1.6em; + text-align: center; + } + } + } + + >h2 { + font-size: 2em; + line-height: 1.5em; + margin-bottom: 20px !important; + + a { + color: var(--ifm-heading-color); + + &:hover { + text-decoration: none; + } + } + + @media (max-width: 570px) { + & { + font-size: 1.7em; + } + } + } + + >div>time { + color: var(--post-pub-date-color); + } + } + + .markdown p, + .markdown ul { + font-family: var(--content-font-family); + } + } + + @media (max-width: 1000px) { + .blog-list--item { + padding-right: 1em; + } + } +} + +.article-bg { + background: var(--blog-item-background-color); + box-shadow: var(--blog-item-shadow); + padding: 1.5em 1em; + position: relative; + border-radius: 5px; +} + + +[data-theme="dark"] { + .article-bg { + box-shadow: none; + } + + .blog-list--item { + box-shadow: none; + border: 1px solid #2c2e40; + } + +} diff --git a/src/theme/BlogPostItem/Content/index.tsx b/src/theme/BlogPostItem/Content/index.tsx new file mode 100644 index 000000000..7842fa067 --- /dev/null +++ b/src/theme/BlogPostItem/Content/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import clsx from 'clsx'; +import {blogPostContainerID} from '@docusaurus/utils-common'; +//@ts-ignore internal func +import {useBlogPost} from '@docusaurus/theme-common/internal'; +import MDXContent from '@theme/MDXContent'; +import type {Props} from '@theme/BlogPostItem/Content'; + +export default function BlogPostItemContent({ + children, + className, +}: Props): JSX.Element { + const {isBlogPostPage} = useBlogPost(); + return ( +
+ {children} +
+ ); +} diff --git a/src/theme/BlogPostItem/Footer/ReadMoreLink/index.tsx b/src/theme/BlogPostItem/Footer/ReadMoreLink/index.tsx new file mode 100644 index 000000000..205c3efd1 --- /dev/null +++ b/src/theme/BlogPostItem/Footer/ReadMoreLink/index.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import Translate, {translate} from '@docusaurus/Translate'; +import Link from '@docusaurus/Link'; +import type {Props} from '@theme/BlogPostItem/Footer/ReadMoreLink'; +import styles from './styles.module.css'; +function ReadMoreLabel() { + return ( + + + Read More + + + ); +} + +export default function BlogPostItemFooterReadMoreLink( + props: Props, +): JSX.Element { + const {blogPostTitle, ...linkProps} = props; + return ( + + + + ); +} diff --git a/src/theme/BlogPostItem/Footer/ReadMoreLink/styles.module.css b/src/theme/BlogPostItem/Footer/ReadMoreLink/styles.module.css new file mode 100644 index 000000000..2b3755d0b --- /dev/null +++ b/src/theme/BlogPostItem/Footer/ReadMoreLink/styles.module.css @@ -0,0 +1,11 @@ +.readMore { + margin-top: 1.4em; + /* background-color: var(--ifm-link-color); + color: white; */ + border-radius: 8px; + color: white; + padding: 0.75em 2em; + background: linear-gradient(90deg, var(--ifm-color-primary) 11.3%, var(--ifm-color-primary-light) 161.54%); + box-shadow: 0px 0px 32px rgba(0, 105, 165, 0.35); + border-radius: 7px; +} diff --git a/src/theme/BlogPostItem/Footer/index.tsx b/src/theme/BlogPostItem/Footer/index.tsx new file mode 100644 index 000000000..6462f0e2f --- /dev/null +++ b/src/theme/BlogPostItem/Footer/index.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import clsx from 'clsx'; +//@ts-ignore internal func +import { useBlogPost } from '@docusaurus/theme-common/internal'; +import EditThisPage from '@theme/EditThisPage'; +import ReadMoreLink from '@theme/BlogPostItem/Footer/ReadMoreLink'; +import TagsListInline from '@theme/TagsListInline'; + +import styles from './styles.module.css'; + +export default function BlogPostItemFooter(): JSX.Element | null { + const { metadata, isBlogPostPage } = useBlogPost(); + const { tags, title, editUrl, hasTruncateMarker } = metadata; + + // A post is truncated if it's in the "list view" and it has a truncate marker + const truncatedPost = !isBlogPostPage && hasTruncateMarker; + + const renderFooter = truncatedPost || editUrl; + + + + if (!renderFooter) { + return null; + } + + const BlogPostPageFooter = () => { + if (!isBlogPostPage) return null + const tagsExists = tags.length > 0; + const footerDom = [] + if (tagsExists) { + footerDom.push( +
+ +
+ ) + } + if (editUrl) { + footerDom.push( +
+ +
+ ) + } + return footerDom + } + + return ( +
+ +
+ {BlogPostPageFooter()} +
+ + + {truncatedPost && ( +
+ +
+ )} +
+ ); +} diff --git a/src/theme/BlogPostItem/Footer/styles.module.css b/src/theme/BlogPostItem/Footer/styles.module.css new file mode 100644 index 000000000..f9272fb53 --- /dev/null +++ b/src/theme/BlogPostItem/Footer/styles.module.css @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +.blogPostFooterDetailsFull { + flex-direction: column; +} diff --git a/src/theme/BlogPostItem/Header/Author/index.tsx b/src/theme/BlogPostItem/Header/Author/index.tsx new file mode 100644 index 000000000..ea35ca41d --- /dev/null +++ b/src/theme/BlogPostItem/Header/Author/index.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import clsx from 'clsx'; +import Link, {type Props as LinkProps} from '@docusaurus/Link'; + +import type {Props} from '@theme/BlogPostItem/Header/Author'; + +function MaybeLink(props: LinkProps): JSX.Element { + if (props.href) { + return ; + } + return <>{props.children}; +} + +export default function BlogPostItemHeaderAuthor({ + author, + className, +}: Props): JSX.Element { + const {name, title, url, imageURL, email} = author; + const link = url || (email && `mailto:${email}`) || undefined; + return ( +
+ {imageURL && ( + + {name} + + )} + + {name && ( + + )} +
+ ); +} diff --git a/src/theme/BlogPostItem/Header/Authors/index.tsx b/src/theme/BlogPostItem/Header/Authors/index.tsx new file mode 100644 index 000000000..8827c8715 --- /dev/null +++ b/src/theme/BlogPostItem/Header/Authors/index.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import clsx from 'clsx'; +//@ts-ignore internal func +import {useBlogPost} from '@docusaurus/theme-common/internal'; +import BlogPostItemHeaderAuthor from '@theme/BlogPostItem/Header/Author'; +import type {Props} from '@theme/BlogPostItem/Header/Authors'; +import styles from './styles.module.css'; + +// Component responsible for the authors layout +export default function BlogPostItemHeaderAuthors({ + className, +}: Props): JSX.Element | null { + const { + metadata: {authors}, + assets, + } = useBlogPost(); + const authorsCount = authors.length; + if (authorsCount === 0) { + return null; + } + const imageOnly = authors.every(({name}) => !name); + return ( +
+ {authors.map((author, idx) => ( +
+ +
+ ))} +
+ ); +} diff --git a/src/theme/BlogPostItem/Header/Authors/styles.module.css b/src/theme/BlogPostItem/Header/Authors/styles.module.css new file mode 100644 index 000000000..b1bd1106f --- /dev/null +++ b/src/theme/BlogPostItem/Header/Authors/styles.module.css @@ -0,0 +1,21 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +.authorCol { + max-width: inherit !important; + flex-grow: 1 !important; +} + +.imageOnlyAuthorRow { + display: flex; + flex-flow: row wrap; +} + +.imageOnlyAuthorCol { + margin-left: 0.3rem; + margin-right: 0.3rem; +} diff --git a/src/theme/BlogPostItem/Header/Info/index.tsx b/src/theme/BlogPostItem/Header/Info/index.tsx new file mode 100644 index 000000000..2e0aea7c3 --- /dev/null +++ b/src/theme/BlogPostItem/Header/Info/index.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import clsx from 'clsx'; +import {translate} from '@docusaurus/Translate'; +import {usePluralForm} from '@docusaurus/theme-common'; +//@ts-ignore internal func +import {useBlogPost} from '@docusaurus/theme-common/internal'; +import type {Props} from '@theme/BlogPostItem/Header/Info'; + +import styles from './styles.module.css'; + +// Very simple pluralization: probably good enough for now +function useReadingTimePlural() { + const {selectMessage} = usePluralForm(); + return (readingTimeFloat: number) => { + const readingTime = Math.ceil(readingTimeFloat); + return selectMessage( + readingTime, + translate( + { + id: 'theme.blog.post.readingTime.plurals', + description: + 'Pluralized label for "{readingTime} min read". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)', + message: 'One min read|{readingTime} min read', + }, + {readingTime}, + ), + ); + }; +} + +function ReadingTime({readingTime}: {readingTime: number}) { + const readingTimePlural = useReadingTimePlural(); + return <>{readingTimePlural(readingTime)}; +} + +function Date({date, formattedDate}: {date: string; formattedDate: string}) { + return ( + + ); +} + +function Spacer() { + return <>{' · '}; +} + +export default function BlogPostItemHeaderInfo({ + className, +}: Props): JSX.Element { + const {metadata,isBlogPostPage} = useBlogPost(); + const {date, formattedDate, readingTime} = metadata; + if(!isBlogPostPage) return null + return ( +
+ + {typeof readingTime !== 'undefined' && ( + <> + + + + )} +
+ ); +} diff --git a/src/theme/BlogPostItem/Header/Info/styles.module.css b/src/theme/BlogPostItem/Header/Info/styles.module.css new file mode 100644 index 000000000..796b0f171 --- /dev/null +++ b/src/theme/BlogPostItem/Header/Info/styles.module.css @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +.container { + font-size: 0.9rem; +} diff --git a/src/theme/BlogPostItem/Header/Tags/index.tsx b/src/theme/BlogPostItem/Header/Tags/index.tsx new file mode 100644 index 000000000..8ea240894 --- /dev/null +++ b/src/theme/BlogPostItem/Header/Tags/index.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +//@ts-ignore internal func +import { useBlogPost } from '@docusaurus/theme-common/internal'; +import Link from '@docusaurus/Link'; + +export default function BlogPostItemHeaderTitle(): JSX.Element { + const { metadata, isBlogPostPage } = useBlogPost(); + + const { tags,hasTruncateMarker } = metadata; + if (isBlogPostPage || tags.length === 0) return null + + return (tags.length > 0 || hasTruncateMarker) && ( +
+ {tags.length > 0 && ( + <> + + {tags + .slice(0, 4) + .map(({ label, permalink: tagPermalink }, index) => ( + 0 ? "margin-horiz--sm" : "margin-right--sm" + }`} + to={tagPermalink} + style={{ fontSize: "0.75em", fontWeight: 500 }} + > + {label} + + ))} + + )} +
+ ); +} diff --git a/src/theme/BlogPostItem/Header/Title/index.tsx b/src/theme/BlogPostItem/Header/Title/index.tsx new file mode 100644 index 000000000..e2cb8c169 --- /dev/null +++ b/src/theme/BlogPostItem/Header/Title/index.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import clsx from 'clsx'; +import Link from '@docusaurus/Link'; +//@ts-ignore internal func +import { useBlogPost } from '@docusaurus/theme-common/internal'; +import type { Props } from '@theme/BlogPostItem/Header/Title'; + +import styles from './styles.module.css'; + +export default function BlogPostItemHeaderTitle({ + className, +}: Props): JSX.Element { + const { metadata, isBlogPostPage } = useBlogPost(); + const { permalink, title } = metadata; + const TitleHeading = isBlogPostPage ? 'h1' : 'h2'; + return ( + + {isBlogPostPage ? ( + title + ) : ( +
+ + {title} + +
+ )} +
+ ); +} diff --git a/src/theme/BlogPostItem/Header/Title/styles.module.css b/src/theme/BlogPostItem/Header/Title/styles.module.css new file mode 100644 index 000000000..02e6c3ddf --- /dev/null +++ b/src/theme/BlogPostItem/Header/Title/styles.module.css @@ -0,0 +1,56 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +.blogPostTitle { + font-size: 3rem; +} +.blogPostTitleLink { + color: var(--ifm-heading-color); + font-weight: 500; + position: relative; + transition: 0.5s; +} +.blogPostTitleLink a { + color: var(--ifm-heading-color); + font-weight: 500; + position: relative; + transition: 0.5s; +} + +.blogPostTitleLink:hover { + color: var(--ifm-color-primary); + text-decoration: none; +} + +.blogPostTitleLink:hover:after { + -webkit-transform: scaleX(1); + transform: scaleX(1); + visibility: visible; +} + +.blogPostTitleLink:after { + background: var(--ifm-color-primary); + bottom: 0; + content: ""; + height: 2px; + left: 0; + position: absolute; + -webkit-transform: scaleX(0); + transform: scaleX(0); + transition: 0.3s linear; + visibility: hidden; + width: 100%; +} + +/** + Blog post title should be smaller on smaller devices +**/ +@media (max-width: 576px) { + .title { + font-size: 2rem; + } +} diff --git a/src/theme/BlogPostItem/Header/index.tsx b/src/theme/BlogPostItem/Header/index.tsx new file mode 100644 index 000000000..38078941f --- /dev/null +++ b/src/theme/BlogPostItem/Header/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import BlogPostItemHeaderTitle from '@theme/BlogPostItem/Header/Title'; +import BlogPostItemHeaderInfo from '@theme/BlogPostItem/Header/Info'; +import BlogPostItemHeaderAuthors from '@theme/BlogPostItem/Header/Authors'; +import BlogPostItemHeaderTags from './Tags'; + +export default function BlogPostItemHeader(): JSX.Element { + return ( +
+ + + + +
+ ); +} diff --git a/src/theme/BlogPostItem/index.tsx b/src/theme/BlogPostItem/index.tsx new file mode 100644 index 000000000..c63fbfc79 --- /dev/null +++ b/src/theme/BlogPostItem/index.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import clsx from 'clsx'; +//@ts-ignore internal func +import { useBlogPost } from '@docusaurus/theme-common/internal'; +import BlogPostItemContainer from '@theme/BlogPostItem/Container'; +import BlogPostItemHeader from '@theme/BlogPostItem/Header'; +import BlogPostItemContent from '@theme/BlogPostItem/Content'; +import BlogPostItemFooter from '@theme/BlogPostItem/Footer'; +import type { Props } from '@theme/BlogPostItem'; + +// apply a bottom margin in list view +function useContainerClassName() { + const { isBlogPostPage } = useBlogPost(); + return !isBlogPostPage ? 'margin-bottom--xl' : undefined; +} + +export default function BlogPostItem({ + children, + className, +}: Props): JSX.Element { + const containerClassName = useContainerClassName(); + return ( + + + {children} + + + ); +} diff --git a/src/theme/BlogPostItems/index.tsx b/src/theme/BlogPostItems/index.tsx new file mode 100644 index 000000000..1a6cf1f8d --- /dev/null +++ b/src/theme/BlogPostItems/index.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +//@ts-ignore internal func +import {BlogPostProvider} from '@docusaurus/theme-common/internal'; +import BlogPostItem from '@theme/BlogPostItem'; +import type {Props} from '@theme/BlogPostItems'; +import { motion, Variants } from 'framer-motion' + +const variants: Variants = { + from: { opacity: 0.01, y: 100 }, + to: i => ({ + opacity: 1, + y: 0, + transition: { + type: 'spring', + damping: 25, + stiffness: 100, + bounce: 0.2, + duration: 0.3, + delay: i * 0.2, + }, + }), +} + +export default function BlogPostItems({ + items, + component: BlogPostItemComponent = BlogPostItem, +}: Props): JSX.Element { + return ( + <> + {items.map(({content: BlogPostContent},i) => ( + + + + + + + + + ))} + + ); +} diff --git a/src/theme/BlogPostLisView/index.tsx b/src/theme/BlogPostLisView/index.tsx new file mode 100644 index 000000000..ce3bd5754 --- /dev/null +++ b/src/theme/BlogPostLisView/index.tsx @@ -0,0 +1,98 @@ +import Link from "@docusaurus/Link"; +import React from "react"; +import { motion } from 'framer-motion' + + +const container = { + hidden: { opacity: 1, scale: 0 }, + visible: { + opacity: 1, + scale: 1, + transition: { + delayChildren: 0.3, + staggerChildren: 0.2, + }, + }, +} + +const item = { + hidden: { y: 20, opacity: 0 }, + visible: { + y: 0, + opacity: 1, + }, +} + + +export default function BlogPostLisView({ items }: any): JSX.Element { + + return ( + + { + items.map(({ content: BlogPostContent }, index) => { + const { metadata: blogMetaData, frontMatter } = BlogPostContent; + const { title } = frontMatter; + const { permalink, date, tags } = blogMetaData; + + const dateObj = new Date(date); + + const year = dateObj.getFullYear(); + let month = ("0" + (dateObj.getMonth() + 1)).slice(-2); + const day = ("0" + dateObj.getDate()).slice(-2); + + return ( + + + + + {title} + + +
+ {tags.length > 0 && + tags + .slice(0, 2) + .map( + ( + { label, permalink: tagPermalink }, + index + ) => ( + + {label} + + ) + )} +
+
+ {year}-{month}-{day} +
+
+
+ ); + }) + } + +
+ ) + +} diff --git a/src/theme/BlogPostPage/Metadata/index.tsx b/src/theme/BlogPostPage/Metadata/index.tsx new file mode 100644 index 000000000..16e8ae885 --- /dev/null +++ b/src/theme/BlogPostPage/Metadata/index.tsx @@ -0,0 +1,44 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import {PageMetadata} from '@docusaurus/theme-common'; +//@ts-ignore internal func +import {useBlogPost} from '@docusaurus/theme-common/internal'; + +export default function BlogPostPageMetadata(): JSX.Element { + const {assets, metadata} = useBlogPost(); + const {title, description, date, tags, authors, frontMatter} = metadata; + + const {keywords} = frontMatter; + const image = assets.image ?? frontMatter.image; + return ( + + + + {authors.some((author) => author.url) && ( + author.url) + .filter(Boolean) + .join(',')} + /> + )} + {tags.length > 0 && ( + tag.label).join(',')} + /> + )} + + ); +} diff --git a/src/theme/BlogPostPage/index.tsx b/src/theme/BlogPostPage/index.tsx new file mode 100644 index 000000000..6a6f1902b --- /dev/null +++ b/src/theme/BlogPostPage/index.tsx @@ -0,0 +1,65 @@ +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import {HtmlClassNameProvider, ThemeClassNames} from '@docusaurus/theme-common'; +//@ts-ignore internal func +import {BlogPostProvider, useBlogPost} from '@docusaurus/theme-common/internal'; +import BlogLayout from '@theme/BlogLayout'; +import BlogPostItem from '@theme/BlogPostItem'; +import BlogPostPaginator from '@theme/BlogPostPaginator'; +import BlogPostPageMetadata from '@theme/BlogPostPage/Metadata'; +import TOC from '@theme/TOC'; +import type {Props} from '@theme/BlogPostPage'; +import type {BlogSidebar} from '@docusaurus/plugin-content-blog'; + +function BlogPostPageContent({ + sidebar, + children, +}: { + sidebar: BlogSidebar; + children: ReactNode; +}): JSX.Element { + const {metadata, toc} = useBlogPost(); + const {nextItem, prevItem, frontMatter} = metadata; + const { + hide_table_of_contents: hideTableOfContents, + toc_min_heading_level: tocMinHeadingLevel, + toc_max_heading_level: tocMaxHeadingLevel, + } = frontMatter; + return ( + 0 ? ( + + ) : undefined + }> + {children} + + {(nextItem || prevItem) && ( + + )} + + ); +} + +export default function BlogPostPage(props: Props): JSX.Element { + const BlogPostContent = props.content; + return ( + + + + + + + + + ); +} diff --git a/src/theme/BlogPostPaginator/index.tsx b/src/theme/BlogPostPaginator/index.tsx new file mode 100644 index 000000000..acf93a008 --- /dev/null +++ b/src/theme/BlogPostPaginator/index.tsx @@ -0,0 +1,51 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import Translate, {translate} from '@docusaurus/Translate'; +import PaginatorNavLink from '@theme/PaginatorNavLink'; +import type {Props} from '@theme/BlogPostPaginator'; + +export default function BlogPostPaginator(props: Props): JSX.Element { + const {nextItem, prevItem} = props; + + return ( + + ); +} diff --git a/src/theme/BlogSidebar/Desktop/index.tsx b/src/theme/BlogSidebar/Desktop/index.tsx new file mode 100644 index 000000000..2e52e2438 --- /dev/null +++ b/src/theme/BlogSidebar/Desktop/index.tsx @@ -0,0 +1,67 @@ +import React from 'react' +import clsx from 'clsx' +import Link from '@docusaurus/Link' +import { translate } from '@docusaurus/Translate' +import type { Props } from '@theme/BlogSidebar/Desktop' +import { motion } from 'framer-motion' +import styles from './styles.module.css' + +export default function BlogSidebarDesktop({ sidebar }: Props): JSX.Element { + + const handleBack = () => { + window.history.back() + } + + return ( + + + + + ) +} diff --git a/src/theme/BlogSidebar/Desktop/styles.module.css b/src/theme/BlogSidebar/Desktop/styles.module.css new file mode 100644 index 000000000..e016d8363 --- /dev/null +++ b/src/theme/BlogSidebar/Desktop/styles.module.css @@ -0,0 +1,80 @@ +.sidebar { + max-height: calc(100vh - (var(--ifm-navbar-height) + 2rem)); + overflow-y: auto; + position: sticky; + top: calc(var(--ifm-navbar-height) + 2rem); +} + +.sidebarItemTitle { + font-size: var(--ifm-h4-font-size); + font-weight: var(--ifm-font-weight-bold); + font-family: var(--ifm-heading-font-family); + color: var(--ifm-text-color); +} + +.sidebarItemTitle:hover { + color: var(--ifm-link-color); + text-decoration: none; +} + +.sidebarItemList { + font-size: 0.8rem; +} + +.sidebarItem { + margin-top: 0.7rem; +} + +.sidebarItemLink { + color: var(--ifm-font-color-base); + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.sidebarItemLink:hover { + text-decoration: none; +} + +.sidebarItemLinkActive { + color: var(--ifm-color-primary) !important; +} + +@media (max-width: 996px) { + .sidebar { + display: none; + } +} + +.backButton { + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 2rem; + text-align: right; + float: right; + transition: all 0.3s ease-in-out; + cursor: pointer; + background-color: #fff; +} +.backButton svg { + width: 16px; + height: 16px; +} + +.backButton:hover { + color: var(--ifm-link-color); + background-color: #f1f5f9; +} + +html[data-theme="dark"] .backButton { + background-color: #1e293b; +} + +html[data-theme="dark"] .backButton:hover { + background-color: #334155; +} diff --git a/src/theme/BlogSidebar/Mobile/index.tsx b/src/theme/BlogSidebar/Mobile/index.tsx new file mode 100644 index 000000000..d2590a92c --- /dev/null +++ b/src/theme/BlogSidebar/Mobile/index.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import Link from '@docusaurus/Link' +import { NavbarSecondaryMenuFiller } from '@docusaurus/theme-common' +import type { Props } from '@theme/BlogSidebar/Mobile' + +function BlogSidebarMobileSecondaryMenu({ sidebar }: Props): JSX.Element { + return ( +
    + {sidebar.items.map(item => ( +
  • + + {item.title} + +
  • + ))} +
+ ) +} + +export default function BlogSidebarMobile(props: Props): JSX.Element { + return ( + + ) +} diff --git a/src/theme/BlogSidebar/index.tsx b/src/theme/BlogSidebar/index.tsx new file mode 100644 index 000000000..766c394fd --- /dev/null +++ b/src/theme/BlogSidebar/index.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { useWindowSize } from '@docusaurus/theme-common' +import BlogSidebarDesktop from '@theme/BlogSidebar/Desktop' +import BlogSidebarMobile from '@theme/BlogSidebar/Mobile' +import type { Props } from '@theme/BlogSidebar' + +export default function BlogSidebar({ sidebar }: Props): JSX.Element | null { + const windowSize = useWindowSize() + if (!sidebar?.items.length) { + return null + } + // Mobile sidebar doesn't need to be server-rendered + if (windowSize === 'mobile') { + return + } + return +} diff --git a/src/theme/BlogTagsListPage/index.tsx b/src/theme/BlogTagsListPage/index.tsx new file mode 100644 index 000000000..847f1da57 --- /dev/null +++ b/src/theme/BlogTagsListPage/index.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import clsx from 'clsx'; +import { + PageMetadata, + HtmlClassNameProvider, + ThemeClassNames, + translateTagsPageTitle, +} from '@docusaurus/theme-common'; +import BlogLayout from '@theme/BlogLayout'; +import TagsListByLetter from '@theme/TagsListByLetter'; +import type {Props} from '@theme/BlogTagsListPage'; +import SearchMetadata from '@theme/SearchMetadata'; + +export default function BlogTagsListPage({tags, sidebar}: Props): JSX.Element { + const title = translateTagsPageTitle(); + return ( + + + + +

{title}

+ +
+
+ ); +} diff --git a/src/theme/BlogTagsPostsPage/index.tsx b/src/theme/BlogTagsPostsPage/index.tsx new file mode 100644 index 000000000..e63986c69 --- /dev/null +++ b/src/theme/BlogTagsPostsPage/index.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import clsx from 'clsx'; +import Translate, {translate} from '@docusaurus/Translate'; +import { + PageMetadata, + HtmlClassNameProvider, + ThemeClassNames, + usePluralForm, +} from '@docusaurus/theme-common'; +import Link from '@docusaurus/Link'; +import BlogLayout from '@theme/BlogLayout'; +import BlogListPaginator from '@theme/BlogListPaginator'; +import SearchMetadata from '@theme/SearchMetadata'; +import type {Props} from '@theme/BlogTagsPostsPage'; +import BlogPostItems from '@theme/BlogPostItems'; + +// Very simple pluralization: probably good enough for now +function useBlogPostsPlural() { + const {selectMessage} = usePluralForm(); + return (count: number) => + selectMessage( + count, + translate( + { + id: 'theme.blog.post.plurals', + description: + 'Pluralized label for "{count} posts". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)', + message: 'One post|{count} posts', + }, + {count}, + ), + ); +} + +function useBlogTagsPostsPageTitle(tag: Props['tag']): string { + const blogPostsPlural = useBlogPostsPlural(); + return translate( + { + id: 'theme.blog.tagTitle', + description: 'The title of the page for a blog tag', + message: '{nPosts} tagged with "{tagName}"', + }, + {nPosts: blogPostsPlural(tag.count), tagName: tag.label}, + ); +} + +function BlogTagsPostsPageMetadata({tag}: Props): JSX.Element { + const title = useBlogTagsPostsPageTitle(tag); + return ( + <> + + + + ); +} + +function BlogTagsPostsPageContent({ + tag, + items, + sidebar, + listMetadata, +}: Props): JSX.Element { + const title = useBlogTagsPostsPageTitle(tag); + return ( + +
+

{title}

+ + + + View All Tags + + +
+ + +
+ ); +} +export default function BlogTagsPostsPage(props: Props): JSX.Element { + return ( + + + + + ); +} diff --git a/src/theme/TOC/index.tsx b/src/theme/TOC/index.tsx new file mode 100644 index 000000000..f4c9d7731 --- /dev/null +++ b/src/theme/TOC/index.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import clsx from 'clsx'; +import TOCItems from '@theme/TOCItems'; +import type {Props} from '@theme/TOC'; + +import styles from './styles.module.css'; +import { motion } from 'framer-motion' + +// Using a custom className +// This prevents TOCInline/TOCCollapsible getting highlighted by mistake +const LINK_CLASS_NAME = 'table-of-contents__link toc-highlight'; +const LINK_ACTIVE_CLASS_NAME = 'table-of-contents__link--active'; + +export default function TOC({className, ...props}: Props): JSX.Element { + return ( + + + + ); +} diff --git a/src/theme/TOC/styles.module.css b/src/theme/TOC/styles.module.css new file mode 100644 index 000000000..4b6d2bcc6 --- /dev/null +++ b/src/theme/TOC/styles.module.css @@ -0,0 +1,23 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +.tableOfContents { + max-height: calc(100vh - (var(--ifm-navbar-height) + 2rem)); + overflow-y: auto; + position: sticky; + top: calc(var(--ifm-navbar-height) + 1rem); +} + +@media (max-width: 996px) { + .tableOfContents { + display: none; + } + + .docItemContainer { + padding: 0 0.3rem; + } +} diff --git a/static/image/circle.svg b/static/image/circle.svg new file mode 100644 index 000000000..3dd2d7ff1 --- /dev/null +++ b/static/image/circle.svg @@ -0,0 +1,8 @@ + + + +