bi-designer设计

bi-designer 是阿里数据中台团队自研的前端搭建引擎,基于它开发了阿里内部最大的数据分析平台,以及阿里云上的 QuickBI。

bi-designer 目前没有开源,因此文中使用的私有 npm 源 @alife/bi-designer 是无法在公网访问的。

本文介绍 bi-designer 设计器的使用 API。

bi-designer 设计有如下几个特点:

  • 心智统一:编辑模式与渲染模式统一
  • 通用搭建:支持接入任意通用 npm 组件
  • 低入侵:围绕数据分析能力做了增强,但对组件代码无入侵

渲染画布

做搭建,第一步是将画布渲染出来,需要用到 DesignerCanvas 组件:

1import { Designer, Canvas } from '@alife/bi-designer' 2export () => ( 3 <Designer> 4 <Canvas /> 5 </Designer> 6)
  • Designer:数据容器,用于管理渲染引擎数据流。
    • 参数 defaultPageSchema:页面 DSL 默认值。
    • 参数 defaultMode:控制编辑渲染状态,edit or render
  • Canvas:渲染画布的所有组件,会根据 DSL 结构将组件一一渲染出来。

编辑模式

编辑模式 = 渲染画布(编辑模式)+ 拓展一些自定义面板。

1import { Designer, Canvas } from '@alife/bi-designer' 2 3export () => ( 4 <Designer defaultMode="edit"> 5 <div>Header</div> 6 <Canvas /> 7 <div>Footer</div> 8 </Designer> 9)

编辑模式的拓展采用了 JSX 模式,没有增加任何新的语法,只要放置任意数量的组件,并将画布 Canvas 摆放在想要的位置即可。

defaultMode 描述了当前引擎所处状态,有 editrender 两个可选值,可以通过 { mode } = useDesigner(modeSelector) 获取。bi-designer 没有对 mode 做任何特殊处理,我们可以在 panel、组件中判断不同的 mode 走不同的逻辑,以此区分编辑与渲染态。

页面 DSL 结构

pageSchema 描述了页面 DSL 信息,其结构是一个 Map<组件 id, 组件实例信息>

这里统一一下名词:

  • 组件实例信息:componentInstance
  • 组件元信息:componentMeta

那么 pageSchema 的结构大致如下:

1{ 2 "componentInstances": { 3 "1": { 4 "id": "1", 5 "componentName": "root", 6 }, 7 "2": { 8 "id": "2", 9 "parentId": "1", 10 "componentName": "button", 11 } 12 } 13}

根据 id parentId 关系描述了组件父子关系,对于同一个父节点在流式布局下的顺序,还会增加 index 标记顺序。

注册组件

DSL 描述信息中最重要的是 componentName,为了告诉渲染引擎这个组件是什么,我们需要将组件元信息(componentMetas)传递给 Designer

1import { Designer, Canvas, Interfaces } from '@alife/bi-designer' 2 3export () => ( 4 <Designer componentMetas={componentMetas}> 5 <Canvas /> 6 </Designer> 7) 8 9const componentMetas: Interfaces.ComponentMetas = { 10 button: { 11 componentName: 'button', 12 element: Button 13 } 14}

关于 componentMeta 会在下一篇精读详细介绍,这里只说明两个最重要的属性:

  • componentName:组件名,唯一。
  • element:组件 UI 对象,对应一个 React 组件实例。

注意这里就留下了不少拓展空间,componentMetas 可以存储在服务端,element 可以远程异步加载,也可以在项目代码中固化,但传递给渲染引擎的 API 是固定的。

布局

bi-designer 支持流式布局、磁贴布局、自由布局三种模式,通过 Designer.layout 属性定义:

1import { Designer, Canvas, Interfaces } from '@alife/bi-designer' 2import { LayoutMover } from '@alife/bi-designer-stream-layout' 3 4export () => ( 5 <Designer layout={LayoutMover}> 6 <Canvas /> 7 </Designer> 8)

我们提供了三种不同的布局包,切换对应的包即可切换布局,你甚至可以再包裹一层,通过代码控制在运行时切换布局。

layout 会包裹在每个组件外层,无论是流式、磁贴还是自由布局,都可以通过附着在每个组件外层来实现。

操作/获取画布内容

只要在数据容器 Designer 下,就可以通过 useDesigner() 获取画布信息或者修改画布内容。

举个例子,比如实现组件配置面板,需要获取到 当前选中组件,以及实现操作 更新 DSL 中某个组件信息

1import { Designer, Canvas, useDesigner, selectedComponentsSelector } from '@alife/bi-designer'; 2 3const EditPanel = () => { 4 const { updateComponentById, selectedComponents } = 5 useDesigner(selectedComponentsSelector()); 6 7 // 在合适的时候调用 updateComponentById 更新 selectedComponents 8 9 // 渲染组件配置表单.. 10} 11 12export () => ( 13 <Designer> 14 <Canvas /> 15 <EditPanel /> 16 </Designer> 17)

我们在 Canvas 下面渲染了一个自定义组件 EditPanel 作为组件配置面板,这个配置面板中,最重要的是这块代码:

1import { useDesigner, selectedComponentsSelector } from '@alife/bi-designer'; 2const { updateComponentById, selectedComponents } = 3 useDesigner(selectedComponentsSelector());
  • useDesigner 是 React Hook,导出的函数都是静态的,不会因为画布信息变更而导致组件重渲染。
  • 如果需要监听一些会变化的元素,比如当前选中组件,就需要用 Selector 完成,当这些信息变更时,使用了这些 Selector 的组件也会重渲染,具体 Selector 有很多,比如:
    • selectedComponentsSelector: 当前选中的组件。
    • pageSchemaSelector: 当前画布 DSL。
    • modeSelector: 当前渲染模式。等等。
  • 对画布组件操作有几个重要的静态方法,包括:
    • updateComponentById: 更新某个 id 组件信息。
    • addComponent: 添加组件。
    • deleteComponent: 删除组件。
    • moveComponent: 移动组件。等等。
  • 除此之外,useDesigner 还提供了很多有用的方法,在用到时再介绍。

主题风格

通过 pageSchema.theme 设置主题风格:

1import { Designer } from '@alife/bi-designer' 2 3const App = () => ( 4 <Designer 5 defaultPageSchema={{ 6 theme: { primaryColor: '#333' } 7 }} 8 /> 9)

我们也可以在运行时使用 setTheme 动态修改主题风格,做到动态切换主题:

1const { setTheme, theme } = useDesigner(); 2 3return <Button onClick={() => { 4 setTheme({ 5 ...theme, 6 primaryColor: '#ffffff' 7 }) 8}} />

这些主题颜色,组件可以通过 css 变量拿到:

1.ok-button { 2 color: var(--primaryColor); 3}

获取组件数据

数据分析引擎中,组件是由数据驱动展示的,这些数据可能来自 OLAP 数据集,或者普通 URL 接口,但无论如何数据都是一个组件重要组成部分,因此对组件的取数与数据操作是 bi-designer 的一个重点。

可以利用 fetchStateSelector 获取任意组件的数据信息,包括取数状态、数据、是否有查询错误等:

1import { useDesigner, fetchStateSelector } from '@alife/bi-designer'; 2 3const App = () => { 4 const { fetchState } = useDesigner(fetchStateSelector(componentInstance.id)); 5 6 console.log( 7 fetchState.isFetching, // 是否在取数中 8 fetchState.isFilterReady, // 筛选条件是否准备好了 9 fetchState.data, // 取数结果 10 fetchState.error, // 取数错误,如果取数阶段报错的话 11 ) 12}

bi-designer 将所有组件的取数状态统一管理,因此可以跨组件获取数据信息,实现一些复杂需求:比如某些组件配置面板要获取组件取数结果填充配置表单。

组件加载器

组件加载器 ComponentLoader 可以加载任意组件, Canvas 就是基于此实现的。

加载画布中已有组件

通过申明 id 加载一个画布中已有组件,与其共享同一套数据:

1import { ComponentLoader } from '@alife/bi-designer' 2const App = () => { 3 return <ComponentLoader id="some-id-already-exist" /> 4}

加载一个额外的新组件

如果这个组件不需要响应事件,只是做简单的渲染,那就不需要记录到数据流中,此时仅申明 componentName 即可:

1import { ComponentLoader } from '@alife/bi-designer' 2const App = () => { 3 return <ComponentLoader componentName="button" /> 4}

但这种方式加载的组件存在如下问题:

  • 其组件 id 不会存储到 pageSchema ,后端可能无法做一些校验。
  • 无法响应事件,因为事件响应前提是组件信息存在于 pageSchema 中。

加载一个有事件功能的额外新组件

通过申明 idcomponentName 加载一个全新组件,为了在其销毁时做有效清理,请将其 id 记录到 useKeepComponentLoaders 中。

1import { ComponentLoader, useDesigner } from '@alife/bi-designer' 2const App = () => { 3 const { useKeepComponentLoaders } = useDesigner(); 4 useKeepComponentLoaders(["1"]) 5 6 return <ComponentLoader id="1" componentName="button" /> 7}

通过此方式加载的组件会在其渲染时记录到 pageSchema 中。

注意,此时 id 与仅写一个 id 时含义不同,这个 id 在当前父组件作用域下唯一就可以。

全屏功能

所有组件实例都可以存在副本,共享一套状态数据,可以通过 ComponentLoader 随时渲染一个组件副本:

1import { ComponentLoader } from '@alife/bi-designer' 2 3// ... 任意可拿到 componentInstance 处 4return ( 5 <ComponentLoader id={componentInstance.id} /> 6)

那么全屏就是将组件渲染到一个新容器内,非常 easy。

局部配置覆盖

可以通过 DesignerProvider 实现干涉其子元素 useDesigner 获取信息的能力:

1import { DesignerProvider, ComponentLoader } from '@alife/bi-designer'; 2 3// 某个组件内,或者某个 UI 内以 render 模式加载组件 4// ... 5return ( 6 <DesignerProvider mode="render"> 7 <ComponentLoader id={id} /> 8 </DesignerProvider> 9)

举个例子,比如在编辑模式下要全屏预览组件,可以通过 ComponentLoader + id 把某个画布组件实例渲染到弹出的 Modal 中,但问题是当前属于编辑模式,组件还可以被拖拽甚至响应编辑效果,我们只想让局部变成渲染状态,怎么做呢?

答案就是通过 DesignerProvider 包裹这个 Modal,这个 Modal 内部无论是组件还是其他 Panel 代码通过 const { mode } = useDesigner(modeSelector) 拿到的值都会被强制覆盖为 render

配置国际化

国际化信息在 pageSchema.i18n 定义:

1import { Designer } from '@alife/bi-designer' 2 3const App = () => ( 4 <Designer 5 defaultPageSchema={{ 6 i18n: { 7 "zh-CN": { 8 你好: "你好", 9 中国: "中国" 10 }, 11 "en-US": { 12 你好: "Hello", 13 中国: "China" 14 } 15 } 16 }} 17 defaultLocaleKey="zh-CN" 18 /> 19)
  • defaultLocaleKey: 默认国际化语言,可以通过 { setLocaleKey } = useDesigner() 动态改变。

这样在 DSL 中通过描述 JSExpression 表达式的 this.i18n 访问:

1{ 2 "componentInstances": { 3 "1": { 4 "id": "1", 5 "componentName": "button", 6 "props": { 7 "text": { 8 "type": "JSExpression", 9 "value": "this.i18n['你好']" 10 } 11 } 12 } 13 } 14}

容器拓展组件 props

componentMeta.container 可以定义组件外层容器,但有的时候我们想在容器做一点事情,比如获取宽高,以 props 的方式传递给子组件。

因为子组件以 children 的方式书写不易拓展,因此提供了 PropsProvider 来拓展子组件拿到的 props:

1import { Interfaces, PropsProvider } from '@alife/bi-designer' 2 3const ComponentContainer = ({ children }) => { 4 return ( 5 // 注入 width 和 height 6 <PropsProvider width={100} height={100}> 7 {children} 8 </PropsProvider> 9 ) 10} 11 12const Element = ({ width, height }) => { 13 // width=100 14 // height=100 15} 16 17const componentMeta: Interfaces.ComponentMeta = { 18 element: Element, 19 container: ComponentContainer 20};

上面的例子中,因为 container 注入了 width,因此组件可以通过 props.width 拿到容器注入的值。

撤销重做

撤销重做按钮在基于每个搭建系统都有,在 bi-designer 的使用方式是这样:

1import { useDesigner } from '@alife/bi-designer' 2 3export default () => { 4 const { undo, redo } = useDesigner() 5 6 // 撤销调用 undo() 7 // 重做调用 redo() 8}

是不是觉得很简单?是的,因为所有值得撤销重做的操作在引擎内部使用了 HistoryManager 管理,因此引擎知道每一个可以被撤销或者重做的操作,直接调用函数即可。

组件复制

执行 copyComponent 命令即可复制组件,比如:

1const App() { 2 const { copyComponent } = useDesigner() 3 4 // 复制组件 copyComponent(componentInstance) 5}

copyComponent 的参数分别为:

1function copyComponent( 2 componentInstance?: ComponentInstance, 3 parentId?: string, 4 index?: number 5)
  • 如不指定 parentId ,默认复制到自己父元素下。
  • 如不指定 index ,默认复制到当前元素下方。

组件模版

如果觉得某些组件配置可能被复用,可以在画布组件右上角增加一个 “添加到组件模版” 按钮,bi-designer 也提供了生成、添加组件模版的方法。

创建组件模版

利用 createCombine 函数从画布中已有组件创建出组件模版,也可以将其生成结果持久化,作为一个固定的组件模版:

1const ComponentContainer: Interfaces.InnerComponentElement = ({ componentInstance }) => { 2 const { createCombine } = useDesigner(); 3 4 const setToCombine = React.useCallback(() => { 5 // 创建组件模版 6 const combine = createCombine(componentInstance.id) 7 }, [createCombine]); 8}

createCombine 的参数就是画布中组件的 id

添加组件模版到画布

利用 addCombine 函数将组件模版添加到画布,第一个参数就是上面生成的 combine 对象:

1const App = () => { 2 const { addCombine } = useDesigner(); 3 4 const addComponent = React.useCallback(() => { 5 // 创建组件模版 6 const combine = addCombine(combine, parentId) 7 }, [addCombine]); 8}

渲染完成标识

当画布中所有组件都完成渲染了,可能要做一些监控上报,或者告诉截图软件可以截图了,bi-designer 提供了这种回调时机 onRendered:

1import { Designer } from '@alife/bi-designer' 2 3const App = () => ( 4 <Designer 5 onRendered={errors => { 6 errors.map(each => { 7 // 错误组件 id 8 console.log(each.id) 9 10 // 错误信息 11 console.log(each.error) 12 }) 13 // 渲染完毕 14 }} 15 /> 16)
  • errors: 如果有组件代码报错,引擎会吞掉这个错误保证其他组件正常渲染,并把错误组件的 id 和错误信息返回到这里。

自定义数据流

如果 useDesigner 提供的数据流无法满足业务需要,可以通过进行自定义拓展。

1. 拓展字段

举个例子,我们需要新增一个 edges 字段描述当前画布中有哪些 “边节点”:

1import { Designer } from '@alife/bi-designer'; 2const App = ({ defaultPageSchema }) => ( 3 <Designer defaultPageSchema={{ 4 ...defaultPageSchema, 5 edges: [] 6 }} /> 7)

可以看到,只要任意拓展 pageSchema 即可。

2. 通过 useDesigner 拿到拓展字段

首先定义一个 edgesSelector

1import { DesignerState } from '@alife/bi-designer'; 2export const edgesSelector = () => (state: DesignerState) => { 3 return { 4 // 从 pageSchema.edges 读取 edges 5 edges: state.pageSchema?.edges as Edge[], 6 }; 7};

在需要读取的地方结合 useDesigner

1import { useDesigner } from '@alife/bi-designer'; 2import { edgesSelector } from './selector' 3const Panel = () => { 4 // 自带类型 5 const { edges } = useDesigner(edgesSelector()) 6}

3. 通过 useDesigner 修改拓展字段

通过 setPageSchema 更新拓展字段:

1import { useDesigner } from '@alife/bi-designer'; 2const Panel = () => { 3 const { setPageSchema } = useDesigner() 4 5 const handleChangeEdges = React.useCallback(newEdges => { 6 setPageSchema(pageSchema => ({ 7 ...pageSchema, 8 newEdges 9 })) 10 }, [setPageSchema]) 11}

总结一下,这个拓展字段由业务定义,透过 useDesigner 读与改,使业务数据管理方式更聚合。

存储临时非结构化数据

对于非结构化数据比如组件 ref 是不能存储到数据流的,既不能使用 setPageSchema,也不能调用 updateComponentId 存储到 componentInstance 中。

此时可以利用 temporary 进行临时数据存取,要注意非结构化数据是无法监听变化的,引用永远保持不变:

1import { useDesigner } from '@alife/bi-designer'; 2const App = () => ( 3 const { temporary } = useDesigner() 4 // 写 5 temporary.set('component1', ref) 6 // 读 7 console.log(temporary.get('component1')) 8)

temporary 本质是个 Map,所以拥有 Map 类型所有语法。

拦截画布操作

如果你限制某个低配版本只能在画布使用最多 50 个组件,我们需要阻止画布超过 50 个组件的添加,这个场景可以通过 DesignerProps 生命周期可以对画布操作进行拦截。

shouldAddComponents() 返回 false 可以阻止画布添加组件:

1import { Designer } from '@alife/bi-designer' 2const App = () => ( 3 <Designer 4 shouldAddComponents={({addedComponentInstancesArray, pageSchema}) => { 5 // 阻止添加 6 return false 7 }} 8 /> 9)
  • addedComponentInstancesArray :添加的组件, ComponentInstance[] 类型。

shouldMoveComponents() 返回 false 可以阻止画布移动组件:

1import { Designer } from '@alife/bi-designer' 2const App = () => ( 3 <Designer 4 shouldmoveComponents={({movedComponentInstancesArray, targetComponentInstance, pageSchema}) => { 5 // 阻止移动 6 return false 7 }} 8 /> 9)
  • movedComponentInstancesArray :移动的组件,ComponentInstance[] 类型。
  • taragetComponentInstance :要移动到的父组件实例信息, ComponentInstance 类型。

shouldDeleteComponents() 返回 false 可以阻止画布删除组件:

1import { Designer } from '@alife/bi-designer' 2const App = () => ( 3 <Designer 4 shouldDeleteComponents={({deletedComponentInstancesArray, pageSchema}) => { 5 // 阻止删除 6 return false 7 }} 8 /> 9)
  • deletedComponentInstancesArray :删除的组件, ComponentInstance[] 类型。

仅刷新可视区域组件

默认组件都会以按需加载的方式渲染,即对于不在可视区域的组件,不会触发任何重渲染,以此提升交互操作的效率,以及首屏速度。

对于筛选条件等可能影响到其他组件的组件,可以通过 ComponentMeta.keepActive 强制保持激活状态:

1import { Interfaces } from '@alife/bi-designer' 2const componentMeta: Interfaces.ComponentMeta = { 3 keepActive: true 4}
  • keepActive:组件始终保持激活状态,即不出现在可视区域也会被渲染与响应刷新,默认关闭。

对于特殊场景比如截图,可能要求所有组件强制为 active 状态,可以通过 forceActive 函数实现:

1import { Interfaces, useDesigner } from '@alife/bi-designer' 2const Test: Interfaces.ComponentElement = () => { 3 const { forceActive, cancelForceActive } = useDesigner() 4 5 // forceActive() 强制所有组件 active 6 // cancelForceActive() 取消强制 active,组件根据实际情况 active 7};

可以通过 getSnapshot().actives 获取任意组件当前瞬时 active 状态:

1import { useDesigner } from '@alife/bi-designer' 2const Test = () => { 3 const { getSnapshot, id } = useDesigner() 4 5 // 当前组件激活状态 6 const active = getSnapshot().actives[id] 7};

上下文数据对象

组件 DSL 描述中,表达式类型(JSExpression)可以通过 this. 访问到上下文数据对象。上下文数据对象符合如下规则:

  • 任何组件都通过配置 ComponentMeta.stateful 持有上下文。
  • 画布根节点 root 一定是 stateful 的。
  • JSFunctionJSExpression 都可通过 this.state 访问上下文, this.setState 修改上下文。

举例子:

1// 初始化 pageSchema 2const defaultPageSchema: Interfaces.PageSchema = { 3 componentInstances: { 4 test1: { 5 id: 'test1', 6 componentName: 'test', 7 parentId: 'jtw4x8ns', 8 index: 0, 9 props: { 10 variable: { 11 type: 'JSExpression', 12 value: 'this.state.variable + "%"', 13 }, 14 onClick: { 15 type: 'JSFunction', 16 value: 'function onClick() { this.setState({ variable: 5 }) }', 17 }, 18 }, 19 } 20 } 21};

这个例子中,组件调用 this.props.onClick 会修改上下文 a=5 ,触发后,其 this.props.variable 拿到的值会变为 5% 。

任何组件或容器只要设置了 stateful 就可以持有状态:

1import { Interfaces } from '@alife/bi-designer' 2const statefulComponentMeta: Interfaces.ComponentMeta = { 3 stateful: true 4}

被有状态的容器包裹的组件 this.statethis.setState 都局限在当前状态容器内,也就是当前状态容器内组件的 state 是互通的,且一个有状态容器与外部环境是隔离的,可以独立运行。

工具类拓展

工具类拓展可以通过上下文访问,如下是拓展方式:

1import { Interfaces } from '@alife/bi-designer' 2// DSL 中增加 utils 描述 3const defaultPageSchema: Interfaces.PageSchema = { 4 utils: [ 5 { 6 name: 'format', 7 type: 'function', 8 content: `function format(str){ return str + '%' }`, 9 }, 10 ] 11};
  • name :工具函数名。
  • type :类型,包括 npmumdfunction
  • content :内容。

用法:

1JSFunctionJSExpression 都可以通过 this.utils 访问工具类拓展函数,比如 2// DSL 中增加 Expression 描述 3const defaultPageSchema: Interfaces.PageSchema = { 4 componentInstances: { 5 test: { 6 id: 'tg43g42f', 7 componentName: 'expressionComponent', 8 index: 0, 9 props: { 10 variable: { 11 type: 'JSExpression', 12 value: 'this.utils.format("100")', 13 } 14 }, 15 }, 16 }, 17};

上面的例子中,组件拿到的 props.variable 值为 100% 。

总结

如果你认真看完了全文,就会发现,bi-designer 是一个集成了数据流的开发框架,而不仅是一个渲染引擎,但却可以和你现有的业务代码友好相处,没有入侵性。

像渲染完成标识、按需渲染、组件加载器、局部配置覆盖等功能是强依赖渲染引擎存在的,因此较难在剥离渲染引擎的条件下转换为代码,因为做 BI 分析工具毕竟不是做研发提效用,业务上没有出码的必要,因此我们会做许多依赖渲染引擎的能力增强。

更多数据分析特性的功能将在下一个话题 API 之组件说明。

文章来源: https://github.com/ascoders/weekly/blob

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )